├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Exceptions └── Exception.php ├── LICENSE.md ├── README.md ├── SimpleThread.php ├── Tests └── ThreadTest.php ├── Thread.php ├── ThreadPool.php ├── composer.json ├── docs ├── en │ ├── 0.Index.md │ ├── 1.Main.md │ ├── 2.Events.md │ ├── 3.Options.md │ ├── 4.Hooks.md │ ├── 5.Signals.md │ ├── 6.Other.md │ └── Examples.md └── ru │ ├── 0.Index.md │ ├── 1.Main.md │ ├── 2.Events.md │ ├── 3.Options.md │ ├── 4.Hooks.md │ ├── 5.Signals.md │ ├── 6.Other.md │ └── Examples.md ├── examples ├── example.php └── speed_test.php └── phpunit.xml.dist /.gitignore: -------------------------------------------------------------------------------- 1 | # Composer and packages 2 | composer.phar 3 | composer.lock 4 | vendor/ 5 | 6 | # PhpUnit 7 | phpunit.xml 8 | code_coverage/ 9 | 10 | # Ignore IDE settings 11 | *.iml 12 | *.ipr 13 | *.iws 14 | .idea/ 15 | 16 | # Ignore temp work dirs and files 17 | _/ 18 | *~ 19 | ~* 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3.3 5 | - 5.3 6 | - 5.4 7 | - 5.5 8 | 9 | before_script: 10 | - composer install 11 | 12 | script: phpunit --configuration phpunit.xml.dist --group=unit 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | ## Version 1.2 (work in progress) 5 | 6 | 7 | ## Version 1.1 (28.05.2013) 8 | - **MINOR:** Some refactoring, fixes and improvements (amal) 9 | 10 | 26.05.2013 11 | - **MINOR:** `Thread::cleanAll()` method added (amal) 12 | - **IMPROVED:** Better waiting for dead child process with `pcntl_waitpid` (amal) 13 | - **IMPROVED:** The child is now explicitly notify a parent before death (amal) 14 | - **MINOR:** Support for the updated AzaLibEvent API (amal) 15 | - **CHANGED:** Confirmation mode (eventLocking) removed for support for the newest libevent (amal) 16 | 17 | 25.05.2013 18 | - **FEATURE:** Support for the newest libevent has been added (amal) 19 | - **MINOR:** Added test for huge events data (amal) 20 | 21 | 24.05.2013 22 | - **IMPROVED:** Separate detailed documentation in two languages (amal) 23 | 24 | 16.05.2013 25 | - **MINOR:** Small improvements and fixes (amal) 26 | - **IMPROVED:** IPC optimizations (8-10% speedup) (amal) 27 | - **FEATURE:** Simple API for closure thread - `SimpleThread` class (amal) 28 | - **FEATURE:** New `onCleanup` hook (amal) 29 | - **FEATURE:** Thread now can be configured externally (amal) 30 | 31 | 13.05.2013 32 | - **FEATURE:** Threads statistics collection + API for accessing it via pool (Issue #4, amal) 33 | - **MINOR:** Additional tests (empty argument/result, thread hooks) (amal) 34 | 35 | 12.05.2013 36 | - **FIXED:** Inconsistent behaviour with `multitask=false` removed (Issue #6, amal) 37 | 38 | 11.05.2013 39 | - **IMPROVED:** Full code coverage (amal) 40 | - **IMPROVED:** Speedup in IPC. Overall, IPC accelerated by 15-50% compared with the v1.0 release (amal) 41 | - **FIXED:** Job identifier added to discard orphaned results (amal) 42 | - **MINOR:** Small typo fix (amal) 43 | 44 | 09.05.2013 45 | - **MINOR:** Error callbacks for buffer events (amal) 46 | 47 | 05.05.2013 48 | - **FIXED:** Discarding of duplicate job packets. This could occur when the worker dies (amal) 49 | 50 | 04.05.2013 51 | - **FIXED:** Thread can no longer start new job while result of previous is not fetched (amal) 52 | - **MINOR:** More tests for sync mode, better groups of tests (amal) 53 | - **FIXED:** Confirmed bug fix for resources damage with child death (Issue #2, amal) 54 | 55 | 03.05.2013 56 | - **IMPROVED:** Thread pool structure API improvements (amal) 57 | 58 | 28.04.2013 59 | - **CHANGED:** Debug flag moved to the end of arguments list in pool and thread constructors (amal) 60 | 61 | 27.04.2013 62 | - **FIXED:** Master pipe read event cleanup after worker death added (cause can be damaged) (amal) 63 | - **IMPROVED:** New tests, better feature and code coverage (amal) 64 | - **CHANGED:** Main processing errors handling now works without exceptions (amal) 65 | - **FIXED:** Removed incorrect event loop cleanup in some cases (amal) 66 | 67 | 26.04.2013 68 | - **CHANGED:** More thoughtful public getters instead of public/protected properties (amal) 69 | - **MINOR:** Better PhpDocs and some code reorganization (amal) 70 | - **IMPROVED:** Added cleanup for all other (redundant) threads and pools in child after forking (amal) 71 | 72 | 25.04.2013 73 | - **FIXED:** Signal handler in master process with many different threads now works normally (amal) 74 | - **FIXED:** Confirmation mode (eventLocking) for worker events fixed (amal) 75 | - **MINOR:** Debugging improvements. PID update for every call, more data in logs (amal) 76 | 77 | 21.04.2013 78 | - **MINOR:** Many new tests, better feature and code coverage (amal) 79 | - **MINOR:** Many small optimizations, improvements and code cleanup (amal) 80 | - **FEATURE:** Optional arguments mapping (amal) 81 | - **FIXED:** Support for several different threads used at one time not in pool (amal) 82 | - **FIXED:** Worker interval to check parent process did not restarted after first iteration (amal) 83 | - **FIXED:** Worker job waiting timeout missed the first iteration (amal) 84 | - **FIXED:** Tags stripping for debug messages could corrupt them (`strip_tags` replaced with regex) (amal) 85 | 86 | 17.04.2013 87 | - **MINOR:** Better event listeners cleanup (amal) 88 | - **MINOR:** `@return $this` in PhpDocs (amal) 89 | 90 | 14.04.2013 91 | - **FIXED:** Fixed issue with exceptions in events triggering (amal) 92 | 93 | 13.04.2013 94 | - **FIXED:** `Thread::onFork` hook is now properly called in child after forking (Issue #7, amal) 95 | - **MINOR:** Small thread improvements with code cleanup and refactoring (amal) 96 | - **MINOR:** Unit tests structure improvements (amal) 97 | 98 | 09.04.2013 99 | - **IMPROVED:** `Thread::forkThread` method visibility changed to protected (Issue #3, amal) 100 | - **MINOR:** Small code style, comments and tests improvements (amal) 101 | - **FIXED:** Remove dependency on Daemon::getTimeForLog (Issue #1, amal) 102 | 103 | 08.04.2013 104 | - **MINOR:** Massive README and CHANGELOG improvements (amal) 105 | 106 | 107 | ## Version 1.0 (27.02.2013) 108 | - **MINOR:** Refactoring and improvements (amal) 109 | - **FEATURE:** Travis CI support (amal) 110 | - **FEATURE:** Composer support (amal) 111 | 112 | 113 | ## Version 0.9 (01.08.2012) 114 | - **FIXED:** `pool->getState` visibility fix (protected => public) (amal) 115 | 116 | 10.04.12 117 | - **FIXED:** 4096 bytes results limit (Issue amal/AzaThread#5, amal) 118 | - **FIXED:** Fixed results error with SIGCHLD (Issue amal/AzaThread#4, amal) 119 | - **CHANGED:** Massive component rewrite. Namespaces and PSR-0. (amal) 120 | 121 | 122 | ## Version 0.1 (25.01.2012) 123 | 21.12.11 124 | - **FEATURE:** Thread events with pool (amal) 125 | 126 | 13.12.11 127 | - First public release 128 | -------------------------------------------------------------------------------- /Exceptions/Exception.php: -------------------------------------------------------------------------------- 1 | 12 | * @license MIT 13 | */ 14 | class Exception extends BaseExeption {} 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013 Amal Samally and AzaGroup 4 | https://github.com/Anizoptera/AzaThread 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is furnished 11 | to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AzaThread 2 | ========= 3 | 4 | Simple and powerful threads emulation component for PHP (based on forks). 5 | Old name - CThread. 6 | 7 | https://github.com/Anizoptera/AzaThread 8 | 9 | [![Build Status][TravisImage]][Travis] 10 | 11 | 12 | Table of Contents 13 | ----------------- 14 | 15 | 1. [Introduction](#introduction) 16 | 2. [Requirements](#requirements) 17 | 3. [Installation](#installation) 18 | 4. [Documentation and examples](#documentation-and-examples) 19 | 5. [Tests](#tests) 20 | 6. [Credits](#credits) 21 | 7. [License](#license) 22 | 8. [Links](#links) 23 | 24 | 25 | Introduction 26 | ------------ 27 | 28 | **Features:** 29 | 30 | * Uses [forks](http://php.net/pcntl-fork) to operate asynchronously; 31 | * Supports synchronous compatibility mode if there are no required extensions; 32 | * Reuse of the child processes; 33 | * Full exchange of data between processes. Sending arguments, receiving results; 34 | * Transfer of events between the "thread" and the parent process; 35 | * Working with a thread pool with preservation of multiple use, passing arguments and receiving results; 36 | * Uses [libevent][] with socket pairs for efficient inter-process communication; 37 | * Supports two variants of data serialization for transfer (igbinary, native php serialization); 38 | * Errors handling; 39 | * Timeouts for work, child process waiting, initialization; 40 | * Maximum performance and customization; 41 | 42 | 43 | Requirements 44 | ------------ 45 | 46 | * PHP 5.3.3 (or later); 47 | * Unix system; 48 | * [libevent][]; 49 | * [pcntl](http://php.net/pcntl); 50 | * [posix](http://php.net/posix); 51 | * [AzaLibevent](https://github.com/Anizoptera/AzaLibEvent) - will be installed automatically with composer; 52 | * [AzaSocket](https://github.com/Anizoptera/AzaSocket) - will be installed automatically with composer; 53 | * [AzaCliBase](https://github.com/Anizoptera/AzaCliBase) - will be installed automatically with composer; 54 | 55 | NOTE: You can use synchronous compatibility mode even without requirements (or on windows, for example). 56 | 57 | 58 | Installation 59 | ------------ 60 | 61 | The recommended way to install AzaThread is [through composer](http://getcomposer.org). 62 | You can see [package information on Packagist][ComposerPackage]. 63 | 64 | ```JSON 65 | { 66 | "require": { 67 | "aza/thread": "~1.0" 68 | } 69 | } 70 | ``` 71 | 72 | 73 | Documentation and examples 74 | -------------------------- 75 | 76 | See [full documentation](docs/en/0.Index.md) and [main examples](docs/en/Examples.md). Documentation is available in several languages! 77 | 78 | Other examples can be seen in the file [examples/example.php](examples/example.php) and in unit test [Tests/ThreadTest.php](Tests/ThreadTest.php). 79 | 80 | You can also run the performance tests, choose the number of threads and pick the best settings for your system configuration by using [examples/speed_test.php](examples/speed_test.php). 81 | 82 | 83 | Tests 84 | ----- 85 | 86 | Tests are in the `Tests` folder. 87 | To run them, you need PHPUnit. 88 | Example: 89 | 90 | $ phpunit --configuration phpunit.xml.dist 91 | 92 | 93 | Credits 94 | ------- 95 | 96 | AzaThread is a part of [Anizoptera CMF][], written by [Amal Samally][] (amal.samally at gmail.com) and [AzaGroup][] team. 97 | 98 | 99 | License 100 | ------- 101 | 102 | Released under the [MIT](LICENSE.md) license. 103 | 104 | 105 | Links 106 | ----- 107 | 108 | * [Mail list](mailto:azathread@googlegroups.com) (via [Google Group](https://groups.google.com/forum/#!forum/azathread)) 109 | * [Composer package][ComposerPackage] 110 | * [Last build on the Travis CI][Travis] 111 | * [Project profile on the Ohloh](https://www.ohloh.net/p/AzaThread) 112 | * (RU) [AzaThread — многопоточность для PHP с блэкджеком](http://habrahabr.ru/blogs/php/134501/) 113 | * Other Anizoptera CMF components on the [GitHub][Anizoptera CMF] / [Packagist](https://packagist.org/packages/aza) 114 | * (RU) [AzaGroup team blog][AzaGroup] 115 | 116 | 117 | 118 | [libevent]: http://php.net/libevent 119 | 120 | [Anizoptera CMF]: https://github.com/Anizoptera 121 | [Amal Samally]: http://azagroup.ru/about/#amal 122 | [AzaGroup]: http://azagroup.ru/ 123 | [ComposerPackage]: https://packagist.org/packages/aza/thread 124 | [TravisImage]: https://secure.travis-ci.org/Anizoptera/AzaThread.png?branch=master 125 | [Travis]: http://travis-ci.org/Anizoptera/AzaThread 126 | -------------------------------------------------------------------------------- /SimpleThread.php: -------------------------------------------------------------------------------- 1 | 12 | * @license MIT 13 | */ 14 | class SimpleThread extends Thread 15 | { 16 | /** 17 | * Whether the thread will wait for next tasks. 18 | * Preforked threads are always multitask. 19 | * 20 | * @see prefork 21 | */ 22 | protected $multitask = false; 23 | 24 | /** 25 | * Perform pre-fork, to avoid wasting resources later. 26 | * Preforked threads are always multitask. 27 | * 28 | * @see multitask 29 | */ 30 | protected $prefork = false; 31 | 32 | /** 33 | * Maximum timeout for master to wait for the job results 34 | * (in seconds, can be fractional). 35 | * Set it to less than one, to disable. 36 | */ 37 | protected $timeoutMasterResultWait = 10; 38 | 39 | 40 | /** 41 | * Callable 42 | * 43 | * @var callable 44 | */ 45 | protected $callable; 46 | 47 | 48 | 49 | /** 50 | * Creates new thread with closure 51 | * 52 | * @param callable|Closure $callable
53 | * Thread closure (callable) 54 | *
55 | * @param array $options [optional]56 | * Thread options (array [property => value]) 57 | *
58 | * @param bool $debug [optional]59 | * Whether to output debugging information 60 | *
61 | * 62 | * @return static 63 | */ 64 | public static function create($callable, array $options = null, $debug = false) 65 | { 66 | 67 | $options || $options = array(); 68 | $options['callable'] = $callable; 69 | return new static(null, null, $debug, $options); 70 | } 71 | 72 | 73 | /** 74 | * Prepares closure (only in PHP >= 5.4.0) 75 | * 76 | * @codeCoverageIgnore 77 | */ 78 | protected function onLoad() 79 | { 80 | // Prepare closure 81 | $callable = $this->callable; 82 | if ($callable instanceof Closure && method_exists($callable, 'bindTo')) { 83 | /** @noinspection PhpUndefinedMethodInspection */ 84 | $callable->bindTo($this, $this); 85 | } 86 | } 87 | 88 | /** 89 | * Callable cleanup 90 | */ 91 | protected function onCleanup() 92 | { 93 | $this->callable = null; 94 | } 95 | 96 | 97 | /** 98 | * Main processing. You need to override this method. 99 | * Use {@link getParam} method to get processing parameters. 100 | * Returned result will be available via {@link getResult} 101 | * in the master process. 102 | * 103 | * @internal 104 | */ 105 | function process() 106 | { 107 | return call_user_func_array( 108 | $this->callable, $this->getParams() 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Tests/ThreadTest.php: -------------------------------------------------------------------------------- 1 | 23 | * @license MIT 24 | */ 25 | class ThreadTest extends TestCase 26 | { 27 | /** @var bool */ 28 | protected static $defUseForks; 29 | 30 | /** @var int */ 31 | protected static $defIpcDataMode; 32 | 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public static function setUpBeforeClass() 38 | { 39 | self::$defUseForks = Thread::$useForks; 40 | self::$defIpcDataMode = Thread::$ipcDataMode; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | protected function setUp() 47 | { 48 | // Set values to default 49 | Thread::$useForks = self::$defUseForks; 50 | Thread::$ipcDataMode = self::$defIpcDataMode; 51 | 52 | // Cleanup 53 | EventBase::cleanAllLoops(); 54 | Thread::cleanAll(); 55 | gc_collect_cycles(); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public static function tearDownAfterClass() 62 | { 63 | // Set values to default 64 | Thread::$useForks = self::$defUseForks; 65 | Thread::$ipcDataMode = self::$defIpcDataMode; 66 | 67 | // Cleanup 68 | EventBase::cleanAllLoops(); 69 | Thread::cleanAll(); 70 | gc_collect_cycles(); 71 | } 72 | 73 | 74 | 75 | /** 76 | * Tests threads debugging 77 | * 78 | * @author amal 79 | * @group unit 80 | */ 81 | public function testDebug() 82 | { 83 | $this->expectOutputString(''); 84 | 85 | Thread::$useForks = false; 86 | 87 | $value = 'test12345'; 88 | 89 | // Test thread debug output 90 | $thread = new TestThreadReturnFirstArgument(); 91 | $this->assertSame(Thread::STATE_WAIT, $thread->getState()); 92 | $ref = new ReflectionMethod($thread, 'debug'); 93 | $ref->setAccessible(true); 94 | ob_start(); ob_start(); 95 | $thread->debug = true; 96 | $ref->invoke($thread, $value); 97 | $thread->debug = false; 98 | ob_get_clean(); 99 | $output = ob_get_clean(); 100 | $this->assertContains($value, $output); 101 | $thread->cleanup(); 102 | 103 | // Test thread pool debug output 104 | $className = get_class($thread); 105 | ob_start(); ob_start(); 106 | $pool = new ThreadPool($className); 107 | $ref = new ReflectionMethod($pool, 'debug'); 108 | $ref->setAccessible(true); 109 | $pool->debug = true; 110 | $ref->invoke($pool, $value); 111 | $pool->debug = false; 112 | ob_get_clean(); 113 | $output = ob_get_clean(); 114 | $this->assertContains($value, $output); 115 | $pool->cleanup(); 116 | } 117 | 118 | /** 119 | * Tests external configured thread 120 | * 121 | * @author amal 122 | * @group unit 123 | */ 124 | public function testExternalConfiguration() 125 | { 126 | Thread::$useForks = false; 127 | 128 | $property = 'timeoutWorkerJobWait'; 129 | $expected = -mt_rand(100, 999); 130 | 131 | $thread = new TestThreadReturnFirstArgument( 132 | null, null, false, array($property => $expected) 133 | ); 134 | $ref = new ReflectionProperty($thread, $property); 135 | $ref->setAccessible(true); 136 | $value = $ref->getValue($thread); 137 | $this->assertSame($expected, $value); 138 | 139 | $thread = new TestThreadReturnFirstArgument(); 140 | $value = $ref->getValue($thread); 141 | $this->assertNotSame($expected, $value); 142 | } 143 | 144 | 145 | 146 | #region Synchronous (fallback mode) tests 147 | 148 | /** 149 | * Simple thread test (sync mode) 150 | * 151 | * @author amal 152 | * @group unit 153 | */ 154 | public function testSyncThread() 155 | { 156 | Thread::$useForks = false; 157 | $this->processThread(false); 158 | } 159 | 160 | /** 161 | * Additional thread test 162 | * 163 | * @author amal 164 | * @group unit 165 | */ 166 | public function testSyncThread2() 167 | { 168 | Thread::$useForks = false; 169 | $this->processThread2(false); 170 | } 171 | 172 | /** 173 | * Thread test with big data (sync mode) 174 | * 175 | * @author amal 176 | * @group unit 177 | */ 178 | public function testSyncThreadWithBigData() 179 | { 180 | Thread::$useForks = false; 181 | $this->processThread(false, true); 182 | } 183 | 184 | /** 185 | * Thread test with childs (sync mode) 186 | * 187 | * @author amal 188 | * @group unit 189 | */ 190 | public function testSyncThreadWithChilds() 191 | { 192 | Thread::$useForks = false; 193 | $this->processThread(false, false, true); 194 | } 195 | 196 | /** 197 | * Thread test with childs and big data (sync mode) 198 | * 199 | * @author amal 200 | * @group unit 201 | */ 202 | public function testSyncThreadWithBigDataAndChilds() 203 | { 204 | Thread::$useForks = false; 205 | $this->processThread(false, true, true); 206 | } 207 | 208 | /** 209 | * Thread events test (sync mode) 210 | * 211 | * @author amal 212 | * @group unit 213 | */ 214 | public function testSyncThreadWithEvents() 215 | { 216 | $debug = false; 217 | 218 | Thread::$useForks = false; 219 | $this->processThreadEvent($debug); 220 | 221 | 222 | // Non-closure test 223 | $thread = new TestThreadEvents(null, null, $debug); 224 | 225 | $thread->bind( 226 | TestThreadEvents::EV_PROCESS, 227 | array($this, 'processEventCallback') 228 | ); 229 | 230 | $count = $this->getCount(); 231 | $thread->run(1)->wait(); 232 | 233 | $this->assertSame($count+3, $this->getCount()); 234 | $this->assertSame(Thread::STATE_WAIT, $thread->getState()); 235 | $this->assertTrue( 236 | $thread->getSuccess(), 237 | 'Job failure - #' . $thread->getLastErrorCode() 238 | . ' ' . $thread->getLastErrorMsg() 239 | ); 240 | 241 | $thread->cleanup(); 242 | } 243 | 244 | /** 245 | * Thread events test with big data (sync mode) 246 | * 247 | * @author amal 248 | * @group unit 249 | */ 250 | public function testSyncThreadWithEventsBigData() 251 | { 252 | Thread::$useForks = false; 253 | $this->processThreadEvent(false, true); 254 | } 255 | 256 | /** 257 | * Errorable thread test (sync mode) 258 | * 259 | * @author amal 260 | * @group unit 261 | */ 262 | public function testSyncThreadErrorable() 263 | { 264 | Thread::$useForks = false; 265 | $this->processThreadErrorable(false); 266 | } 267 | 268 | /** 269 | * Thread arguments mapping test (sync mode) 270 | * 271 | * @author amal 272 | * @group unit 273 | */ 274 | public function testSyncThreadArgumentsMapping() 275 | { 276 | Thread::$useForks = false; 277 | $this->processThreadArgumentsMapping(false); 278 | } 279 | 280 | /** 281 | * Multitask disabled thread test (sync mode) 282 | * 283 | * @author amal 284 | * @group unit 285 | * @ticket GitHub Anizoptera/AzaThread/#6 286 | */ 287 | public function testSyncThreadOnetask() 288 | { 289 | Thread::$useForks = false; 290 | $this->processThread(false, true, false, true); 291 | } 292 | 293 | /** 294 | * Closure thread test (sync mode) 295 | * 296 | * @author amal 297 | * @group unit 298 | */ 299 | public function testSyncThreadClosure() 300 | { 301 | Thread::$useForks = false; 302 | $this->processThreadClosure(false); 303 | } 304 | 305 | 306 | /** 307 | * Simple thread pool test (sync mode) 308 | * 309 | * @author amal 310 | * @group unit 311 | */ 312 | public function testSyncThreadPool() 313 | { 314 | Thread::$useForks = false; 315 | $this->processPool(false); 316 | } 317 | 318 | /** 319 | * Additional thread pool test (sync mode) 320 | * 321 | * @author amal 322 | * @group unit 323 | */ 324 | public function testSyncThreadPool2() 325 | { 326 | Thread::$useForks = false; 327 | $this->processPool2(false); 328 | } 329 | 330 | /** 331 | * Thread pool test with big data (sync mode) 332 | * 333 | * @author amal 334 | * @group unit 335 | */ 336 | public function testSyncThreadPoolWithBigData() 337 | { 338 | Thread::$useForks = false; 339 | $this->processPool(false, true); 340 | } 341 | 342 | /** 343 | * Thread pool test with childs (sync mode) 344 | * 345 | * @author amal 346 | * @group unit 347 | */ 348 | public function testSyncThreadPoolWithChilds() 349 | { 350 | Thread::$useForks = false; 351 | $this->processPool(false, false, true); 352 | } 353 | 354 | /** 355 | * Thread pool test with childs and big data (sync mode) 356 | * 357 | * @author amal 358 | * @group unit 359 | */ 360 | public function testSyncThreadPoolWithBigDataAndChilds() 361 | { 362 | Thread::$useForks = false; 363 | $this->processPool(false, true, true); 364 | } 365 | 366 | /** 367 | * Thread pool events test (sync mode) 368 | * 369 | * @author amal 370 | * @group unit 371 | */ 372 | public function testSyncThreadPoolWithEvents() 373 | { 374 | $debug = false; 375 | 376 | Thread::$useForks = false; 377 | $this->processPoolEvent($debug); 378 | 379 | 380 | // Non-closure test 381 | $pool = new ThreadPool( 382 | __NAMESPACE__ . '\TestThreadEvents', 383 | 1, null, null, $debug 384 | ); 385 | 386 | $pool->bind( 387 | TestThreadEvents::EV_PROCESS, 388 | array($this, 'processEventCallback') 389 | ); 390 | 391 | $threadId = $pool->run(1); 392 | $this->assertNotEmpty($threadId); 393 | 394 | $pool->wait(); 395 | 396 | $pool->cleanup(); 397 | } 398 | 399 | /** 400 | * Errorable thread pool test (sync mode) 401 | * 402 | * @author amal 403 | * @group unit 404 | */ 405 | public function testSyncThreadPoolErrorable() 406 | { 407 | Thread::$useForks = false; 408 | $this->processPoolErrorable(false); 409 | } 410 | 411 | /** 412 | * Thread pool test with external stop (sync mode) 413 | * 414 | * @author amal 415 | * @group unit 416 | */ 417 | public function testSyncThreadPoolErrorableExternal() 418 | { 419 | Thread::$useForks = false; 420 | $this->processPoolErrorableExternal(false); 421 | } 422 | 423 | /** 424 | * Thread pool test with multitask disabled (sync mode) 425 | * 426 | * @author amal 427 | * @group unit 428 | * @ticket GitHub Anizoptera/AzaThread/#6 429 | */ 430 | public function testSyncPoolOnetask() 431 | { 432 | Thread::$useForks = false; 433 | $this->processPool(false, false, true, true); 434 | } 435 | 436 | 437 | /** 438 | * Test for exception in events triggering (sync mode) 439 | * 440 | * @author amal 441 | * @group unit 442 | */ 443 | public function testSyncExceptionInEventListener() 444 | { 445 | Thread::$useForks = false; 446 | $this->processExceptionInEventListener(false); 447 | } 448 | 449 | /** 450 | * Two different threads test (sync mode) 451 | * 452 | * @author amal 453 | * @group unit 454 | */ 455 | public function testSyncTwoDifferentThreads() 456 | { 457 | Thread::$useForks = false; 458 | $this->processTwoDifferentThreads(false); 459 | } 460 | 461 | #endregion 462 | 463 | 464 | #region Full feature tests 465 | 466 | /** 467 | * Simple thread test 468 | * 469 | * @author amal 470 | * @group integrational 471 | * @group thread 472 | * 473 | * @requires extension posix 474 | * @requires extension pcntl 475 | */ 476 | public function testThread() 477 | { 478 | $testCase = $this; 479 | $this->processAsyncTest(function() use ($testCase) { 480 | $testCase->processThread(false); 481 | }, true); 482 | } 483 | 484 | /** 485 | * Additional thread test 486 | * 487 | * @author amal 488 | * @group integrational 489 | * @group thread 490 | * 491 | * @requires extension posix 492 | * @requires extension pcntl 493 | */ 494 | public function testThread2() 495 | { 496 | $testCase = $this; 497 | $this->processAsyncTest(function() use ($testCase) { 498 | $testCase->processThread2(false); 499 | }, true); 500 | } 501 | 502 | /** 503 | * Thread test with big data 504 | * 505 | * @author amal 506 | * @group integrational 507 | * @group thread 508 | * 509 | * @requires extension posix 510 | * @requires extension pcntl 511 | */ 512 | public function testThreadWithBigData() 513 | { 514 | $testCase = $this; 515 | $this->processAsyncTest(function() use ($testCase) { 516 | $testCase->processThread(false, true); 517 | }, true); 518 | } 519 | 520 | /** 521 | * Thread test with childs 522 | * 523 | * @author amal 524 | * @group integrational 525 | * @group thread 526 | * 527 | * @requires extension posix 528 | * @requires extension pcntl 529 | */ 530 | public function testThreadWithChilds() 531 | { 532 | $testCase = $this; 533 | $this->processAsyncTest(function() use ($testCase) { 534 | $testCase->processThread(false, false, true); 535 | }, true); 536 | } 537 | 538 | /** 539 | * Thread test with childs and big data 540 | * 541 | * @author amal 542 | * @group integrational 543 | * @group thread 544 | * 545 | * @requires extension posix 546 | * @requires extension pcntl 547 | */ 548 | public function testThreadWithBigDataAndChilds() 549 | { 550 | $testCase = $this; 551 | $this->processAsyncTest(function() use ($testCase) { 552 | $testCase->processThread(false, true, true); 553 | }); 554 | } 555 | 556 | /** 557 | * Thread events test 558 | * 559 | * @author amal 560 | * @group integrational 561 | * @group thread 562 | * 563 | * @requires extension posix 564 | * @requires extension pcntl 565 | */ 566 | public function testThreadWithEvents() 567 | { 568 | $testCase = $this; 569 | $this->processAsyncTest(function() use ($testCase) { 570 | $testCase->processThreadEvent(false); 571 | }, true); 572 | } 573 | 574 | /** 575 | * Thread events test with big data 576 | * 577 | * @author amal 578 | * @group integrational 579 | * @group thread 580 | * 581 | * @requires extension posix 582 | * @requires extension pcntl 583 | */ 584 | public function testThreadWithEventsBigData() 585 | { 586 | $testCase = $this; 587 | $this->processAsyncTest(function() use ($testCase) { 588 | $testCase->processThreadEvent(false, true); 589 | }, true); 590 | } 591 | 592 | /** 593 | * Errorable thread test 594 | * 595 | * @author amal 596 | * @group integrational 597 | * @group thread 598 | * 599 | * @requires extension posix 600 | * @requires extension pcntl 601 | */ 602 | public function testThreadErrorable() 603 | { 604 | $testCase = $this; 605 | $this->processAsyncTest(function() use ($testCase) { 606 | $testCase->processThreadErrorable(false); 607 | }, true); 608 | } 609 | 610 | /** 611 | * Thread arguments mapping test (sync mode) 612 | * 613 | * @author amal 614 | * @group integrational 615 | * @group thread 616 | * 617 | * @requires extension posix 618 | * @requires extension pcntl 619 | */ 620 | public function testThreadArgumentsMapping() 621 | { 622 | $testCase = $this; 623 | $this->processAsyncTest(function() use ($testCase) { 624 | $testCase->processThreadArgumentsMapping(false); 625 | }, true); 626 | } 627 | 628 | /** 629 | * Multitask disabled thread test 630 | * 631 | * @author amal 632 | * @group integrational 633 | * @group thread 634 | * @ticket GitHub Anizoptera/AzaThread/#6 635 | * 636 | * @requires extension posix 637 | * @requires extension pcntl 638 | */ 639 | public function testThreadOnetask() 640 | { 641 | $debug = false; 642 | $testCase = $this; 643 | $this->processAsyncTest(function() use ($testCase, $debug) { 644 | $testCase->processThread($debug, true, false, true); 645 | $testCase->processThread($debug, false, false, true); 646 | }, true); 647 | } 648 | 649 | /** 650 | * Closure thread test 651 | * 652 | * @author amal 653 | * @group integrational 654 | * @group thread 655 | * 656 | * @requires extension posix 657 | * @requires extension pcntl 658 | */ 659 | public function testThreadClosure() 660 | { 661 | $testCase = $this; 662 | $this->processAsyncTest(function() use ($testCase) { 663 | $testCase->processThreadClosure(false); 664 | }, true); 665 | } 666 | 667 | 668 | /** 669 | * Simple thread pool test 670 | * 671 | * @author amal 672 | * @group integrational 673 | * @group pool 674 | * 675 | * @requires extension posix 676 | * @requires extension pcntl 677 | */ 678 | public function testThreadPool() 679 | { 680 | $testCase = $this; 681 | $this->processAsyncTest(function() use ($testCase) { 682 | $testCase->processPool(false); 683 | }, true); 684 | } 685 | 686 | /** 687 | * Additional thread pool test 688 | * 689 | * @author amal 690 | * @group integrational 691 | * @group pool 692 | * 693 | * @requires extension posix 694 | * @requires extension pcntl 695 | */ 696 | public function testThreadPool2() 697 | { 698 | $testCase = $this; 699 | $this->processAsyncTest(function() use ($testCase) { 700 | $testCase->processPool2(false); 701 | }, true); 702 | } 703 | 704 | /** 705 | * Thread pool test with big data 706 | * 707 | * @author amal 708 | * @group integrational 709 | * @group pool 710 | * 711 | * @requires extension posix 712 | * @requires extension pcntl 713 | */ 714 | public function testThreadPoolWithBigData() 715 | { 716 | $testCase = $this; 717 | $this->processAsyncTest(function() use ($testCase) { 718 | $testCase->processPool(false, true); 719 | }, true); 720 | } 721 | 722 | /** 723 | * Thread pool test with childs 724 | * 725 | * @author amal 726 | * @group integrational 727 | * @group pool 728 | * 729 | * @requires extension posix 730 | * @requires extension pcntl 731 | */ 732 | public function testThreadPoolWithChilds() 733 | { 734 | $testCase = $this; 735 | $this->processAsyncTest(function() use ($testCase) { 736 | $testCase->processPool(false, false, true); 737 | }, true); 738 | } 739 | 740 | /** 741 | * Thread pool test with childs and big data 742 | * 743 | * @author amal 744 | * @group integrational 745 | * @group pool 746 | * 747 | * @requires extension posix 748 | * @requires extension pcntl 749 | */ 750 | public function testThreadPoolWithBigDataAndChilds() 751 | { 752 | $testCase = $this; 753 | $this->processAsyncTest(function() use ($testCase) { 754 | $testCase->processPool(false, true, true); 755 | }); 756 | } 757 | 758 | /** 759 | * Thread pool events test 760 | * 761 | * @author amal 762 | * @group integrational 763 | * @group pool 764 | * 765 | * @requires extension posix 766 | * @requires extension pcntl 767 | */ 768 | public function testThreadPoolWithEvents() 769 | { 770 | $testCase = $this; 771 | $this->processAsyncTest(function() use ($testCase) { 772 | $testCase->processPoolEvent(false); 773 | }); 774 | } 775 | 776 | /** 777 | * Errorable thread pool test 778 | * 779 | * @author amal 780 | * @group integrational 781 | * @group pool 782 | * 783 | * @requires extension posix 784 | * @requires extension pcntl 785 | */ 786 | public function testThreadPoolErrorable() 787 | { 788 | $testCase = $this; 789 | $this->processAsyncTest(function() use ($testCase) { 790 | $testCase->processPoolErrorable(false); 791 | }); 792 | } 793 | 794 | /** 795 | * Thread pool test with external stop (for issue) 796 | * 797 | * @author amal 798 | * @group integrational 799 | * @group pool 800 | * @ticket GitHub Anizoptera/AzaThread/#2 801 | * 802 | * @requires extension posix 803 | * @requires extension pcntl 804 | */ 805 | public function testThreadPoolErrorableExternal() 806 | { 807 | $testCase = $this; 808 | $this->processAsyncTest(function() use ($testCase) { 809 | $testCase->processPoolErrorableExternal(false); 810 | }); 811 | } 812 | 813 | /** 814 | * Thread pool test with multitask disabled 815 | * 816 | * @author amal 817 | * @group integrational 818 | * @group pool 819 | * @ticket GitHub Anizoptera/AzaThread/#6 820 | * 821 | * @requires extension posix 822 | * @requires extension pcntl 823 | */ 824 | public function testPoolOnetask() 825 | { 826 | $testCase = $this; 827 | $this->processAsyncTest(function() use ($testCase) { 828 | $testCase->processPool(false, false, true, true); 829 | }, true); 830 | } 831 | 832 | 833 | /** 834 | * Test for exception in events triggering 835 | * 836 | * @author amal 837 | * @group integrational 838 | * @group thread 839 | * 840 | * @requires extension posix 841 | * @requires extension pcntl 842 | */ 843 | public function testExceptionInEventListener() 844 | { 845 | $testCase = $this; 846 | $this->processAsyncTest(function() use ($testCase) { 847 | $testCase->processExceptionInEventListener(false); 848 | }, true); 849 | } 850 | 851 | /** 852 | * Two different threads test 853 | * 854 | * @author amal 855 | * @group integrational 856 | * @group thread 857 | * 858 | * @requires extension posix 859 | * @requires extension pcntl 860 | */ 861 | public function testTwoDifferentThreads() 862 | { 863 | $testCase = $this; 864 | $this->processAsyncTest(function() use ($testCase) { 865 | $testCase->processTwoDifferentThreads(false); 866 | }); 867 | } 868 | 869 | /** 870 | * POSIX signals handling test 871 | * 872 | * @author amal 873 | * @group integrational 874 | * @group thread 875 | * 876 | * @requires extension posix 877 | * @requires extension pcntl 878 | */ 879 | public function testSignalsHandling() 880 | { 881 | $debug = false; 882 | $testCase = $this; 883 | $this->processAsyncTest(function() use ($testCase, $debug) { 884 | if ($debug) { 885 | echo '----------------------------', PHP_EOL, 886 | "POSIX signals handling test ", PHP_EOL, 887 | '----------------------------', PHP_EOL; 888 | } 889 | 890 | 891 | // Random thread init, to test handling of signals for different threads 892 | $t = new TestThreadReturnFirstArgument( 893 | null, null, $debug 894 | ); 895 | 896 | 897 | TestSignalsHandling::$catchedSignalsInParent = $counter = 0; 898 | $testCase->assertSame($counter, TestSignalsHandling::$catchedSignalsInParent); 899 | 900 | 901 | $thread = new TestSignalsHandling('SignalsHandling', null, $debug); 902 | $thread->wait(); 903 | 904 | $testCase->assertSame($counter++, TestSignalsHandling::$catchedSignalsInParent); 905 | 906 | $catchedSignals = (int)$thread->run()->wait()->getResult(); 907 | $testCase->assertTrue(0 === $catchedSignals); 908 | $thread->getEventLoop()->loop(EVLOOP_NONBLOCK); // Needed sometimes if parent isn't catched signal yet 909 | $testCase->assertSame($counter++, TestSignalsHandling::$catchedSignalsInParent); 910 | 911 | $thread->sendSignalToChild(SIGUSR1); 912 | usleep(1000); 913 | $catchedSignals = (int)$thread->run()->wait()->getResult(); 914 | $testCase->assertTrue(1 === $catchedSignals || 0 === $catchedSignals); 915 | $thread->getEventLoop()->loop(EVLOOP_NONBLOCK); // Needed sometimes if parent isn't catched signal yet 916 | $testCase->assertSame($counter++, TestSignalsHandling::$catchedSignalsInParent); 917 | 918 | $thread->sendSignalToChild(SIGUSR1); 919 | usleep(1000); 920 | $catchedSignals = (int)$thread->run()->wait()->getResult(); 921 | $testCase->assertTrue(2 === $catchedSignals || 1 === $catchedSignals); 922 | $thread->getEventLoop()->loop(EVLOOP_NONBLOCK); // Needed sometimes if parent isn't catched signal yet 923 | $testCase->assertSame($counter++, TestSignalsHandling::$catchedSignalsInParent); 924 | 925 | $thread->sendSignalToChild(SIGUSR1); 926 | usleep(1000); 927 | $catchedSignals = (int)$thread->run()->wait()->getResult(); 928 | $testCase->assertTrue(3 === $catchedSignals || 2 === $catchedSignals); 929 | $thread->getEventLoop()->loop(EVLOOP_NONBLOCK); // Needed sometimes if parent isn't catched signal yet 930 | $testCase->assertSame($counter++, TestSignalsHandling::$catchedSignalsInParent); 931 | 932 | $thread->sendSignalToParent(SIGWINCH); 933 | $thread->sendSignalToParent(SIGCHLD); 934 | 935 | $thread->getEventLoop()->loop(EVLOOP_NONBLOCK); 936 | $thread->cleanup(); 937 | 938 | 939 | $thread = new TestSignalsHandling(null, null, $debug); 940 | $thread->wait()->sendSignalToChild(SIGUSR1); 941 | usleep(1000); 942 | $catchedSignals = (int)$thread->run()->wait()->getResult(); 943 | $testCase->assertTrue(1 === $catchedSignals || 0 === $catchedSignals); 944 | $thread->getEventLoop()->loop(EVLOOP_NONBLOCK); // Needed sometimes if parent isn't catched signal yet 945 | $testCase->assertSame($counter++, TestSignalsHandling::$catchedSignalsInParent); 946 | $thread->getEventLoop()->loop(EVLOOP_NONBLOCK); 947 | $thread->cleanup(); 948 | 949 | 950 | $thread = new TestSignalsHandling(null, null, $debug); 951 | $thread->sendSignalToChild(SIGUSR1)->wait(); 952 | usleep(1000); 953 | $catchedSignals = (int)$thread->run()->wait()->getResult(); 954 | $testCase->assertTrue(0 === $catchedSignals || 1 === $catchedSignals); 955 | $thread->getEventLoop()->loop(EVLOOP_NONBLOCK); // Needed sometimes if parent isn't catched signal yet 956 | $testCase->assertSame($counter++, TestSignalsHandling::$catchedSignalsInParent); 957 | $thread->getEventLoop()->loop(EVLOOP_NONBLOCK); 958 | $thread->cleanup(); 959 | 960 | 961 | $t->cleanup(); 962 | unset($counter); 963 | }); 964 | } 965 | 966 | 967 | /** 968 | * Prefork wait timeout test 969 | * 970 | * @author amal 971 | * @group integrational 972 | * @group timeout 973 | * 974 | * @requires extension posix 975 | * @requires extension pcntl 976 | */ 977 | public function testPreforkWaitTimeout() 978 | { 979 | $debug = false; 980 | $testCase = $this; 981 | $this->processAsyncTest(function() use ($testCase, $debug) { 982 | if ($debug) { 983 | echo '-------------------------', PHP_EOL, 984 | "Prefork wait timeout test ", PHP_EOL, 985 | '-------------------------', PHP_EOL; 986 | } 987 | 988 | $thread = new TestPreforkWaitTimeout( 989 | null, null, $debug 990 | ); 991 | 992 | $testCase->assertFalse($thread->getSuccess()); 993 | $testCase->assertSame( 994 | Thread::ERR_TIMEOUT_INIT, 995 | $thread->getLastErrorCode() 996 | ); 997 | $testCase->assertContains( 998 | 'Exceeded timeout: thread initialization', 999 | $thread->getLastErrorMsg() 1000 | ); 1001 | $testCase->assertSame( 1002 | Thread::STATE_WAIT, 1003 | $thread->getState() 1004 | ); 1005 | 1006 | $thread->cleanup(); 1007 | }, true); 1008 | } 1009 | 1010 | /** 1011 | * Result wait timeout test 1012 | * 1013 | * @author amal 1014 | * @group integrational 1015 | * @group timeout 1016 | * 1017 | * @requires extension posix 1018 | * @requires extension pcntl 1019 | */ 1020 | public function testResultWaitTimeout() 1021 | { 1022 | $debug = false; 1023 | $testCase = $this; 1024 | $this->processAsyncTest(function() use ($testCase, $debug) { 1025 | if ($debug) { 1026 | echo '-------------------------', PHP_EOL, 1027 | "Result wait timeout test ", PHP_EOL, 1028 | '-------------------------', PHP_EOL; 1029 | } 1030 | 1031 | $thread = new TestResultWaitTimeout( 1032 | null, null, $debug 1033 | ); 1034 | $thread->wait()->run()->wait(); 1035 | 1036 | $testCase->assertFalse($thread->getSuccess()); 1037 | $testCase->assertSame( 1038 | Thread::ERR_TIMEOUT_RESULT, 1039 | $thread->getLastErrorCode() 1040 | ); 1041 | $testCase->assertContains( 1042 | 'Exceeded timeout: thread work', 1043 | $thread->getLastErrorMsg() 1044 | ); 1045 | $testCase->assertSame( 1046 | Thread::STATE_WAIT, 1047 | $thread->getState() 1048 | ); 1049 | 1050 | $thread->cleanup(); 1051 | }, true); 1052 | } 1053 | 1054 | /** 1055 | * Job wait timeout test 1056 | * 1057 | * @author amal 1058 | * @group integrational 1059 | * @group timeout 1060 | * 1061 | * @requires extension posix 1062 | * @requires extension pcntl 1063 | */ 1064 | public function testJobWaitTimeout() 1065 | { 1066 | $debug = false; 1067 | $testCase = $this; 1068 | $this->processAsyncTest(function() use ($testCase, $debug) { 1069 | if ($debug) { 1070 | echo '-------------------------', PHP_EOL, 1071 | "Job wait (by worker) timeout test ", PHP_EOL, 1072 | '-------------------------', PHP_EOL; 1073 | } 1074 | 1075 | $thread = new TestJobWaitTimeout( 1076 | null, null, $debug 1077 | ); 1078 | $pid = $thread->getChildPid(); 1079 | $thread->wait(); 1080 | 1081 | // To certainly meet the timeout 1082 | usleep(20000); 1083 | $thread->getEventLoop()->loop(EVLOOP_NONBLOCK); 1084 | 1085 | if ($thread->getIsForked()) { 1086 | $thread->run()->wait(); 1087 | } 1088 | 1089 | $testCase->assertSame( 1090 | Thread::ERR_DEATH, 1091 | $thread->getLastErrorCode() 1092 | ); 1093 | $testCase->assertContains( 1094 | 'Worker is dead', 1095 | $thread->getLastErrorMsg() 1096 | ); 1097 | $testCase->assertSame( 1098 | Thread::STATE_WAIT, 1099 | $thread->getState() 1100 | ); 1101 | 1102 | $testCase->assertFalse($thread->getSuccess()); 1103 | $testCase->assertFalse($thread->getIsForked()); 1104 | 1105 | $testCase->assertNotSame($pid, $thread->getChildPid()); 1106 | $testCase->assertSame( 1107 | $thread->getParentPid(), 1108 | $thread->getChildPid() 1109 | ); 1110 | 1111 | $thread->cleanup(); 1112 | }, true); 1113 | } 1114 | 1115 | /** 1116 | * Parent check timeout test 1117 | * 1118 | * @author amal 1119 | * @group integrational 1120 | * @group timeout 1121 | * 1122 | * @requires extension posix 1123 | * @requires extension pcntl 1124 | */ 1125 | public function testParentCheckTimeout() 1126 | { 1127 | $debug = false; 1128 | $testCase = $this; 1129 | $this->processAsyncTest(function() use ($testCase, $debug) { 1130 | if ($debug) { 1131 | echo '-------------------------', PHP_EOL, 1132 | "Parent check timeout test ", PHP_EOL, 1133 | '-------------------------', PHP_EOL; 1134 | } 1135 | 1136 | $thread = new TestParentCheckTimeout( 1137 | null, null, $debug 1138 | ); 1139 | $childPid = $thread->wait()->getChildPid(); 1140 | $testCase->assertNotEmpty($childPid); 1141 | 1142 | $childChildPid = null; 1143 | $triggered = false; 1144 | $thread->bind( 1145 | TestParentCheckTimeout::EV_PID, 1146 | function($event_name, $e_data) 1147 | use (&$childChildPid, &$triggered, $testCase) 1148 | { 1149 | $triggered = true; 1150 | $testCase->assertSame( 1151 | TestParentCheckTimeout::EV_PID, 1152 | $event_name 1153 | ); 1154 | $childChildPid = $e_data; 1155 | } 1156 | )->run()->wait(); 1157 | 1158 | $testCase->assertTrue($triggered); 1159 | $testCase->assertNotEmpty($childChildPid); 1160 | 1161 | // Child is dead 1162 | $isAliveChild = Base::getProcessIsAlive($childPid); 1163 | $testCase->assertFalse($isAliveChild); 1164 | 1165 | // Child's child can be alive in that moment, so wait a litle 1166 | usleep(20000); 1167 | $i = 50; 1168 | do { 1169 | if (!$isAliveChildChild = Base::getProcessIsAlive($childChildPid)) { 1170 | break; 1171 | } 1172 | usleep(20000); 1173 | $i--; 1174 | } while ($i > 0); 1175 | $testCase->assertFalse($isAliveChildChild); 1176 | 1177 | $thread->cleanup(); 1178 | }, true); 1179 | } 1180 | 1181 | #endregion 1182 | 1183 | 1184 | 1185 | #region Auxiliary (helper) test methods 1186 | 1187 | /** 1188 | * Helper method for testing threads in asynchronous mode 1189 | * 1190 | * @param callable $callback 1191 | * @param bool $simple More simple test 1192 | * 1193 | * @throws \Exception 1194 | */ 1195 | function processAsyncTest($callback, $simple = false) 1196 | { 1197 | if (!Thread::$useForks) { 1198 | $this->markTestSkipped( 1199 | 'You need LibEvent, PCNTL and POSIX support' 1200 | .' with CLI sapi to fully test Threads' 1201 | ); 1202 | return; 1203 | } 1204 | 1205 | if ($simple) { 1206 | $ipcModes = array(Thread::$ipcDataMode => false); 1207 | $socketModes = array(Socket::$useSockets); 1208 | } else { 1209 | $ipcModes = array( 1210 | Thread::IPC_IGBINARY => 'igbinary_serialize', 1211 | Thread::IPC_SERIALIZE => false, 1212 | ); 1213 | $socketModes = array(true, false); 1214 | } 1215 | 1216 | $defSocketMode = Socket::$useSockets; 1217 | 1218 | foreach ($socketModes as $socketMode) { 1219 | Socket::$useSockets = $socketMode; 1220 | 1221 | foreach ($ipcModes as $mode => $check) { 1222 | if ($check && !function_exists($check)) { 1223 | continue; 1224 | } 1225 | 1226 | Thread::$ipcDataMode = $mode; 1227 | 1228 | try { 1229 | if (false === $callback()) { 1230 | break 2; 1231 | } 1232 | } catch (\Exception $e) { 1233 | if ($base = EventBase::getMainLoop(false)) { 1234 | $base->resource && $base->loopBreak(); 1235 | } 1236 | throw $e; 1237 | } 1238 | } 1239 | } 1240 | 1241 | Socket::$useSockets = $defSocketMode; 1242 | } 1243 | 1244 | 1245 | /** 1246 | * Thread 1247 | * 1248 | * @param bool $debug 1249 | * @param bool $bigResult 1250 | * @param bool $withChild 1251 | * @param bool $oneTask 1252 | */ 1253 | function processThread($debug = false, $bigResult = false, $withChild = false, $oneTask = false) 1254 | { 1255 | $num = 10; 1256 | 1257 | $async = Thread::$useForks; 1258 | if ($debug) { 1259 | echo '-----------------------', PHP_EOL, 1260 | "Thread test: ", ($async ? 'Async' : 'Sync'), PHP_EOL, 1261 | '-----------------------', PHP_EOL; 1262 | } 1263 | 1264 | if ($oneTask) { 1265 | $thread = new TestThreadOneTask(null, null, $debug); 1266 | } else { 1267 | $thread = $withChild 1268 | ? new TestThreadWithChilds(null, null, $debug) 1269 | : new TestThreadReturnFirstArgument(null, null, $debug); 1270 | } 1271 | 1272 | // You can override preforkWait property 1273 | // to TRUE to not wait thread at first time manually 1274 | $thread->wait(); 1275 | 1276 | if ($async) { 1277 | if ($oneTask) { 1278 | $this->assertFalse($thread->getIsForked()); 1279 | } else { 1280 | $this->assertTrue($thread->getIsForked()); 1281 | } 1282 | } 1283 | $this->assertFalse($thread->getIsChild()); 1284 | $this->assertSame(Thread::STATE_WAIT, $thread->getState()); 1285 | $this->assertEmpty( 1286 | $thread->getLastErrorCode(), 1287 | $thread->getLastErrorMsg() 1288 | ); 1289 | $this->assertEmpty($thread->getLastErrorMsg()); 1290 | $this->assertFalse($thread->getIsCleaning()); 1291 | 1292 | for ($i = 0; $i < $num; $i++) { 1293 | $value = $bigResult ? str_repeat($i, 100000) : $i; 1294 | $thread->run($value)->wait(); 1295 | $this->assertSame(Thread::STATE_WAIT, $thread->getState()); 1296 | $this->assertTrue( 1297 | $thread->getSuccess(), 1298 | 'Job failure - #' . $thread->getLastErrorCode() 1299 | . ' ' . $thread->getLastErrorMsg() 1300 | ); 1301 | $this->assertEquals($value, $thread->getResult()); 1302 | $this->assertEmpty( 1303 | $thread->getLastErrorCode(), 1304 | $thread->getLastErrorMsg() 1305 | ); 1306 | $this->assertEmpty($thread->getLastErrorMsg()); 1307 | } 1308 | 1309 | $this->assertSame($num, $thread->getStartedJobs()); 1310 | $this->assertSame($num, $thread->getSuccessfulJobs()); 1311 | $this->assertSame(0, $thread->getFailedJobs()); 1312 | 1313 | $thread->cleanup(); 1314 | $this->assertTrue($thread->getIsCleaning()); 1315 | $this->assertSame('TERM', $thread->getStateName()); 1316 | $this->assertSame('INIT', $thread->getStateName(Thread::STATE_INIT)); 1317 | } 1318 | 1319 | /** 1320 | * Thread 2 1321 | * 1322 | * @param bool $debug 1323 | */ 1324 | function processThread2($debug = false) 1325 | { 1326 | $async = Thread::$useForks; 1327 | if ($debug) { 1328 | echo '-------------------------', PHP_EOL, 1329 | "Additional thread test: ", ($async ? 'Async' : 'Sync'), PHP_EOL, 1330 | '-------------------------', PHP_EOL; 1331 | } 1332 | 1333 | 1334 | // Empty argument 1335 | $thread = new TestThreadReturnAllArguments( 1336 | null, null, $debug 1337 | ); 1338 | $thread->wait()->run(null)->wait(); 1339 | $this->assertTrue( 1340 | $thread->getSuccess(), 1341 | 'Job failure - #' . $thread->getLastErrorCode() 1342 | . ' ' . $thread->getLastErrorMsg() 1343 | ); 1344 | $this->assertSame(array(null), $thread->getResult()); 1345 | $this->assertEmpty( 1346 | $thread->getLastErrorCode(), 1347 | $thread->getLastErrorMsg() 1348 | ); 1349 | $this->assertEmpty($thread->getLastErrorMsg()); 1350 | $thread->cleanup(); 1351 | 1352 | 1353 | // Empty result 1354 | $thread = new TestThreadDoNothing( 1355 | null, null, $debug 1356 | ); 1357 | $thread->wait()->run()->wait(); 1358 | $this->assertTrue( 1359 | $thread->getSuccess(), 1360 | 'Job failure - #' . $thread->getLastErrorCode() 1361 | . ' ' . $thread->getLastErrorMsg() 1362 | ); 1363 | $this->assertSame(null, $thread->getResult()); 1364 | $this->assertEmpty( 1365 | $thread->getLastErrorCode(), 1366 | $thread->getLastErrorMsg() 1367 | ); 1368 | $this->assertEmpty($thread->getLastErrorMsg()); 1369 | $thread->cleanup(); 1370 | 1371 | 1372 | // Hooks, etc. 1373 | $thread = new TestThreadHooks( 1374 | null, null, $debug 1375 | ); 1376 | 1377 | $isAlive = new ReflectionMethod($thread, 'isAlive'); 1378 | $isAlive->setAccessible(true); 1379 | if ($async) { 1380 | $this->assertNotEmpty($thread->getEventLoop()); 1381 | $this->assertNotEmpty($thread->getPid()); 1382 | $this->assertNotEmpty($thread->getParentPid()); 1383 | $this->assertNotEmpty($thread->getChildPid()); 1384 | $this->assertTrue($thread->getIsForked()); 1385 | $this->assertTrue($isAlive->invoke($thread)); 1386 | } else { 1387 | $this->assertEmpty($thread->getEventLoop()); 1388 | $this->assertEmpty($thread->getPid()); 1389 | $this->assertEmpty($thread->getParentPid()); 1390 | $this->assertEmpty($thread->getChildPid()); 1391 | $this->assertFalse($thread->getIsForked()); 1392 | $this->assertFalse($isAlive->invoke($thread)); 1393 | } 1394 | 1395 | $this->assertSame(1, $thread->onLoadHookCalls); 1396 | $this->assertSame( 1397 | $async ? 0 : 1, 1398 | $thread->onForkHookCalls 1399 | ); 1400 | $this->assertSame(0, $thread->onShutdownHookCalls); 1401 | $this->assertSame(0, $thread->onCleanupHookCalls); 1402 | $thread->wait()->run()->wait(); 1403 | $this->assertTrue( 1404 | $thread->getSuccess(), 1405 | 'Job failure - #' . $thread->getLastErrorCode() 1406 | . ' ' . $thread->getLastErrorMsg() 1407 | ); 1408 | $this->assertSame(array(1, 1, 0, 0), $thread->getResult()); 1409 | $thread->cleanup(); 1410 | $this->assertSame(1, $thread->onLoadHookCalls); 1411 | $this->assertSame( 1412 | $async ? 0 : 1, 1413 | $thread->onForkHookCalls 1414 | ); 1415 | $this->assertSame(0, $thread->onShutdownHookCalls); 1416 | $this->assertSame(1, $thread->onCleanupHookCalls); 1417 | } 1418 | 1419 | /** 1420 | * Thread, random errors 1421 | * 1422 | * @param bool $debug 1423 | */ 1424 | function processThreadErrorable($debug = false) 1425 | { 1426 | $num = 10; 1427 | 1428 | $async = Thread::$useForks; 1429 | 1430 | if ($debug) { 1431 | echo '-----------------------', PHP_EOL, 1432 | "Thread errorable test: ", ($async ? 'Async' : 'Sync'), PHP_EOL, 1433 | '-----------------------', PHP_EOL; 1434 | } 1435 | 1436 | $thread = new TestThreadErrorable( 1437 | null, null, $debug 1438 | ); 1439 | 1440 | $value = $i = $j = 0; 1441 | $max = (int)($num*2); 1442 | 1443 | // You can override preforkWait property 1444 | // to TRUE to not wait thread at first time manually 1445 | $thread->wait(); 1446 | 1447 | while ($num > $i && $j <= $max) { 1448 | $j++; 1449 | $thread->run($value, $j)->wait(); 1450 | $state = $thread->getState(); 1451 | $this->assertSame(Thread::STATE_WAIT, $state); 1452 | if ($thread->getSuccess()) { 1453 | $result = $thread->getResult(); 1454 | $this->assertEquals($value, $result); 1455 | $value = ++$i; 1456 | } 1457 | } 1458 | 1459 | $this->assertSame($num, $i, 'All jobs must be done'); 1460 | if ($async) { 1461 | $this->assertTrue($j > $num); 1462 | } else { 1463 | $this->assertSame($num, $j); 1464 | } 1465 | 1466 | if ($async) { 1467 | $this->assertTrue($thread->getFailedJobs() > 0); 1468 | } 1469 | $this->assertSame( 1470 | $thread->getStartedJobs(), 1471 | $thread->getSuccessfulJobs() + $thread->getFailedJobs() 1472 | ); 1473 | 1474 | $thread->cleanup(); 1475 | } 1476 | 1477 | /** 1478 | * Thread, events 1479 | * 1480 | * @param bool $debug 1481 | * @param bool $bigData 1482 | * 1483 | * @throws \Exception 1484 | */ 1485 | function processThreadEvent($debug = false, $bigData = false) 1486 | { 1487 | $events = 11; 1488 | $num = 3; 1489 | 1490 | if ($debug) { 1491 | echo '-----------------------', PHP_EOL, 1492 | "Thread events test: ", (Thread::$useForks ? 'Async' : 'Sync'), PHP_EOL, 1493 | '-----------------------', PHP_EOL; 1494 | } 1495 | 1496 | $thread = new TestThreadEvents(null, null, $debug); 1497 | 1498 | $arg = mt_rand(19, 976); 1499 | if ($bigData) { 1500 | $arg = str_repeat($arg, 100000); 1501 | } 1502 | $last = 0; 1503 | $testCase = $this; 1504 | $cb = function($event, $e_data, $e_arg) use ($arg, $testCase, &$last) { 1505 | $testCase->assertSame($arg, $e_arg); 1506 | $testCase->assertSame(TestThreadEvents::EV_PROCESS, $event); 1507 | $testCase->assertEquals($last++, $e_data); 1508 | }; 1509 | $thread->bind(TestThreadEvents::EV_PROCESS, $cb, $arg); 1510 | 1511 | // You can override preforkWait property 1512 | // to TRUE to not wait thread at first time manually 1513 | $thread->wait(); 1514 | 1515 | $signalNotChecked = true; 1516 | for ($i = 0; $i < $num; $i++) { 1517 | $last = 0; 1518 | if ($signalNotChecked) { 1519 | $signalNotChecked = false; 1520 | $thread->run($events) 1521 | ->sendSignalToChild(SIGWINCH) 1522 | ->wait(); 1523 | } else { 1524 | $thread->run($events)->wait(); 1525 | } 1526 | $this->assertTrue( 1527 | $thread->getSuccess(), 1528 | 'Job failure - #' . $thread->getLastErrorCode() 1529 | . ' ' . $thread->getLastErrorMsg() 1530 | ); 1531 | } 1532 | 1533 | $this->assertSame($events, $last); 1534 | 1535 | $thread->cleanup(); 1536 | } 1537 | 1538 | /** 1539 | * Arguments mapping 1540 | * 1541 | * @param bool $debug 1542 | */ 1543 | function processThreadArgumentsMapping($debug = false) 1544 | { 1545 | $num = 3; 1546 | 1547 | $async = Thread::$useForks; 1548 | 1549 | if ($debug) { 1550 | echo '-----------------------', PHP_EOL, 1551 | "Thread arguments mapping test: ", ($async ? 'Async' : 'Sync'), PHP_EOL, 1552 | '-----------------------', PHP_EOL; 1553 | } 1554 | 1555 | $thread = new TestThreadArgumentsMapping( 1556 | null, null, $debug 1557 | ); 1558 | 1559 | // You can override preforkWait property 1560 | // to TRUE to not wait thread at first time manually 1561 | $thread->wait(); 1562 | 1563 | for ($i = 0; $i < $num; $i++) { 1564 | $a = mt_rand(99, 999); 1565 | $b = mt_rand(59, 999); 1566 | $c = mt_rand(13, 999); 1567 | $thread->run($a, $b, $c)->wait(); 1568 | $this->assertSame( 1569 | array($a, $b, $c), 1570 | $thread->getResult() 1571 | ); 1572 | } 1573 | 1574 | $thread->cleanup(); 1575 | } 1576 | 1577 | /** 1578 | * Closure thread 1579 | * 1580 | * @param bool $debug 1581 | */ 1582 | function processThreadClosure($debug = false) 1583 | { 1584 | $async = Thread::$useForks; 1585 | if ($debug) { 1586 | echo '-----------------------', PHP_EOL, 1587 | "Closure thread test: ", ($async ? 'Async' : 'Sync'), PHP_EOL, 1588 | '-----------------------', PHP_EOL; 1589 | } 1590 | 1591 | 1592 | // ---- 1593 | $expected = mt_rand(999, 99999); 1594 | $thread = SimpleThread::create(function() use ($expected) { 1595 | return $expected; 1596 | }, null, $debug); 1597 | $result = $thread->run()->wait()->getResult(); 1598 | $this->assertSame($expected, $result); 1599 | 1600 | 1601 | // ---- 1602 | $result = $thread->run()->wait()->getResult(); 1603 | $this->assertSame($expected, $result); 1604 | $thread->cleanup(); 1605 | 1606 | 1607 | // ---- 1608 | $expected = mt_rand(999, 99999); 1609 | $thread = SimpleThread::create(function($arg) { 1610 | return $arg; 1611 | }, null, $debug); 1612 | $result = $thread->run($expected)->wait()->getResult(); 1613 | $this->assertSame($expected, $result); 1614 | 1615 | 1616 | // ---- 1617 | $expected = mt_rand(999, 99999); 1618 | $result = $thread->run($expected)->wait()->getResult(); 1619 | $this->assertSame($expected, $result); 1620 | $thread->cleanup(); 1621 | } 1622 | 1623 | 1624 | /** 1625 | * Pool 1626 | * 1627 | * @param bool $debug 1628 | * @param bool $bigResult 1629 | * @param bool $withChild 1630 | * @param bool $oneTask 1631 | * 1632 | * @throws Exception 1633 | */ 1634 | function processPool($debug = false, $bigResult = false, $withChild = false, $oneTask = false) 1635 | { 1636 | $threads = 2; 1637 | $targetThreads = $threads+2; 1638 | $num = $targetThreads*5; 1639 | 1640 | if ($debug) { 1641 | echo '-----------------------', PHP_EOL, 1642 | "Thread pool test: ", (Thread::$useForks ? 'Async' : 'Sync'), PHP_EOL, 1643 | '-----------------------', PHP_EOL; 1644 | } 1645 | 1646 | 1647 | if ($oneTask) { 1648 | $thread = 'TestThreadOneTask'; 1649 | } else { 1650 | $thread = $withChild 1651 | ? 'TestThreadWithChilds' 1652 | : 'TestThreadReturnFirstArgument'; 1653 | } 1654 | $thread = __NAMESPACE__ . '\\' . $thread; 1655 | 1656 | $name = 'example'; 1657 | $pool = new ThreadPool($thread, $threads, null, $name, $debug); 1658 | 1659 | $this->assertNotEmpty($pool->getId()); 1660 | $this->assertSame($name, $pool->getPoolName()); 1661 | $this->assertEmpty($pool->getThreadProcessName()); 1662 | $this->assertSame($thread, $pool->getThreadClassName()); 1663 | 1664 | if (Thread::$useForks) { 1665 | $this->assertSame($threads, $pool->getThreadsCount()); 1666 | $this->assertSame($threads, $pool->getMaxThreads()); 1667 | 1668 | // We can not set number of threads lower than 1669 | // number of already created threads 1670 | $pool->setMaxThreads($threads-1); 1671 | $this->assertSame($threads, $pool->getThreadsCount()); 1672 | $this->assertSame($threads, $pool->getMaxThreads()); 1673 | 1674 | // But we can set more 1675 | $pool->setMaxThreads($targetThreads); 1676 | $this->assertSame($targetThreads, $pool->getThreadsCount()); 1677 | $this->assertSame($targetThreads, $pool->getMaxThreads()); 1678 | } else { 1679 | $this->assertSame(1, $pool->getThreadsCount()); 1680 | $this->assertSame(1, $pool->getMaxThreads()); 1681 | 1682 | $pool->setMaxThreads(8); 1683 | $this->assertSame(1, $pool->getThreadsCount()); 1684 | $this->assertSame(1, $pool->getMaxThreads()); 1685 | 1686 | $pool->setMaxThreads(0); 1687 | $this->assertSame(1, $pool->getThreadsCount()); 1688 | $this->assertSame(1, $pool->getMaxThreads()); 1689 | } 1690 | 1691 | $jobs = array(); 1692 | 1693 | $i = 0; 1694 | $left = $num; 1695 | $maxI = (int)ceil($num * 1.5); 1696 | $worked = array(); 1697 | do { 1698 | while ($left > 0 && $pool->hasWaiting()) { 1699 | $arg = mt_rand(1, 999); 1700 | if ($bigResult) { 1701 | $arg = str_repeat($arg, 100000); 1702 | } 1703 | $threadId = $pool->run($arg); 1704 | $this->assertNotEmpty($threadId); 1705 | $this->assertTrue( 1706 | !isset($jobs[$threadId]), 1707 | "Thread #$threadId is not failed correctly" 1708 | ); 1709 | $jobs[$threadId] = $arg; 1710 | $worked[$threadId] = true; 1711 | $left--; 1712 | } 1713 | $results = $pool->wait($failed); 1714 | $this->assertEmpty($failed, 'Failed results: ' . print_r($failed, true)); 1715 | if ($results) { 1716 | foreach ($results as $threadId => $res) { 1717 | $this->assertTrue(isset($jobs[$threadId]), "Thread #$threadId"); 1718 | $this->assertEquals($jobs[$threadId], $res, "Thread #$threadId"); 1719 | unset($jobs[$threadId]); 1720 | $num--; 1721 | } 1722 | } 1723 | $i++; 1724 | } while ($num > 0 && $i < $maxI); 1725 | 1726 | $this->assertSame(0, $num, 'All jobs must be done'); 1727 | 1728 | $this->assertSame( 1729 | $pool->getThreadsCount(), count($worked), 1730 | 'Worked threads count is not equals to real threads count' 1731 | ); 1732 | 1733 | $state = $pool->getThreadsState(); 1734 | $statistic = $pool->getThreadsStatistic(); 1735 | if (Thread::$useForks) { 1736 | $this->assertSame($targetThreads, $pool->getThreadsCount()); 1737 | $this->assertSame($targetThreads, count($state)); 1738 | $this->assertSame($targetThreads, count($statistic)); 1739 | } else { 1740 | $this->assertSame(1, $pool->getThreadsCount()); 1741 | $this->assertSame(1, count($state)); 1742 | $this->assertSame(1, count($statistic)); 1743 | } 1744 | foreach ($state as $s) { 1745 | $this->assertSame('WAIT', $s); 1746 | } 1747 | 1748 | $pool->cleanup(); 1749 | $this->assertSame(0, $pool->getThreadsCount()); 1750 | $this->assertEmpty($pool->getThreads()); 1751 | $this->assertFalse($pool->hasWaiting()); 1752 | $this->assertEmpty($pool->getThreadsState()); 1753 | } 1754 | 1755 | /** 1756 | * Pool 2 1757 | * 1758 | * @param bool $debug 1759 | * 1760 | * @throws Exception 1761 | */ 1762 | function processPool2($debug = false) 1763 | { 1764 | $async = Thread::$useForks; 1765 | 1766 | if ($debug) { 1767 | echo '-------------------------', PHP_EOL, 1768 | "Additional thread pool test: ", ($async ? 'Async' : 'Sync'), PHP_EOL, 1769 | '-------------------------', PHP_EOL; 1770 | } 1771 | 1772 | 1773 | // ReturnAllArguments 1774 | $threads = 2; 1775 | $thread = __NAMESPACE__ . '\TestThreadReturnAllArguments'; 1776 | $pName = 'ReturnAllArguments'; 1777 | $pool = new ThreadPool( 1778 | $thread, $threads, $pName, null, $debug 1779 | ); 1780 | 1781 | $this->assertSame($pName, $pool->getThreadProcessName()); 1782 | 1783 | $state = $pool->getThreadsState(); 1784 | $this->assertSame($async ? $threads : 1, count($state)); 1785 | foreach ($state as $s) { 1786 | $this->assertTrue('INIT' === $s || 'WAIT' === $s); 1787 | } 1788 | 1789 | if ($async) { 1790 | $catched = 0; 1791 | try { 1792 | $this->assertFalse($pool->run()); 1793 | } catch (Exception $e) { 1794 | $catched++; 1795 | } 1796 | try { 1797 | $this->assertFalse($pool->run('example')); 1798 | } catch (Exception $e) { 1799 | $catched++; 1800 | } 1801 | $this->assertSame(2, $catched); 1802 | } 1803 | 1804 | $data = array( 1805 | array(), 1806 | array(1), 1807 | array(1, 2), 1808 | array(1, 2, 3), 1809 | array(1, 2, 3, 4), 1810 | array(1, 2, 3, 4, 5), 1811 | array(null), 1812 | array(null, null), 1813 | ); 1814 | 1815 | $worked = $jobs = array(); 1816 | $i = 0; 1817 | $left = $num = count($data); 1818 | $maxI = ceil($num * 1.5); 1819 | do { 1820 | while ($left > 0 && $pool->hasWaiting()) { 1821 | $args = array_shift($data); 1822 | $threadId = call_user_func_array(array($pool, 'run'), $args); 1823 | 1824 | $this->assertNotEmpty($threadId); 1825 | $this->assertTrue( 1826 | !isset($jobs[$threadId]), 1827 | "Thread #$threadId is not failed correctly" 1828 | ); 1829 | 1830 | $jobs[$threadId] = $args; 1831 | $worked[$threadId] = true; 1832 | $left--; 1833 | } 1834 | $results = $pool->wait($failed); 1835 | $this->assertEmpty($failed, 'Failed results: ' . print_r($failed, true)); 1836 | if ($results) { 1837 | foreach ($results as $threadId => $res) { 1838 | $this->assertTrue(isset($jobs[$threadId]), "Thread #$threadId"); 1839 | $this->assertSame($jobs[$threadId], $res, "Thread #$threadId"); 1840 | unset($jobs[$threadId]); 1841 | $num--; 1842 | } 1843 | } 1844 | $i++; 1845 | } while ($num > 0 && $i < $maxI); 1846 | $this->assertSame(0, $num, 'All jobs must be done'); 1847 | $pool->cleanup(); 1848 | 1849 | 1850 | // TestThreadDoNothing 1851 | $threads = 2; 1852 | $thread = __NAMESPACE__ . '\TestThreadDoNothing'; 1853 | $pool = new ThreadPool( 1854 | $thread, $threads, null, null, $debug 1855 | ); 1856 | 1857 | $i = 0; 1858 | $left = $num = 1; 1859 | $maxI = 6; 1860 | do { 1861 | while ($left > 0 && $pool->hasWaiting()) { 1862 | $threadId = $pool->run(); 1863 | $jobs[$threadId] = null; 1864 | $worked[$threadId] = true; 1865 | $left--; 1866 | } 1867 | $results = $pool->wait($failed); 1868 | $this->assertEmpty($failed, 'Failed results: ' . print_r($failed, true)); 1869 | if ($results) { 1870 | foreach ($results as $threadId => $res) { 1871 | $this->assertTrue( 1872 | array_key_exists($threadId, $jobs), "Thread #$threadId" 1873 | ); 1874 | $this->assertSame($jobs[$threadId], $res, "Thread #$threadId"); 1875 | unset($jobs[$threadId]); 1876 | $num--; 1877 | break 2; 1878 | } 1879 | } 1880 | $i++; 1881 | } while ($i < $maxI); 1882 | $this->assertSame(0, $num, 'All jobs must be done'); 1883 | $pool->cleanup(); 1884 | } 1885 | 1886 | /** 1887 | * Pool, events 1888 | * 1889 | * @param bool $debug 1890 | * @param bool $bigData 1891 | * 1892 | * @throws Exception 1893 | */ 1894 | function processPoolEvent($debug = false, $bigData = false) 1895 | { 1896 | $events = 3; 1897 | $num = 12; 1898 | $threads = 3; 1899 | 1900 | $async = Thread::$useForks; 1901 | 1902 | if ($debug) { 1903 | echo '-----------------------', PHP_EOL, 1904 | "Thread pool events test: ", ($async ? 'Async' : 'Sync'), PHP_EOL, 1905 | '-----------------------', PHP_EOL; 1906 | } 1907 | 1908 | $pool = new ThreadPool( 1909 | __NAMESPACE__ . '\TestThreadEvents', 1910 | $threads, null, null, $debug 1911 | ); 1912 | 1913 | $arg = mt_rand(12, 987); 1914 | if ($bigData) { 1915 | $arg = str_repeat($arg, 100000); 1916 | } 1917 | $jobs = $worked = array(); 1918 | $test = $this; 1919 | $cb = function($event, $threadId, $e_data, $e_arg) use ($arg, $test, &$jobs) { 1920 | /** @var $test TestCase */ 1921 | $test->assertSame($arg, $e_arg); 1922 | $test->assertSame(TestThreadEvents::EV_PROCESS, $event); 1923 | if (!isset($jobs[$threadId])) { 1924 | $jobs[$threadId] = 0; 1925 | } 1926 | $test->assertEquals($jobs[$threadId]++, $e_data); 1927 | }; 1928 | $pool->bind(TestThreadEvents::EV_PROCESS, $cb, $arg); 1929 | 1930 | 1931 | $i = 0; 1932 | $left = $num; 1933 | $maxI = ceil($num * 1.5); 1934 | do { 1935 | while ($left > 0 && $pool->hasWaiting()) { 1936 | $threadId = $pool->run($events); 1937 | $this->assertNotEmpty($threadId); 1938 | $worked[$threadId] = true; 1939 | $left--; 1940 | } 1941 | $results = $pool->wait($failed); 1942 | $this->assertEmpty($failed, 'Failed results: ' . print_r($failed, true)); 1943 | if ($results) { 1944 | foreach ($results as $threadId => $res) { 1945 | $num--; 1946 | $this->assertTrue(isset($jobs[$threadId]), "Thread #$threadId"); 1947 | $this->assertSame($events, $jobs[$threadId]); 1948 | unset($jobs[$threadId]); 1949 | } 1950 | } 1951 | $i++; 1952 | } while ($num > 0 && $i < $maxI); 1953 | 1954 | $this->assertSame(0, $num, 'All jobs must be done'); 1955 | 1956 | $pool->cleanup(); 1957 | } 1958 | 1959 | /** 1960 | * Pool, errors 1961 | * 1962 | * @param bool $debug 1963 | * 1964 | * @throws Exception 1965 | */ 1966 | function processPoolErrorable($debug = false) 1967 | { 1968 | $num = 12; 1969 | $threads = 4; 1970 | 1971 | if ($debug) { 1972 | echo '-----------------------', PHP_EOL, 1973 | "Errorable thread pool test: ", (Thread::$useForks ? 'Async' : 'Sync'), PHP_EOL, 1974 | '-----------------------', PHP_EOL; 1975 | } 1976 | 1977 | $pool = new ThreadPool( 1978 | __NAMESPACE__ . '\TestThreadErrorable', 1979 | $threads, null, null, $debug 1980 | ); 1981 | 1982 | $jobs = array(); 1983 | 1984 | $i = $j = 0; 1985 | $left = $num; 1986 | $maxI = ceil($num * 2.5); 1987 | do { 1988 | while ($left > 0 && $pool->hasWaiting()) { 1989 | $arg = mt_rand(1000000, 200000000); 1990 | $threadId = $pool->run($arg, $j); 1991 | 1992 | $this->assertTrue( 1993 | !isset($jobs[$threadId]), 1994 | "Thread #$threadId is not failed correctly" 1995 | ); 1996 | 1997 | $jobs[$threadId] = $arg; 1998 | $left--; 1999 | $j++; 2000 | } 2001 | if ($results = $pool->wait($failed)) { 2002 | foreach ($results as $threadId => $res) { 2003 | $num--; 2004 | $this->assertTrue(isset($jobs[$threadId]), "Thread #$threadId"); 2005 | $this->assertEquals($jobs[$threadId], $res, "Thread #$threadId"); 2006 | unset($jobs[$threadId]); 2007 | } 2008 | } 2009 | if ($failed) { 2010 | foreach ($failed as $threadId => $errArray) { 2011 | list($errCode, $errMsg) = $errArray; 2012 | $this->assertTrue(isset($jobs[$threadId]), "Thread #$threadId"); 2013 | $this->assertNotEmpty($errCode, 'Error code needed'); 2014 | $this->assertTrue(is_int($errCode), 'Error code needed'); 2015 | $this->assertNotEmpty($errMsg, 'Error message needed'); 2016 | unset($jobs[$threadId]); 2017 | $left++; 2018 | } 2019 | } 2020 | $i++; 2021 | } while ($num > 0 && $i < $maxI); 2022 | 2023 | $this->assertSame(0, $num, 'All jobs must be done'); 2024 | 2025 | $pool->cleanup(); 2026 | $this->assertSame(0, $pool->getThreadsCount()); 2027 | $this->assertEmpty($pool->getThreads()); 2028 | $this->assertFalse($pool->hasWaiting()); 2029 | } 2030 | 2031 | /** 2032 | * Pool, external errors 2033 | * 2034 | * @param bool $debug 2035 | * 2036 | * @throws Exception 2037 | */ 2038 | function processPoolErrorableExternal($debug = false) 2039 | { 2040 | $num = 9; 2041 | $threads = 3; 2042 | 2043 | if ($debug) { 2044 | echo '------------------------------------------', PHP_EOL, 2045 | "Thread pool test with external stop: ", (Thread::$useForks ? 'Async' : 'Sync'), PHP_EOL, 2046 | '------------------------------------------', PHP_EOL; 2047 | } 2048 | 2049 | $pool = new ThreadPool( 2050 | __NAMESPACE__ . '\TestThreadErrorableExternal', 2051 | $threads, null, null, $debug 2052 | ); 2053 | 2054 | $argDebug = $debug && true; 2055 | $debugRef = new ReflectionMethod($pool, 'debug'); 2056 | $debugRef->setAccessible(true); 2057 | $debugCb = function ($msg) use ($argDebug, $debugRef, $pool) { 2058 | $argDebug && $debugRef->invoke($pool, $msg); 2059 | }; 2060 | 2061 | $jobs = array(); 2062 | 2063 | $i = $j = 0; 2064 | $left = $num; 2065 | $maxI = ceil($num * 2.5); 2066 | do { 2067 | while ($left > 0 && $pool->hasWaiting()) { 2068 | $arg = str_repeat( 2069 | mt_rand(100, 999), 2070 | $argDebug ? 3 : 10000 2071 | ); 2072 | 2073 | $threadId = $pool->run($arg, $j); 2074 | 2075 | $this->assertNotEmpty($threadId); 2076 | $debugCb( 2077 | "TEST: Thread #$threadId; Job argument on start [$arg]" 2078 | ); 2079 | 2080 | // Stop child 2081 | if (1 & $j) { 2082 | $threads = $pool->getThreads(); 2083 | $threads[$threadId]->sendSignalToChild( 2084 | ($j % 4) > 1 ? SIGTERM : SIGKILL 2085 | ); 2086 | $debugCb( 2087 | "TEST: Thread #$threadId stopped" 2088 | ); 2089 | unset($threads); 2090 | } 2091 | 2092 | $this->assertTrue( 2093 | !isset($jobs[$threadId]), 2094 | "Thread #$threadId is not failed correctly" 2095 | ); 2096 | 2097 | $jobs[$threadId] = $arg; 2098 | $left--; 2099 | $j++; 2100 | } 2101 | if ($results = $pool->wait($failed)) { 2102 | foreach ($results as $threadId => $res) { 2103 | $debugCb( 2104 | "TEST: Thread #$threadId; Job result [$res]" 2105 | ); 2106 | $this->assertTrue(isset($jobs[$threadId]), "Thread #$threadId"); 2107 | $this->assertEquals($jobs[$threadId], $res, "Thread #$threadId"); 2108 | unset($jobs[$threadId]); 2109 | $num--; 2110 | } 2111 | } 2112 | if ($failed) { 2113 | foreach ($failed as $threadId => $errArray) { 2114 | list($errCode, $errMsg) = $errArray; 2115 | $debugCb( 2116 | "TEST: Thread #$threadId; Job fail [#$errCode - $errMsg]" 2117 | ); 2118 | $this->assertTrue(isset($jobs[$threadId]), "Thread #$threadId"); 2119 | $this->assertNotEmpty($errCode, 'Error code needed'); 2120 | $this->assertTrue(is_int($errCode), 'Error code needed'); 2121 | $this->assertNotEmpty($errMsg, 'Error message needed'); 2122 | unset($jobs[$threadId]); 2123 | $left++; 2124 | } 2125 | } 2126 | $i++; 2127 | } while ($num > 0 && $i < $maxI); 2128 | 2129 | $this->assertSame(0, $num, 'All jobs must be done'); 2130 | 2131 | $pool->cleanup(); 2132 | } 2133 | 2134 | /** 2135 | * Exception in events triggering 2136 | * 2137 | * @param bool $debug 2138 | */ 2139 | function processExceptionInEventListener($debug = false) 2140 | { 2141 | if ($debug) { 2142 | echo '-----------------------', PHP_EOL, 2143 | "Exception in event listener test: ", (Thread::$useForks ? 'Async' : 'Sync'), PHP_EOL, 2144 | '-----------------------', PHP_EOL; 2145 | } 2146 | 2147 | $thread = new TestThreadEvents(null, null, $debug); 2148 | 2149 | $testCase = $this; 2150 | $thread->bind( 2151 | TestThreadEvents::EV_PROCESS, 2152 | function($event, $e_data, $e_arg) use ($testCase) { 2153 | $testCase->assertSame(null, $e_arg); 2154 | $testCase->assertSame( 2155 | TestThreadEvents::EV_PROCESS, 2156 | $event 2157 | ); 2158 | $testCase->assertEquals(0, $e_data); 2159 | 2160 | throw new InvalidArgumentException( 2161 | 'Example Message' 2162 | ); 2163 | } 2164 | ); 2165 | 2166 | try { 2167 | $thread->wait()->run(1)->wait(); 2168 | } catch (InvalidArgumentException $e) { 2169 | $catched = true; 2170 | $this->assertTrue($e instanceof InvalidArgumentException); 2171 | $this->assertContains( 2172 | 'Example Message', $e->getMessage() 2173 | ); 2174 | } 2175 | $this->assertFalse(empty($catched)); 2176 | 2177 | $thread->cleanup(); 2178 | } 2179 | 2180 | 2181 | /** 2182 | * Two different threads 2183 | */ 2184 | function processTwoDifferentThreads($debug = false) 2185 | { 2186 | $async = Thread::$useForks; 2187 | 2188 | if ($debug) { 2189 | echo '-----------------------', PHP_EOL, 2190 | "Different threads test ", ($async ? 'Async' : 'Sync'), PHP_EOL, 2191 | '-----------------------', PHP_EOL; 2192 | } 2193 | 2194 | 2195 | $arg1 = 123456789; 2196 | $arg2 = 987654321; 2197 | 2198 | $thread1 = new TestThreadReturnFirstArgument( 2199 | null, null, $debug 2200 | ); 2201 | $thread2 = new TestThreadReturnFirstArgument( 2202 | null, null, $debug 2203 | ); 2204 | 2205 | 2206 | $thread1->wait(); 2207 | $this->assertEmpty( 2208 | $thread1->getLastErrorCode(), 2209 | $thread1->getLastErrorMsg() 2210 | ); 2211 | $async && $this->assertTrue($thread1->getIsForked()); 2212 | $thread1->run($arg1); 2213 | 2214 | $thread2->wait(); 2215 | $this->assertEmpty( 2216 | $thread2->getLastErrorCode(), 2217 | $thread2->getLastErrorMsg() 2218 | ); 2219 | $async && $this->assertTrue($thread2->getIsForked()); 2220 | $thread2->run($arg2); 2221 | 2222 | $res2 = $thread2->wait()->getResult(); 2223 | $res1 = $thread1->wait()->getResult(); 2224 | $this->assertTrue($thread1->getSuccess()); 2225 | $this->assertSame($arg1, $res1); 2226 | $this->assertTrue($thread2->getSuccess()); 2227 | $this->assertSame($arg2, $res2); 2228 | 2229 | 2230 | $thread2->run($arg1); 2231 | $thread1->run($arg2); 2232 | 2233 | $res1 = $thread1->wait()->getResult(); 2234 | $res2 = $thread2->wait()->getResult(); 2235 | $this->assertSame($arg2, $res1); 2236 | $this->assertSame($arg1, $res2); 2237 | 2238 | 2239 | $thread1->run($arg1); 2240 | $thread2->run($arg2); 2241 | 2242 | $res1 = $thread1->wait()->getResult(); 2243 | $res2 = $thread2->wait()->getResult(); 2244 | $this->assertSame($arg1, $res1); 2245 | $this->assertSame($arg2, $res2); 2246 | 2247 | 2248 | $thread2->cleanup(); 2249 | 2250 | $res1 = $thread1->run($arg2)->wait()->getResult(); 2251 | $this->assertSame($arg2, $res1); 2252 | 2253 | 2254 | $thread1->cleanup(); 2255 | } 2256 | 2257 | 2258 | /** 2259 | * Event callback for tests 2260 | */ 2261 | function processEventCallback($event, $e_data, $e_arg) 2262 | { 2263 | $this->assertSame(TestThreadEvents::EV_PROCESS, $event); 2264 | $this->assertEquals(0, $e_arg); 2265 | $this->assertNotSame(null, $e_data); 2266 | } 2267 | 2268 | #endregion 2269 | } 2270 | 2271 | 2272 | 2273 | #region Test mocks 2274 | 2275 | /** 2276 | * Test thread 2277 | */ 2278 | class TestThreadArgumentsMapping extends Thread 2279 | { 2280 | /** 2281 | * {@inheritdoc} 2282 | */ 2283 | protected $timeoutMasterResultWait = 2; 2284 | 2285 | /** 2286 | * {@inheritdoc} 2287 | */ 2288 | protected $argumentsMapping = true; 2289 | 2290 | /** 2291 | * {@inheritdoc} 2292 | */ 2293 | function process($a, $b, $c) 2294 | { 2295 | return array($a, $b, $c); 2296 | } 2297 | } 2298 | 2299 | /** 2300 | * Test thread 2301 | */ 2302 | class TestThreadReturnFirstArgument extends Thread 2303 | { 2304 | /** 2305 | * {@inheritdoc} 2306 | */ 2307 | protected $timeoutMasterInitWait = 1; 2308 | 2309 | /** 2310 | * {@inheritdoc} 2311 | */ 2312 | protected $timeoutMasterResultWait = 10; 2313 | 2314 | /** 2315 | * {@inheritdoc} 2316 | */ 2317 | protected $timeoutWorkerJobWait = 15; 2318 | 2319 | 2320 | /** 2321 | * {@inheritdoc} 2322 | */ 2323 | function process() 2324 | { 2325 | $param = $this->getParam(0); 2326 | if ($this->debug && is_scalar($param) && strlen($param) < 50) { 2327 | $this->debug("TEST: Job argument in worker [$param]"); 2328 | } 2329 | return $param; 2330 | } 2331 | } 2332 | 2333 | /** 2334 | * Test thread 2335 | */ 2336 | class TestThreadOneTask extends TestThreadReturnFirstArgument 2337 | { 2338 | /** 2339 | * {@inheritdoc} 2340 | */ 2341 | protected $multitask = false; 2342 | 2343 | /** 2344 | * {@inheritdoc} 2345 | */ 2346 | protected $prefork = false; 2347 | } 2348 | 2349 | /** 2350 | * Test thread 2351 | */ 2352 | class TestThreadDoNothing extends TestThreadReturnFirstArgument 2353 | { 2354 | /** 2355 | * {@inheritdoc} 2356 | */ 2357 | function process() {} 2358 | } 2359 | 2360 | /** 2361 | * Test thread 2362 | */ 2363 | class TestThreadHooks extends TestThreadReturnFirstArgument 2364 | { 2365 | /** @var int */ 2366 | public $onLoadHookCalls = 0; 2367 | 2368 | /** @var int */ 2369 | public $onForkHookCalls = 0; 2370 | 2371 | /** @var int */ 2372 | public $onShutdownHookCalls = 0; 2373 | 2374 | /** @var int */ 2375 | public $onCleanupHookCalls = 0; 2376 | 2377 | 2378 | /** 2379 | * {@inheritdoc} 2380 | */ 2381 | protected function onLoad() 2382 | { 2383 | $this->onLoadHookCalls++; 2384 | } 2385 | 2386 | /** 2387 | * {@inheritdoc} 2388 | */ 2389 | protected function onFork() 2390 | { 2391 | $this->onForkHookCalls++; 2392 | } 2393 | 2394 | /** 2395 | * {@inheritdoc} 2396 | */ 2397 | protected function onShutdown() 2398 | { 2399 | $this->onShutdownHookCalls++; 2400 | } 2401 | 2402 | /** 2403 | * {@inheritdoc} 2404 | */ 2405 | protected function onCleanup() 2406 | { 2407 | $this->onCleanupHookCalls++; 2408 | } 2409 | 2410 | 2411 | /** 2412 | * {@inheritdoc} 2413 | */ 2414 | function process() 2415 | { 2416 | return array( 2417 | $this->onLoadHookCalls, 2418 | $this->onForkHookCalls, 2419 | $this->onShutdownHookCalls, 2420 | $this->onCleanupHookCalls 2421 | ); 2422 | } 2423 | } 2424 | 2425 | /** 2426 | * Test thread 2427 | */ 2428 | class TestThreadReturnAllArguments extends TestThreadReturnFirstArgument 2429 | { 2430 | /** 2431 | * {@inheritdoc} 2432 | */ 2433 | function process() 2434 | { 2435 | return $this->getParams(); 2436 | } 2437 | } 2438 | 2439 | /** 2440 | * Test thread 2441 | */ 2442 | class TestThreadErrorable extends TestThreadReturnFirstArgument 2443 | { 2444 | /** 2445 | * {@inheritdoc} 2446 | */ 2447 | protected $timeoutMasterResultWait = 0.1; 2448 | 2449 | /** 2450 | * {@inheritdoc} 2451 | */ 2452 | function process() 2453 | { 2454 | if (1 & ($arg = (int)$this->getParam(1))) { 2455 | // Emulate terminating 2456 | $signo = ($arg % 4) > 1 ? SIGTERM : SIGKILL; 2457 | if ($this->debug) { 2458 | $signame = Base::getSignalName($signo); 2459 | $this->debug("TEST: Emulate terminating with $signame ($signo)"); 2460 | } 2461 | $this->sendSignalToChild($signo); 2462 | } 2463 | return parent::process(); 2464 | } 2465 | } 2466 | 2467 | /** 2468 | * Test thread 2469 | */ 2470 | class TestThreadErrorableExternal extends TestThreadReturnFirstArgument 2471 | { 2472 | /** 2473 | * {@inheritdoc} 2474 | */ 2475 | protected $timeoutMasterResultWait = 0.1; 2476 | } 2477 | 2478 | /** 2479 | * Test thread 2480 | */ 2481 | class TestThreadWithChilds extends TestThreadReturnFirstArgument 2482 | { 2483 | /** 2484 | * Enable prefork waiting to test it 2485 | */ 2486 | protected $preforkWait = true; 2487 | 2488 | /** 2489 | * {@inheritdoc} 2490 | */ 2491 | function process() 2492 | { 2493 | /** @noinspection PhpUnusedLocalVariableInspection */ 2494 | $res = `echo 1`; 2495 | return $this->getParam(0); 2496 | } 2497 | } 2498 | 2499 | /** 2500 | * Test thread 2501 | */ 2502 | class TestThreadEvents extends TestThreadReturnFirstArgument 2503 | { 2504 | const EV_PROCESS = 'process'; 2505 | 2506 | /** 2507 | * {@inheritdoc} 2508 | */ 2509 | protected $timeoutMasterResultWait = 2; 2510 | 2511 | /** 2512 | * {@inheritdoc} 2513 | */ 2514 | protected $prefork = false; 2515 | 2516 | /** 2517 | * {@inheritdoc} 2518 | */ 2519 | function process() 2520 | { 2521 | $events = $this->getParam(0); 2522 | for ($i = 0; $i < $events; $i++) { 2523 | $this->trigger(self::EV_PROCESS, $i); 2524 | } 2525 | } 2526 | } 2527 | 2528 | /** 2529 | * Test thread 2530 | */ 2531 | class TestPreforkWaitTimeout extends Thread 2532 | { 2533 | /** 2534 | * {@inheritdoc} 2535 | */ 2536 | protected $preforkWait = true; 2537 | 2538 | /** 2539 | * {@inheritdoc} 2540 | */ 2541 | protected $timeoutMasterInitWait = 0.000001; 2542 | 2543 | /** 2544 | * {@inheritdoc} 2545 | */ 2546 | protected function onFork() 2547 | { 2548 | // To certainly meet the timeout 2549 | usleep(20000); 2550 | } 2551 | 2552 | /** 2553 | * {@inheritdoc} 2554 | */ 2555 | function process() {} 2556 | } 2557 | 2558 | /** 2559 | * Test thread 2560 | */ 2561 | class TestResultWaitTimeout extends Thread 2562 | { 2563 | /** 2564 | * {@inheritdoc} 2565 | */ 2566 | protected $timeoutMasterResultWait = 0.000001; 2567 | 2568 | /** 2569 | * {@inheritdoc} 2570 | */ 2571 | function process() 2572 | { 2573 | // To certainly meet the timeout 2574 | usleep(20000); 2575 | } 2576 | } 2577 | 2578 | /** 2579 | * Test thread 2580 | */ 2581 | class TestJobWaitTimeout extends Thread 2582 | { 2583 | /** 2584 | * {@inheritdoc} 2585 | */ 2586 | protected $timeoutWorkerJobWait = 0.000001; 2587 | 2588 | /** 2589 | * {@inheritdoc} 2590 | */ 2591 | protected $listenMasterSignals = false; 2592 | 2593 | /** 2594 | * {@inheritdoc} 2595 | */ 2596 | function process() {} 2597 | } 2598 | 2599 | /** 2600 | * Test thread 2601 | */ 2602 | class TestParentCheckTimeout extends Thread 2603 | { 2604 | const EV_PID = 'pid'; 2605 | 2606 | /** 2607 | * {@inheritdoc} 2608 | */ 2609 | protected $timeoutMasterResultWait = 0.1; 2610 | 2611 | /** 2612 | * {@inheritdoc} 2613 | */ 2614 | function process() 2615 | { 2616 | // Create new thread. Set debug flag from this thread 2617 | $child = new TestParentCheckTimeoutChild( 2618 | null, null, $this->debug, array( 2619 | 'eventLoop' => new EventBase() 2620 | ) 2621 | ); 2622 | 2623 | // Send child PID to parent via event 2624 | $childPid = $child->wait()->getChildPid(); 2625 | $this->trigger(self::EV_PID, $childPid); 2626 | 2627 | // Emulate terminating with small timeout 2628 | $this->sendSignalToChild(SIGKILL); 2629 | } 2630 | } 2631 | 2632 | /** 2633 | * Test thread 2634 | */ 2635 | class TestParentCheckTimeoutChild extends Thread 2636 | { 2637 | /** 2638 | * {@inheritdoc} 2639 | */ 2640 | protected $intervalWorkerMasterChecks = 0.001; 2641 | 2642 | /** 2643 | * {@inheritdoc} 2644 | */ 2645 | function process() {} 2646 | 2647 | /** 2648 | * {@inheritdoc} 2649 | */ 2650 | protected function registerEventSignals($allSignals = true) {} 2651 | } 2652 | 2653 | /** 2654 | * Test thread 2655 | */ 2656 | class TestSignalsHandling extends TestThreadReturnFirstArgument 2657 | { 2658 | /** 2659 | * @var int 2660 | */ 2661 | protected $catchedSignals = 0; 2662 | 2663 | /** 2664 | * @var int 2665 | */ 2666 | public static $catchedSignalsInParent = 0; 2667 | 2668 | 2669 | /** 2670 | * {@inheritdoc} 2671 | */ 2672 | function process() 2673 | { 2674 | return $this->sendSignalToParent(SIGUSR2)->catchedSignals; 2675 | } 2676 | 2677 | 2678 | /** 2679 | * Child SIGUSR1 handler 2680 | */ 2681 | protected function sigUsr1() 2682 | { 2683 | $this->catchedSignals++; 2684 | } 2685 | 2686 | /** 2687 | * Parent SIGUSR2 handler 2688 | */ 2689 | protected static function mSigUsr2() 2690 | { 2691 | self::$catchedSignalsInParent++; 2692 | } 2693 | } 2694 | 2695 | #endregion 2696 | -------------------------------------------------------------------------------- /ThreadPool.php: -------------------------------------------------------------------------------- 1 | 18 | * @license MIT 19 | */ 20 | class ThreadPool 21 | { 22 | /** 23 | * Default pool name 24 | */ 25 | const DEFAULT_NAME = 'base'; 26 | 27 | /** 28 | * All created pools count 29 | * 30 | * @var int 31 | */ 32 | protected static $allPoolsCount = 0; 33 | 34 | 35 | #region Internal properties 36 | 37 | /** 38 | * Maximum threads number in pool 39 | */ 40 | protected $maxThreads = 4; 41 | 42 | /** 43 | * Internal unique pool id 44 | * 45 | * @var int 46 | */ 47 | protected $id; 48 | 49 | /** 50 | * Internal pool name 51 | * 52 | * @var string 53 | */ 54 | protected $poolName; 55 | 56 | /** 57 | * Thread class name 58 | * 59 | * @var string 60 | */ 61 | protected $threadClassName; 62 | 63 | /** 64 | * Thread process name 65 | * 66 | * @var null|string 67 | */ 68 | protected $threadProcessName; 69 | 70 | /** 71 | * Event listeners 72 | */ 73 | protected $listeners = array(); 74 | 75 | /** 76 | * Threads in pool (threadId => thread) 77 | * 78 | * @var Thread[] 79 | */ 80 | protected $threads = array(); 81 | 82 | /** 83 | * Waiting for job threads IDs (threadId => threadId) 84 | * 85 | * @var int[] 86 | */ 87 | protected $waitingForJob = array(); 88 | 89 | /** 90 | * Waiting for result fetch threads IDs (threadId => threadId) 91 | * 92 | * @var int[] 93 | */ 94 | protected $waitingForFetch = array(); 95 | 96 | /** 97 | * Working threads IDs (threadId => threadId) 98 | * 99 | * @var int[] 100 | */ 101 | protected $working = array(); 102 | 103 | /** 104 | * Initializing threads IDs (threadId => threadId) 105 | * 106 | * @var int[] 107 | */ 108 | protected $initializing = array(); 109 | 110 | /** 111 | * Failed threads IDs (threadId => [errorCode, errorMsg]) 112 | * 113 | * @var array[] 114 | */ 115 | protected $failures = array(); 116 | 117 | /** 118 | * Results flags (threadId => threadId) 119 | */ 120 | protected $resultFlags = array(); 121 | 122 | /** 123 | * Received results (threadId => result) 124 | */ 125 | protected $results = array(); 126 | 127 | /** 128 | * Current threads count 129 | */ 130 | protected $threadsCount = 0; 131 | 132 | /** 133 | * Flag for detached state. 134 | * Enabled in child process after forking. 135 | */ 136 | protected $detached = false; 137 | 138 | /** 139 | * Whether to show debugging information. 140 | * DO NOT USE IN PRODUCTION! 141 | * 142 | * @internal 143 | */ 144 | public $debug = false; 145 | 146 | #endregion 147 | 148 | 149 | 150 | #region Initialization 151 | 152 | /** 153 | * Thread pool initialization 154 | * 155 | * @param string $threadName Thread class name 156 | * @param int $maxThreads Maximum threads number in pool 157 | * @param string $pName Thread process name 158 | * @param string $name Pool name 159 | * @param bool $debug Whether to enable debug mode 160 | * 161 | * @internal 162 | */ 163 | public function __construct($threadName, $maxThreads = null, 164 | $pName = null, $name = null, $debug = false) 165 | { 166 | $debug && $this->debug = true; 167 | 168 | $this->id = ++self::$allPoolsCount; 169 | $this->poolName = $name ?: self::DEFAULT_NAME; 170 | $this->threadClassName = $threadName; 171 | 172 | // @codeCoverageIgnoreStart 173 | $this->debug( 174 | "Pool of '$threadName' threads created (" 175 | . ltrim(spl_object_hash($this), '0') . ')' 176 | ); 177 | // @codeCoverageIgnoreEnd 178 | 179 | if (Thread::$useForks) { 180 | if ($maxThreads > 0) { 181 | $this->maxThreads = (int)$maxThreads; 182 | } 183 | } else { 184 | $this->maxThreads = 1; 185 | } 186 | 187 | if ($pName) { 188 | $this->threadProcessName = $pName; 189 | } 190 | 191 | $this->createAllThreads(); 192 | } 193 | 194 | /** 195 | * Creates threads while pool has free slots 196 | */ 197 | protected function createAllThreads() 198 | { 199 | $count = &$this->threadsCount; 200 | $tMax = $this->maxThreads; 201 | if ($count < $tMax) { 202 | $tName = $this->threadClassName; 203 | $pName = $this->threadProcessName; 204 | $debug = $this->debug; 205 | do { 206 | /** @var $thread Thread */ 207 | $thread = new $tName($pName, $this, $debug); 208 | $count++; 209 | 210 | // @codeCoverageIgnoreStart 211 | $debug && $this->debug( 212 | "Thread #{$thread->getId()} created" 213 | ); 214 | // @codeCoverageIgnoreEnd 215 | } while ($count < $tMax); 216 | } 217 | } 218 | 219 | #endregion 220 | 221 | 222 | 223 | #region Cleanup 224 | 225 | /** 226 | * Destruction 227 | * 228 | * @internal 229 | */ 230 | public function __destruct() 231 | { 232 | // @codeCoverageIgnoreStart 233 | $this->debug && $this->debug( 234 | 'Destructor (' . ltrim(spl_object_hash($this), '0') . ')' 235 | ); 236 | // @codeCoverageIgnoreEnd 237 | $this->cleanup(); 238 | } 239 | 240 | /** 241 | * Pool cleanup 242 | */ 243 | public function cleanup() 244 | { 245 | // @codeCoverageIgnoreStart 246 | ($debug = $this->debug) && $this->debug( 247 | 'Cleanup (' . ltrim(spl_object_hash($this), '0') . ')' 248 | . ($this->detached ? ' (FORCED - redundant instance)' : '') 249 | ); 250 | // @codeCoverageIgnoreEnd 251 | 252 | // Destroy all threads 253 | if (!$this->detached) { 254 | foreach ($this->threads as $thread) { 255 | $thread->cleanup(); 256 | } 257 | } 258 | 259 | // Clean all array fields in pool 260 | $this->listeners = 261 | $this->threads = 262 | $this->waitingForJob = 263 | $this->waitingForFetch = 264 | $this->working = 265 | $this->initializing = 266 | $this->failures = 267 | $this->results = array(); 268 | } 269 | 270 | /** 271 | * Pool detaching (special cleanup 272 | * for pool instance in child process) 273 | * 274 | * @internal 275 | * 276 | * @codeCoverageIgnore Called only in child (can't get coverage from another process) 277 | */ 278 | public function detach() 279 | { 280 | if (!$this->detached) { 281 | $this->detached = true; 282 | $this->cleanup(); 283 | 284 | $this->debug && $this->debug( 285 | "Thread pool is detached" 286 | ); 287 | } else { 288 | $this->debug && $this->debug( 289 | "Thread pool is already detached" 290 | ); 291 | } 292 | } 293 | 294 | #endregion 295 | 296 | 297 | 298 | /** 299 | * Starts job in one of the idle threads 300 | * 301 | * @see hasWaiting 302 | * @see wait 303 | * 304 | * @return int ID of thread that started the job 305 | * 306 | * @throws Exception if no free threads in pool 307 | */ 308 | public function run() 309 | { 310 | if ($waiting = $this->waitingForJob) { 311 | $threadId = reset($waiting); 312 | $thread = $this->threads[$threadId]; 313 | 314 | // @codeCoverageIgnoreStart 315 | ($debug = $this->debug) && $this->debug( 316 | "Starting job in thread #{$threadId}..." 317 | ); 318 | // @codeCoverageIgnoreEnd 319 | 320 | // Use strict call for speedup 321 | // if number of arguments is not too big 322 | $args = func_get_args(); 323 | $count = count($args); 324 | if (0 === $count) { 325 | $thread->run(); 326 | } else if (1 === $count) { 327 | $thread->run($args[0]); 328 | } else if (2 === $count) { 329 | $thread->run($args[0], $args[1]); 330 | } else if (3 === $count) { 331 | $thread->run($args[0], $args[1], $args[2]); 332 | } else { 333 | call_user_func_array(array($thread, 'run'), $args); 334 | } 335 | 336 | // @codeCoverageIgnoreStart 337 | $debug && $this->debug( 338 | "Thread #$threadId started" 339 | ); 340 | // @codeCoverageIgnoreEnd 341 | 342 | return $threadId; 343 | } 344 | 345 | // Strict approach 346 | throw new Exception('No threads waiting for the job'); 347 | } 348 | 349 | 350 | 351 | #region Master waiting 352 | 353 | /** 354 | * Waits for waiting threads in pool 355 | * 356 | * @param array $failed Array of failed threads 357 | * 358 | * @return array Returns array of results (can be empty) 359 | * 360 | * @throws Exception 361 | */ 362 | public function wait(&$failed = null) 363 | { 364 | if ($this->waitingForFetch) { 365 | return $this->getResults($failed); 366 | } 367 | 368 | $threadIds = $this->working + $this->initializing; 369 | if ($threadIds) { 370 | // @codeCoverageIgnoreStart 371 | $this->debug && $this->debug( 372 | 'Waiting for threads: #' . join(', #', $threadIds) 373 | ); 374 | // @codeCoverageIgnoreEnd 375 | 376 | Thread::waitThreads($threadIds); 377 | } else { 378 | // Should not be called 379 | // @codeCoverageIgnoreStart 380 | throw new Exception( 381 | 'Nothing to wait in pool' 382 | ); 383 | // @codeCoverageIgnoreEnd 384 | } 385 | 386 | return $this->getResults($failed); 387 | } 388 | 389 | /** 390 | * Returns array of results by threads 391 | * 392 | * @param array &$failed Array of failed threads 393 | * 394 | * @return array 395 | */ 396 | protected function getResults(&$failed = null) 397 | { 398 | $results = $this->results; 399 | $failed = $this->failures; 400 | 401 | $this->waitingForJob += $this->waitingForFetch; 402 | 403 | // @codeCoverageIgnoreStart 404 | if ($this->debug) { 405 | $this->debug( 406 | $results 407 | ? 'Fetching results for threads: #' 408 | . join(', #', array_keys($results)) 409 | : "No results to return" 410 | ); 411 | $failed && $this->debug( 412 | 'Fetching FAILED results for threads: #' 413 | . join(', #', array_keys($failed)) 414 | ); 415 | foreach ($this->waitingForFetch as $threadId) { 416 | $this->debug( 417 | "Thread #{$threadId} is marked as waiting for job" 418 | ); 419 | } 420 | } 421 | // @codeCoverageIgnoreEnd 422 | 423 | $this->results = 424 | $this->resultFlags = 425 | $this->failures = 426 | $this->waitingForFetch = array(); 427 | 428 | return $results; 429 | } 430 | 431 | #endregion 432 | 433 | 434 | 435 | #region Thread events dispatching 436 | 437 | /** 438 | * Connects a listener to a given event. 439 | * 440 | * @see trigger 441 | * 442 | * @param string $event443 | * An event name. 444 | *
445 | * @param callback $listener
446 | * Callback to be called when the matching event occurs.
447 | *
function(string $event_name, int $thread_id, mixed $event_data, mixed $event_arg){}
448 | *
450 | * Additional argument for callback. 451 | *
452 | */ 453 | public function bind($event, $listener, $arg = null) 454 | { 455 | 456 | if (!isset($this->listeners[$event])) { 457 | $this->listeners[$event] = array(); 458 | } 459 | $this->listeners[$event][] = array($listener, $arg); 460 | 461 | // @codeCoverageIgnoreStart 462 | $this->debug && $this->debug( 463 | "New listener binded on event [$event]" 464 | ); 465 | // @codeCoverageIgnoreEnd 466 | } 467 | 468 | /** 469 | * Notifies all listeners of a given event. 470 | * 471 | * @see bind 472 | * 473 | * @param string $event An event name 474 | * @param int $threadId Id of thread that caused the event 475 | * @param mixed $data Event data for callback 476 | */ 477 | public function trigger($event, $threadId, $data = null) 478 | { 479 | // @codeCoverageIgnoreStart 480 | ($debug = $this->debug) && $this->debug( 481 | "Triggering event \"$event\" on pool" 482 | ); 483 | // @codeCoverageIgnoreEnd 484 | 485 | if (!empty($this->listeners[$event])) { 486 | // @codeCoverageIgnoreStart 487 | $debug && $this->debug( 488 | "Pool has event listeners. Notify them..." 489 | ); 490 | // @codeCoverageIgnoreEnd 491 | 492 | /** @var $cb callback */ 493 | foreach ($this->listeners[$event] as $l) { 494 | list($cb, $arg) = $l; 495 | if ($cb instanceof \Closure) { 496 | $cb($event, $threadId, $data, $arg); 497 | } else { 498 | call_user_func( 499 | $cb, $event, $threadId, $data, $arg 500 | ); 501 | } 502 | } 503 | } 504 | } 505 | 506 | #endregion 507 | 508 | 509 | 510 | #region Getters/Setters 511 | 512 | /** 513 | * Returns if pool has waiting threads 514 | * 515 | * @return bool 516 | */ 517 | public function hasWaiting() 518 | { 519 | return (bool)$this->waitingForJob; 520 | } 521 | 522 | 523 | /** 524 | * Returns internal unique pool id 525 | * 526 | * @return int 527 | */ 528 | public function getId() 529 | { 530 | return $this->id; 531 | } 532 | 533 | /** 534 | * Returns internal pool name 535 | * 536 | * @return string 537 | */ 538 | public function getPoolName() 539 | { 540 | return $this->poolName; 541 | } 542 | 543 | 544 | /** 545 | * Returns thread process name 546 | * 547 | * @return null|string 548 | */ 549 | public function getThreadProcessName() 550 | { 551 | return $this->threadProcessName; 552 | } 553 | 554 | /** 555 | * Returns thread class name 556 | * 557 | * @return string 558 | */ 559 | public function getThreadClassName() 560 | { 561 | return $this->threadClassName; 562 | } 563 | 564 | 565 | /** 566 | * Returns pool threads 567 | * 568 | * @return Thread[] array of threads (threadId => thread) 569 | */ 570 | public function getThreads() 571 | { 572 | return $this->threads; 573 | } 574 | 575 | /** 576 | * Returns status of all threads in pool 577 | * 578 | * @return string[] Array of statuses (threadId => stateName) 579 | */ 580 | public function getThreadsState() 581 | { 582 | $state = array(); 583 | foreach ($this->threads as $threadId => $thread) { 584 | $state[$threadId] = $thread->getStateName(); 585 | } 586 | return $state; 587 | } 588 | 589 | /** 590 | * Returns statistic for all threads in pool 591 | * 592 | * @return array[] Array of data (threadId => data array) 593 | * with fields: state, started_jobs, successful_jobs, failed_jobs 594 | */ 595 | public function getThreadsStatistic() 596 | { 597 | $state = array(); 598 | foreach ($this->threads as $threadId => $thread) { 599 | $state[$threadId] = array( 600 | 'state' => $thread->getStateName(), 601 | 'started_jobs' => $thread->getStartedJobs(), 602 | 'successful_jobs' => $thread->getSuccessfulJobs(), 603 | 'failed_jobs' => $thread->getFailedJobs(), 604 | ); 605 | } 606 | return $state; 607 | } 608 | 609 | /** 610 | * Returns current threads count 611 | */ 612 | public function getThreadsCount() 613 | { 614 | return $this->threadsCount; 615 | } 616 | 617 | 618 | /** 619 | * Returns maximum threads number in pool. 620 | * 621 | * You can increase this number with 622 | * {@link setMaxThreads}, but not decrease 623 | * 624 | * @see setMaxThreads 625 | * 626 | * @return int 627 | */ 628 | public function getMaxThreads() 629 | { 630 | return $this->maxThreads; 631 | } 632 | 633 | /** 634 | * Sets maximum threads number 635 | * 636 | * @param int $value 637 | */ 638 | public function setMaxThreads($value) 639 | { 640 | // Filter value 641 | if ($value < $this->threadsCount) { 642 | $value = $this->threadsCount; 643 | } else if (!Thread::$useForks || $value < 1) { 644 | $value = 1; 645 | } else { 646 | $value = (int)$value; 647 | } 648 | 649 | // Apply new value 650 | if ($value !== $this->maxThreads) { 651 | // @codeCoverageIgnoreStart 652 | $this->debug && $this->debug( 653 | "The number of threads changed: " 654 | . "{$this->maxThreads} => $value" 655 | ); 656 | // @codeCoverageIgnoreEnd 657 | 658 | $this->maxThreads = (int)$value; 659 | 660 | $this->createAllThreads(); 661 | } 662 | } 663 | 664 | #endregion 665 | 666 | 667 | 668 | #region Internal API for usage from threads 669 | 670 | /** 671 | * Registers thread in pool 672 | * 673 | * @param Thread $thread 674 | * 675 | * @internal 676 | */ 677 | public function registerThread($thread) 678 | { 679 | $this->threads[$thread->getId()] = $thread; 680 | 681 | // @codeCoverageIgnoreStart 682 | $this->debug && $this->debug( 683 | "Thread #{$thread->getId()} registered in pool" 684 | ); 685 | // @codeCoverageIgnoreEnd 686 | } 687 | 688 | /** 689 | * Unregisters thread in pool 690 | * 691 | * @param int $threadId 692 | * 693 | * @internal 694 | */ 695 | public function unregisterThread($threadId) 696 | { 697 | if (isset($this->threads[$threadId])) { 698 | unset( 699 | $this->threads[$threadId], 700 | $this->waitingForJob[$threadId], 701 | $this->working[$threadId], 702 | $this->initializing[$threadId] 703 | ); 704 | $this->threadsCount--; 705 | 706 | // @codeCoverageIgnoreStart 707 | $this->debug && $this->debug( 708 | "Thread #{$threadId} removed from pool" 709 | ); 710 | // @codeCoverageIgnoreEnd 711 | } 712 | } 713 | 714 | 715 | /** 716 | * Sets processing result from thread 717 | * 718 | * @param int $threadId 719 | * @param mixed $result 720 | * 721 | * @throws Exception 722 | * 723 | * @internal 724 | */ 725 | public function setResultForThread($threadId, $result) 726 | { 727 | if (empty($this->working[$threadId])) { 728 | // @codeCoverageIgnoreStart 729 | // Should not be called 730 | // Break event loop to avoid freezes and other bugs 731 | $base = isset($this->threads[$threadId]) 732 | ? $this->threads[$threadId]->getEventLoop() 733 | : EventBase::getMainLoop(false); 734 | $base && $base->loopBreak(); 735 | 736 | throw new Exception("Incorrect thread for result #$threadId"); 737 | // @codeCoverageIgnoreEnd 738 | } 739 | 740 | $this->results[$threadId] = $result; 741 | $this->resultFlags[$threadId] = $threadId; 742 | 743 | // @codeCoverageIgnoreStart 744 | $this->debug && $this->debug( 745 | "Received result from thread #{$threadId}" 746 | ); 747 | // @codeCoverageIgnoreEnd 748 | } 749 | 750 | 751 | /** 752 | * Marks thread as waiting for job 753 | * 754 | * @param int $threadId 755 | * @param int $errorCode 756 | * @param string $errorMsg 757 | * 758 | * @throws Exception 759 | * 760 | * @internal 761 | */ 762 | public function markThreadWaiting($threadId, $errorCode = null, $errorMsg = null) 763 | { 764 | // Working thread 765 | if (isset($this->working[$threadId])) { 766 | if (empty($this->resultFlags[$threadId])) { 767 | $this->failures[$threadId] = array($errorCode, $errorMsg); 768 | 769 | // @codeCoverageIgnoreStart 770 | $e = new Exception(); 771 | $this->debug && $this->debug( 772 | "Received fail from thread #{$threadId}: " 773 | ."Error $errorCode - $errorMsg" 774 | ); 775 | // @codeCoverageIgnoreEnd 776 | } 777 | 778 | unset($this->working[$threadId]); 779 | $this->waitingForFetch[$threadId] = $threadId; 780 | 781 | // @codeCoverageIgnoreStart 782 | $this->debug && $this->debug( 783 | "Thread #{$threadId} is marked as waiting for results fetching" 784 | ); 785 | // @codeCoverageIgnoreEnd 786 | 787 | return; 788 | } 789 | 790 | // Skipping.. async fail 791 | else if (isset($this->waitingForFetch[$threadId]) 792 | || isset($this->waitingForJob[$threadId]) 793 | ) { 794 | return; 795 | } 796 | 797 | // Initializing thread 798 | else if (isset($this->initializing[$threadId])) { 799 | unset($this->initializing[$threadId]); 800 | $this->waitingForJob[$threadId] = $threadId; 801 | 802 | // @codeCoverageIgnoreStart 803 | $this->debug && $this->debug( 804 | "Thread #{$threadId} is marked as waiting for job" 805 | ); 806 | // @codeCoverageIgnoreEnd 807 | 808 | return; 809 | } 810 | 811 | // @codeCoverageIgnoreStart 812 | // Incorrect thread - should not be called 813 | // Break event loop to avoid freezes and other bugs 814 | $base = isset($this->threads[$threadId]) 815 | ? $this->threads[$threadId]->getEventLoop() 816 | : EventBase::getMainLoop(false); 817 | $base && $base->loopBreak(); 818 | 819 | throw new Exception( 820 | "Incorrect (not working) thread for waiting for the job #$threadId" 821 | ); 822 | // @codeCoverageIgnoreEnd 823 | } 824 | 825 | /** 826 | * Marks thread as waiting for job 827 | * 828 | * @param int $threadId 829 | * 830 | * @throws Exception 831 | * 832 | * @internal 833 | */ 834 | public function markThreadWorking($threadId) 835 | { 836 | if (empty($this->waitingForJob[$threadId])) { 837 | // @codeCoverageIgnoreStart 838 | // Should not be called 839 | // Break event loop to avoid freezes and other bugs 840 | $base = isset($this->threads[$threadId]) 841 | ? $this->threads[$threadId]->getEventLoop() 842 | : EventBase::getMainLoop(false); 843 | $base && $base->loopBreak(); 844 | 845 | throw new Exception("Incorrect thread for working #$threadId"); 846 | // @codeCoverageIgnoreEnd 847 | } 848 | 849 | unset($this->waitingForJob[$threadId]); 850 | $this->working[$threadId] = $threadId; 851 | 852 | // @codeCoverageIgnoreStart 853 | $this->debug && $this->debug( 854 | "Thread #{$threadId} is marked as working" 855 | ); 856 | // @codeCoverageIgnoreEnd 857 | } 858 | 859 | /** 860 | * Marks thread as initializing 861 | * 862 | * @param int $threadId 863 | * 864 | * @internal 865 | */ 866 | public function markThreadInitializing($threadId) 867 | { 868 | $this->initializing[$threadId] = $threadId; 869 | 870 | // @codeCoverageIgnoreStart 871 | $this->debug && $this->debug( 872 | "Thread #{$threadId} is marked as initializing" 873 | ); 874 | // @codeCoverageIgnoreEnd 875 | } 876 | 877 | #endregion 878 | 879 | 880 | 881 | /** 882 | * Debug logging 883 | * 884 | * @param string $message 885 | */ 886 | protected function debug($message) 887 | { 888 | if (!$this->debug) { 889 | return; 890 | } 891 | 892 | $time = Base::getTimeForLog(); 893 | $poolId = $this->id; 894 | $poolName = $this->poolName; 895 | $pid = posix_getpid(); 896 | $message = "{$time} [debug] [P{$poolId}.{$poolName}] " 897 | ."#{$pid}:>