├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── BEST-PRACTICE-GUIDE.md ├── HOW-TO-INSTALL.md ├── LICENSE ├── README.md ├── autoload.php ├── composer.json ├── examples ├── asyncExample.php ├── calculatePi.php ├── calculatePiWithBc.php ├── closureWorkerExample.php ├── completeExample.php ├── performance.php ├── simpleExample.php └── synchronizedExample.php ├── phpunit.xml ├── src ├── ClosureWorker.php ├── NoSemaphore.php ├── ProcessDetails.php ├── ProcessDetailsCollection.php ├── Semaphore.php ├── SemaphoreException.php ├── SimpleSocket.php ├── SimpleSocketException.php ├── Worker.php ├── WorkerInterface.php ├── WorkerPool.php ├── WorkerPoolException.php ├── WorkerPoolExceptionResult.php └── WorkerPoolResult.php ├── tests ├── ClosureWorkerTest.php ├── Fixtures │ ├── FatalFailingWorker.php │ └── PingWorker.php └── WorkerPoolTest.php └── travisphp.ini /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [qxsch] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | /.phpunit.* 3 | /composer.phar 4 | /phpDocumentor.phar 5 | /phpunit 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 7.2 4 | - 7.3 5 | - 7.4 6 | - 8.0 7 | - 8.1 8 | - 8.2 9 | 10 | install: 11 | - composer require "phpunit/phpunit:8.*" 12 | #before_script: 13 | # - pecl install -f proctitle 14 | # - phpenv config-add travisphp.ini 15 | script: ./vendor/bin/phpunit 16 | 17 | -------------------------------------------------------------------------------- /BEST-PRACTICE-GUIDE.md: -------------------------------------------------------------------------------- 1 | Best Practices 2 | ============== 3 | 4 | Please ensure, that: 5 | 1. you do not have any open handles (DB connections, Files, ...) when: 6 | 1. executing the create method 7 | 1. you are not using objects in the child process, that are going to close shared resources (f.e. in the __destruct method) 8 | 1. your child processes create their own handles (DB connections, Files, ...) 9 | 1. you use the semaphore or other locking mechanisms, when accessing shared ressources 10 | 11 | **THE STRAIGHT TIP:** In case you need handles (f.e. DB connections) in your child processes, open them in the onProcessCreate method and close them in the onProcessDestroy method 12 | 13 | -------------------------------------------------------------------------------- /HOW-TO-INSTALL.md: -------------------------------------------------------------------------------- 1 | Install the required libs 2 | ```bash 3 | yum install php-process php-pcntl 4 | yum install php-pear php-devel 5 | ``` 6 | Finally install the WorkerPool using composer: 7 | ```bash 8 | ./composer.phar require "qxsch/worker-pool" '*' 9 | ``` 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | QXSCH WorkerPool runs tasks in a parallel processing workerpool 635 | Copyright (C) 2014 Marco Weber 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | QXSCH WorkerPool Copyright (C) 2014 QXSCH WorkerPool for PHP 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WorkerPool 2 | ========== 3 | 4 | [![Project Status](http://www.repostatus.org/badges/latest/active.svg)](http://www.repostatus.org/#active) 5 | 6 | 7 | [![Latest Stable Version](https://poser.pugx.org/qxsch/worker-pool/v/stable.png)](https://packagist.org/packages/qxsch/worker-pool) [![Total Downloads](https://poser.pugx.org/qxsch/worker-pool/downloads.png)](https://packagist.org/packages/qxsch/worker-pool) [![License](https://poser.pugx.org/qxsch/worker-pool/license.png)](https://packagist.org/packages/qxsch/worker-pool) 8 | 9 | **Parallel Processing WorkerPool for PHP** 10 | 11 | _10K Downloads within 4 Months, thank you very much! We're adding features as anyone requires them._ 12 | 13 | ## Examples 14 | 15 | 16 | The WorkerPool class provides a very simple interface to pass data to a worker pool and have it processed. 17 | You can at any time fetch the results from the workers. Each worker child can receive and return any value that can be [serialized][serialize]. 18 | 19 | ### A simple example 20 | 21 | ```php 22 | setWorkerPoolSize(4) 26 | ->create(new \QXS\WorkerPool\ClosureWorker( 27 | /** 28 | * @param mixed $input the input from the WorkerPool::run() Method 29 | * @param \QXS\WorkerPool\Semaphore $semaphore the semaphore to synchronize calls accross all workers 30 | * @param \ArrayObject $storage a persistent storage for the current child process 31 | */ 32 | function($input, $semaphore, $storage) { 33 | echo "[".getmypid()."]"." hi $input\n"; 34 | sleep(rand(1,3)); // this is the working load! 35 | return $input; // return null here, in case you do not want to pass any data to the parent 36 | } 37 | ) 38 | ); 39 | 40 | 41 | for($i=0; $i<10; $i++) { 42 | $wp->run($i); 43 | } 44 | 45 | $wp->waitForAllWorkers(); // wait for all workers 46 | 47 | foreach($wp as $val) { 48 | echo $val->dump() . "\n"; // dump the returned values 49 | // var_dump($val); // dump the returned values 50 | } 51 | 52 | ``` 53 | 54 | ### A more sophisticated example 55 | 56 | ```php 57 | sem=$semaphore; 77 | // write something to the stdout 78 | echo "\t[".getmypid()."] has been created.\n"; 79 | // initialize mt_rand 80 | list($usec, $sec) = explode(' ', microtime()); 81 | mt_srand((int)( (float) $sec + ((float) $usec * 100000) )); 82 | } 83 | /** 84 | * before the worker process is getting destroyed 85 | * 86 | * @throws \Exception in case of a processing Error an Exception will be thrown 87 | */ 88 | public function onProcessDestroy() { 89 | // write something to the stdout 90 | echo "\t[".getmypid()."] will be destroyed.\n"; 91 | } 92 | /** 93 | * run the work 94 | * 95 | * @param Serializeable $input the data, that the worker should process 96 | * @return Serializeable Returns the result 97 | * @throws \Exception in case of a processing Error an Exception will be thrown 98 | */ 99 | public function run($input) { 100 | $input=(string)$input; 101 | echo "\t[".getmypid()."] Hi $input\n"; 102 | sleep(mt_rand(0,10)); // this is the workload! 103 | // and sometimes exceptions might occur 104 | if(mt_rand(0,10)==9) { 105 | throw new \RuntimeException('We have a problem for '.$input.'.'); 106 | } 107 | return "Hi $input"; // return null here, in case you do not want to pass any data to the parent 108 | } 109 | } 110 | 111 | 112 | $wp=new WorkerPool(); 113 | $wp->setWorkerPoolSize(10) 114 | ->create(new MyWorker()); 115 | 116 | // produce some tasks 117 | for($i=1; $i<=50; $i++) { 118 | $wp->run($i); 119 | } 120 | 121 | // some statistics 122 | echo "Busy Workers:".$wp->getBusyWorkers()." Free Workers:".$wp->getFreeWorkers()."\n"; 123 | 124 | // wait for completion of all tasks 125 | $wp->waitForAllWorkers(); 126 | 127 | // collect all the results 128 | foreach($wp as $val) { 129 | if(isset($val['data'])) { 130 | echo "RESULT: ".$val['data']."\n"; 131 | } 132 | elseif(isset($val['workerException'])) { 133 | echo "WORKER EXCEPTION: ".$val['workerException']['class'].": ".$val['workerException']['message']."\n".$val['workerException']['trace']."\n"; 134 | } 135 | elseif(isset($val['poolException'])) { 136 | echo "POOL EXCEPTION: ".$val['poolException']['class'].": ".$val['poolException']['message']."\n".$val['poolException']['trace']."\n"; 137 | } 138 | } 139 | 140 | 141 | // write something, before the parent exits 142 | echo "ByeBye\n"; 143 | 144 | ``` 145 | 146 | ### Synchronize your workers 147 | In case you need to access shared ressources, you can synchronize your workers. 148 | 149 | ```php 150 | setWorkerPoolSize(4) 154 | ->create(new \QXS\WorkerPool\ClosureWorker( 155 | /** 156 | * @param mixed $input the input from the WorkerPool::run() Method 157 | * @param \QXS\WorkerPool\Semaphore $semaphore the semaphore to synchronize calls accross all workers 158 | * @param \ArrayObject $storage a persistent storge for the current child process 159 | */ 160 | function($input, $semaphore, $storage) { 161 | $semaphore->synchronizedBegin(); 162 | try { 163 | // this code is being synchronized accross all workers 164 | // so here we have just one worker at a time 165 | echo "[A][".getmypid()."]"." hi $input\n"; 166 | } 167 | finally { 168 | $semaphore->synchronizedEnd(); 169 | } 170 | 171 | // alternative example 172 | $semaphore->synchronize(function() use ($input, $storage) { 173 | // this code is being synchronized accross all workers 174 | // so here we have just one worker at a time 175 | echo "[B][".getmypid()."]"." hi $input\n"; 176 | }); 177 | sleep(rand(1,3)); // this is the working load! 178 | return $input; 179 | } 180 | ) 181 | ); 182 | 183 | 184 | for($i=0; $i<10; $i++) { 185 | $wp->run($i); 186 | } 187 | 188 | $wp->waitForAllWorkers(); // wait for all workers 189 | 190 | foreach($wp as $val) { 191 | var_dump($val); // dump the returned values 192 | } 193 | ``` 194 | 195 | ### Disable semaphore (ability to synchronize workers) 196 | 197 | You can disable the semaphore. Some people complained about opening semaphores, that they do not need at all. 198 | 199 | ```php 200 | setWorkerPoolSize(4) 204 | ->disableSemaphore() // <--- this disables the semaphore support (you can still use it in the worker, but it will have no effect) 205 | ->create(new \QXS\WorkerPool\ClosureWorker( 206 | /** 207 | * @param mixed $input the input from the WorkerPool::run() Method 208 | * @param \QXS\WorkerPool\Semaphore $semaphore the semaphore to synchronize calls accross all workers 209 | * @param \ArrayObject $storage a persistent storage for the current child process 210 | */ 211 | function($input, $semaphore, $storage) { 212 | echo "[".getmypid()."]"." hi $input\n"; 213 | sleep(rand(1,3)); // this is the working load! 214 | return $input; // return null here, in case you do not want to pass any data to the parent 215 | } 216 | ) 217 | ); 218 | 219 | 220 | for($i=0; $i<10; $i++) { 221 | $wp->run($i); 222 | } 223 | 224 | $wp->waitForAllWorkers(); // wait for all workers 225 | 226 | foreach($wp as $val) { 227 | var_dump($val); // dump the returned values 228 | } 229 | 230 | ``` 231 | 232 | 233 | ### Automatic respawn 234 | 235 | You can choose to automatically respawn dead workers. 236 | 237 | ```php 238 | setWorkerPoolSize(4) 242 | ->respawnAutomatically() 243 | ->create(new \QXS\WorkerPool\ClosureWorker( 244 | /** 245 | * @param mixed $input the input from the WorkerPool::run() Method 246 | * @param \QXS\WorkerPool\Semaphore $semaphore the semaphore to synchronize calls accross all workers 247 | * @param \ArrayObject $storage a persistent storage for the current child process 248 | */ 249 | function($input, $semaphore, $storage) { 250 | echo "[".getmypid()."]"." hi $input\n"; 251 | sleep(rand(1,3)); // this is the working load! 252 | 253 | // Simulate unexpected worker death 254 | if (rand(1, 10) > 5) exit; 255 | 256 | return $input; // return null here, in case you do not want to pass any data to the parent 257 | } 258 | ) 259 | ); 260 | 261 | 262 | for($i=0; $i<10; $i++) { 263 | $wp->run($i); 264 | } 265 | 266 | $wp->waitForAllWorkers(); // wait for all workers 267 | 268 | foreach($wp as $val) { 269 | var_dump($val); // dump the returned values 270 | } 271 | 272 | ``` 273 | 274 | Each time a worker dies, a new one will be created with an incremented index. 275 | 276 | You should avoid the situation where a worker dies but the respawn capability can be a useful workaround until you fix the situation. 277 | 278 | 279 | ### Transparent output to ps 280 | 281 | See what's happening when running a PS: 282 | 283 | ``` 284 | root 2378 \_ simpleExample.php: Parent 285 | root 2379 \_ simpleExample.php: Worker 1 of QXS\WorkerPool\ClosureWorker [busy] 286 | root 2380 \_ simpleExample.php: Worker 2 of QXS\WorkerPool\ClosureWorker [busy] 287 | root 2381 \_ simpleExample.php: Worker 3 of QXS\WorkerPool\ClosureWorker [free] 288 | root 2382 \_ simpleExample.php: Worker 4 of QXS\WorkerPool\ClosureWorker [free] 289 | ``` 290 | 291 | ### Documentation 292 | 293 | The documentation can be found here http://qxsch.github.io/WorkerPool/doc/ 294 | 295 | [serialize]: http://php.net/serialize 296 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | 'src', 6 | 'QXS\Tests\WorkerPool' => 'tests' 7 | ); 8 | foreach ($pathMap as $namespace => $directory) { 9 | $strLen = strlen($namespace); 10 | if ($strLen <= strlen($className) && substr($className, 0, $strLen) === $namespace) { 11 | $path = __DIR__ . DIRECTORY_SEPARATOR . $directory . substr($className, $strLen) . '.php'; 12 | $path = str_replace('\\', DIRECTORY_SEPARATOR, $path); 13 | require_once($path); 14 | return TRUE; 15 | } 16 | } 17 | return FALSE; 18 | }); 19 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qxsch/worker-pool", 3 | "description": "Runs tasks in a parallel processing workerpool.", 4 | "keywords": ["fork", "parallel", "worker", "workerpool", "task"], 5 | "homepage": "https://github.com/qxsch/WorkerPool", 6 | "type": "library", 7 | "license": "GPL-3.0+", 8 | "authors": [ 9 | { 10 | "name": "Marco Weber", 11 | "email": "marco.weber@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=7.1.0", 16 | "ext-pcntl": "*", 17 | "ext-posix": "*", 18 | "ext-sysvsem": "*", 19 | "ext-sockets": "*" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": ">=4.0.0" 23 | }, 24 | "suggest": { 25 | "php": ">=8.0.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "QXS\\WorkerPool\\": "src/", 30 | "QXS\\Tests\\WorkerPool\\": "tests/" 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /examples/asyncExample.php: -------------------------------------------------------------------------------- 1 | setWorkerPoolSize(4) 8 | ->create(new \QXS\WorkerPool\ClosureWorker( 9 | /** 10 | * @param mixed $input the input from the WorkerPool::run() Method 11 | * @param \QXS\WorkerPool\Semaphore $semaphore the semaphore to synchronize calls accross all workers 12 | * @param \ArrayObject $storage a persistent storge for the current child process 13 | */ 14 | function($input, $semaphore, $storage) { 15 | sleep(rand(1,3)); // this is the working load! 16 | return $input; 17 | } 18 | ) 19 | ); 20 | 21 | $i=20; 22 | while($i<40) { 23 | // is there a free worker? 24 | if($wp->getFreeWorkers()>0) { 25 | $wp->run($i); 26 | $i++; 27 | } 28 | else { 29 | // poll some data - get free workers could be removed to priorize result polling in favor of result submission 30 | while($wp->hasResults() && $wp->getFreeWorkers()==0) { 31 | $val=$wp->getNextResult(); 32 | echo "Received: ".$val['data']." from pid ".$val['pid']."\n"; 33 | } 34 | $wp->waitForOneFreeWorker(); 35 | //// as an alternative you can manually wait for free workers? 36 | //if($wp->getFreeWorkers()==0) { 37 | // usleep(1000); // and sleep a bit 38 | //} 39 | } 40 | } 41 | 42 | // bug fix - run get busy workers before has results (worker can finish between the calls and the next call will deliver results) 43 | while($wp->getBusyWorkers()>0 || $wp->hasResults()) { 44 | // poll some data 45 | foreach($wp as $val) { 46 | echo "Received: ".$val['data']." from pid ".$val['pid']."\n"; 47 | } 48 | usleep(1000); 49 | } 50 | 51 | 52 | -------------------------------------------------------------------------------- /examples/calculatePi.php: -------------------------------------------------------------------------------- 1 | num_steps = (int) $num_steps; 24 | $this->step = (float) $step; 25 | 26 | } 27 | 28 | /** 29 | * After the worker has been forked into another process 30 | * 31 | * @param \QXS\WorkerPool\Semaphore $semaphore the semaphore to run synchronized tasks 32 | * @throws \Exception in case of a processing Error an Exception will be thrown 33 | */ 34 | public function onProcessCreate(Semaphore $semaphore) { } 35 | 36 | /** 37 | * Before the worker process is getting destroyed 38 | * 39 | * @throws \Exception in case of a processing Error an Exception will be thrown 40 | */ 41 | public function onProcessDestroy() { } 42 | 43 | /** 44 | * run the work 45 | * 46 | * @param \Serializable $input the data, that the worker should process 47 | * @return \Serializable Returns the result 48 | * @throws \Exception in case of a processing Error an Exception will be thrown 49 | */ 50 | public function run($input) { 51 | list($part_number, $part_step) = $input; 52 | $x=0.0; 53 | $sum=0.0; 54 | for ($i = $part_number; $i < $this->num_steps; $i += $part_step) { 55 | $x = ($i + 0.5) * $this->step; 56 | $sum += 4.0 / (1.0 + $x * $x); 57 | } 58 | return $sum; 59 | } 60 | 61 | } 62 | 63 | 64 | 65 | 66 | $num_steps = 1000000; // setps - sets the calculation detail 67 | $part_step = 8; // parallelization degree set to number of cores 68 | 69 | $step = 1.0 / $num_steps; 70 | 71 | $wp=new WorkerPool(); 72 | $wp->setWorkerPoolSize($part_step) 73 | ->create(new PiWorker($num_steps, $step)); 74 | 75 | for($i = 0; $i < $part_step; $i++) { 76 | $wp->run(array($i, $part_step)); 77 | } 78 | 79 | 80 | $wp->waitForAllWorkers(); // wait for all workers 81 | 82 | $sum=0; 83 | foreach($wp as $result) { 84 | $sum+=$result['data']; 85 | } 86 | 87 | $pi = $step * $sum; 88 | 89 | echo "PI is $pi\n"; 90 | 91 | 92 | -------------------------------------------------------------------------------- /examples/calculatePiWithBc.php: -------------------------------------------------------------------------------- 1 | num_steps = (int) $num_steps; 24 | $this->step = (string) $step; 25 | 26 | } 27 | 28 | /** 29 | * After the worker has been forked into another process 30 | * 31 | * @param \QXS\WorkerPool\Semaphore $semaphore the semaphore to run synchronized tasks 32 | * @throws \Exception in case of a processing Error an Exception will be thrown 33 | */ 34 | public function onProcessCreate(Semaphore $semaphore) { } 35 | 36 | /** 37 | * Before the worker process is getting destroyed 38 | * 39 | * @throws \Exception in case of a processing Error an Exception will be thrown 40 | */ 41 | public function onProcessDestroy() { } 42 | 43 | /** 44 | * run the work 45 | * 46 | * @param \Serializable $input the data, that the worker should process 47 | * @return \Serializable Returns the result 48 | * @throws \Exception in case of a processing Error an Exception will be thrown 49 | */ 50 | public function run($input) { 51 | list($part_number, $part_step) = $input; 52 | $sum='0'; 53 | for ($i = $part_number; $i < $this->num_steps; $i += $part_step) { 54 | //$x = ($i + 0.5) * $this->step; 55 | //$sum += 4.0 / (1.0 + $x * $x); 56 | $x = bcmul(bcadd($i, '0.5'), $this->step); 57 | $sum = bcadd($sum, bcdiv('4.0', bcadd('1.0', bcmul($x, $x)))); 58 | } 59 | return $sum; 60 | } 61 | 62 | } 63 | 64 | 65 | 66 | bcscale(100); // sets the scale 67 | $num_steps = 1000000; // setps - sets the calculation detail 68 | $part_step = 8; // parallelization degree set to number of cores 69 | 70 | $step = bcdiv('1.0', (string)$num_steps); 71 | 72 | $wp=new WorkerPool(); 73 | $wp->setWorkerPoolSize($part_step) 74 | ->create(new PiWorker($num_steps, $step)); 75 | 76 | for($i = 0; $i < $part_step; $i++) { 77 | $wp->run(array($i, $part_step)); 78 | } 79 | 80 | 81 | $wp->waitForAllWorkers(); // wait for all workers 82 | 83 | $sum='0'; 84 | foreach($wp as $result) { 85 | $sum=bcadd($sum, $result['data']); 86 | } 87 | 88 | $pi = bcmul($step, $sum); 89 | 90 | echo "PI is $pi\n"; 91 | 92 | 93 | -------------------------------------------------------------------------------- /examples/closureWorkerExample.php: -------------------------------------------------------------------------------- 1 | setWorkerPoolSize(4) 8 | ->create(new \QXS\WorkerPool\ClosureWorker( 9 | /** 10 | * The Worker::run() Method 11 | * @param mixed $input the input from the WorkerPool::run() Method 12 | * @param \QXS\WorkerPool\Semaphore $semaphore the semaphore to synchronize calls accross all workers 13 | * @param \ArrayObject $storage a persistent storge for the current child process 14 | */ 15 | function($input, $semaphore, $storage) { 16 | $storage->append($input); 17 | echo "[".getmypid()."]"." hi $input\n"; 18 | sleep(rand(1,3)); // this is the working load! 19 | return $input; 20 | }, 21 | /** 22 | * The Worker::onProcessCreate() Method 23 | * @param \QXS\WorkerPool\Semaphore $semaphore the semaphore to synchronize calls accross all workers 24 | * @param \ArrayObject $storage a persistent storge for the current child process 25 | */ 26 | function($semaphore, $storage) { 27 | echo "[".getmypid()."]"." child has been created\n"; 28 | }, 29 | /** 30 | * The Worker::onProcessDestroy() Method 31 | * @param \QXS\WorkerPool\Semaphore $semaphore the semaphore to synchronize calls accross all workers 32 | * @param \ArrayObject $storage a persistent storge for the current child process 33 | */ 34 | function($semaphore, $storage) { 35 | $semaphore->synchronizedBegin(); 36 | try { 37 | echo "[".getmypid()."]"." child will be destroyed, see its history\n"; 38 | foreach($storage as $val) { 39 | echo "\t$val\n"; 40 | } 41 | } 42 | finally { 43 | $semaphore->synchronizedEnd(); 44 | } 45 | } 46 | 47 | ) 48 | ); 49 | 50 | 51 | for($i=0; $i<10; $i++) { 52 | $wp->run($i); 53 | } 54 | 55 | $wp->waitForAllWorkers(); // wait for all workers 56 | 57 | foreach($wp as $val) { 58 | echo "Parent has retrieved: ".$val['data']."\n"; 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /examples/completeExample.php: -------------------------------------------------------------------------------- 1 | sem=$semaphore; 25 | // write something to the stdout 26 | echo "\t[".getmypid()."] has been created.\n"; 27 | // initialize mt_rand 28 | list($usec, $sec) = explode(' ', microtime()); 29 | mt_srand((int)( (float) $sec + ((float) $usec * 100000) )); 30 | } 31 | /** 32 | * before the worker process is getting destroyed 33 | * 34 | * @throws \Exception in case of a processing Error an Exception will be thrown 35 | */ 36 | public function onProcessDestroy() { 37 | // write something to the stdout 38 | echo "\t[".getmypid()."] will be destroyed.\n"; 39 | } 40 | /** 41 | * run the work 42 | * 43 | * @param Serializeable $input the data, that the worker should process 44 | * @return Serializeable Returns the result 45 | * @throws \Exception in case of a processing Error an Exception will be thrown 46 | */ 47 | public function run($input) { 48 | $input=(string)$input; 49 | echo "\t[".getmypid()."] Hi $input\n"; 50 | sleep(mt_rand(0,10)); // this is the workload! 51 | // and sometimes exceptions might occur 52 | if(mt_rand(0,10)==9) { 53 | throw new \RuntimeException('We have a problem for '.$input.'.'); 54 | } 55 | return "Hi $input"; // return null here, in case you do not want to pass any data to the parent 56 | } 57 | } 58 | 59 | 60 | $wp=new WorkerPool(); 61 | $wp->setWorkerPoolSize(10) 62 | ->create(new MyWorker()); 63 | 64 | // produce some tasks 65 | for($i=1; $i<=50; $i++) { 66 | $wp->run($i); 67 | } 68 | 69 | // some statistics 70 | echo "Busy Workers:".$wp->getBusyWorkers()." Free Workers:".$wp->getFreeWorkers()."\n"; 71 | 72 | // wait for completion of all tasks 73 | $wp->waitForAllWorkers(); 74 | 75 | // collect all the results 76 | foreach($wp as $val) { 77 | if(isset($val['data'])) { 78 | echo "RESULT: ".$val['data']."\n"; 79 | } 80 | elseif(isset($val['workerException'])) { 81 | echo "WORKER EXCEPTION: ".$val['workerException']['class'].": ".$val['workerException']['message']."\n".$val['workerException']['trace']."\n"; 82 | } 83 | elseif(isset($val['poolException'])) { 84 | echo "POOL EXCEPTION: ".$val['poolException']['class'].": ".$val['poolException']['message']."\n".$val['poolException']['trace']."\n"; 85 | } 86 | } 87 | 88 | 89 | // write something, before the parent exits 90 | echo "ByeBye\n"; 91 | -------------------------------------------------------------------------------- /examples/performance.php: -------------------------------------------------------------------------------- 1 | setWorkerPoolSize(100) 12 | ->create(new \QXS\WorkerPool\ClosureWorker( 13 | /** 14 | * @param mixed $input the input from the WorkerPool::run() Method 15 | * @param \QXS\WorkerPool\Semaphore $semaphore the semaphore to synchronize calls accross all workers 16 | * @param \ArrayObject $storage a persistent storge for the current child process 17 | */ 18 | function ($input, $semaphore, $storage) { 19 | usleep(rand(1000000,2000000)); // this is the working load! 20 | return NULL; 21 | } 22 | ) 23 | ); 24 | 25 | for ($i = 0; $i < 500; $i++) { 26 | $wp->run($i); 27 | } 28 | 29 | $wp->waitForAllWorkers(); // wait for all workers 30 | 31 | 32 | $timeused=microtime(true)-$timeused; 33 | echo "\t- With the workerpool it took: ".number_format($timeused, 2)." seconds\n"; 34 | echo "\t- In this example the workerpool is ". number_format(750/$timeused, 2) ." times faster to the avg!\n"; 35 | echo "\t- BTW: This is a simulation of a real world example, where we were waiting for remote results. This initiated the development of the workerpool.\n"; 36 | -------------------------------------------------------------------------------- /examples/simpleExample.php: -------------------------------------------------------------------------------- 1 | setWorkerPoolSize(4) 8 | ->create(new \QXS\WorkerPool\ClosureWorker( 9 | /** 10 | * @param mixed $input the input from the WorkerPool::run() Method 11 | * @param \QXS\WorkerPool\Semaphore $semaphore the semaphore to synchronize calls accross all workers 12 | * @param \ArrayObject $storage a persistent storge for the current child process 13 | */ 14 | function($input, $semaphore, $storage) { 15 | echo "[".getmypid()."]"." hi $input\n"; 16 | sleep(rand(1,3)); // this is the working load! 17 | if(rand(1, 4) === 2) { 18 | throw new \Exception("ohje"); 19 | } 20 | return $input; 21 | } 22 | ) 23 | ); 24 | 25 | 26 | for($i=0; $i<10; $i++) { 27 | $wp->run($i); 28 | } 29 | 30 | $wp->waitForAllWorkers(); // wait for all workers 31 | 32 | foreach($wp as $val) { 33 | echo $val->dump() . "\n"; 34 | //var_dump($val); // you can also dump the returned values 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/synchronizedExample.php: -------------------------------------------------------------------------------- 1 | setWorkerPoolSize(4) 8 | ->create(new \QXS\WorkerPool\ClosureWorker( 9 | /** 10 | * @param mixed $input the input from the WorkerPool::run() Method 11 | * @param \QXS\WorkerPool\Semaphore $semaphore the semaphore to synchronize calls accross all workers 12 | * @param \ArrayObject $storage a persistent storge for the current child process 13 | */ 14 | function($input, $semaphore, $storage) { 15 | // synchronized begin and end (Always use try finally to make sure, that lock will be cleaned up!) 16 | $semaphore->synchronizedBegin(); 17 | try { 18 | // this code is being synchronized accross all workers 19 | echo "[A][".getmypid()."]"." hi $input\n"; 20 | } 21 | finally { 22 | $semaphore->synchronizedEnd(); 23 | } 24 | 25 | 26 | // alternative example 27 | $semaphore->synchronize(function() use ($input, $storage) { 28 | // this code is being synchronized accross all workers 29 | echo "[B][".getmypid()."]"." hi $input\n"; 30 | }); 31 | sleep(rand(1,3)); // this is the working load! 32 | return $input; 33 | } 34 | ) 35 | ); 36 | 37 | 38 | for($i=0; $i<10; $i++) { 39 | $wp->run($i); 40 | } 41 | 42 | $wp->waitForAllWorkers(); // wait for all workers 43 | 44 | foreach($wp as $val) { 45 | echo $val->dump() . "\n"; 46 | //var_dump($val); // you can also dump the returned values 47 | } 48 | 49 | 50 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 23 | ./tests/ 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/ClosureWorker.php: -------------------------------------------------------------------------------- 1 | storage = new \ArrayObject(); 36 | if(is_null($create)) { 37 | $create=function($semaphore, $storage) { }; 38 | } 39 | if(is_null($destroy)) { 40 | $destroy=function($semaphore, $storage) { }; 41 | } 42 | $this->create = $create; 43 | $this->run = $run; 44 | $this->destroy = $destroy; 45 | } 46 | 47 | /** 48 | * After the worker has been forked into another process 49 | * 50 | * @param \QXS\WorkerPool\Semaphore $semaphore the semaphore to run synchronized tasks 51 | * @throws \Exception in case of a processing Error an Exception will be thrown 52 | */ 53 | public function onProcessCreate(Semaphore $semaphore) { 54 | $this->semaphore = $semaphore; 55 | $this->create->__invoke($this->semaphore, $this->storage); 56 | } 57 | 58 | /** 59 | * Before the worker process is getting destroyed 60 | * 61 | * @throws \Exception in case of a processing Error an Exception will be thrown 62 | */ 63 | public function onProcessDestroy() { 64 | $this->destroy->__invoke($this->semaphore, $this->storage); 65 | } 66 | 67 | /** 68 | * run the work 69 | * 70 | * @param \Serializable $input the data, that the worker should process 71 | * @return \Serializable Returns the result 72 | * @throws \Exception in case of a processing Error an Exception will be thrown 73 | */ 74 | public function run($input) { 75 | return $this->run->__invoke($input, $this->semaphore, $this->storage); 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/NoSemaphore.php: -------------------------------------------------------------------------------- 1 | semKey; 22 | } 23 | 24 | /** 25 | * Create a semaphore 26 | * @param string|int $semKey the key of the semaphore - use a specific number or Semaphore::SEM_RAND_KEY or Semaphore::SEM_FTOK_KEY 27 | * @param int $maxAcquire the maximum number of processes, that can acquire the semaphore 28 | * @param int $perms the unix permissions for (user,group,others) - valid range from 0 to 0777 29 | * @throws SemaphoreException 30 | * @return \QXS\WorkerPool\Semaphore the current object 31 | */ 32 | public function create($semKey = Semaphore::SEM_FTOK_KEY, int $maxAcquire = 1, int $perms=0666) : Semaphore { 33 | $this->semaphore = NULL; 34 | $this->semKey = 1; 35 | 36 | return $this; 37 | } 38 | 39 | /** 40 | * Acquire the semaphore 41 | * @throws SemaphoreException in case of an error 42 | * @return \QXS\WorkerPool\Semaphore the current object 43 | */ 44 | public function acquire() : Semaphore { 45 | return $this; 46 | } 47 | 48 | /** 49 | * Releases the semaphore 50 | * @throws SemaphoreException in case of an error 51 | * @return \QXS\WorkerPool\Semaphore the current object 52 | */ 53 | public function release() : Semaphore { 54 | return $this; 55 | } 56 | 57 | /** 58 | * Has the semaphore been created? 59 | * @return bool true in case the semaphore has been created 60 | */ 61 | public function isCreated() : bool { 62 | return $this->semKey!==NULL; 63 | } 64 | 65 | /** 66 | * Destroys the semaphore 67 | * @throws SemaphoreException in case of an error 68 | * @return \QXS\WorkerPool\Semaphore the current object 69 | */ 70 | public function destroy() : Semaphore { 71 | $this->semaphore = NULL; 72 | $this->semKey = NULL; 73 | return $this; 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/ProcessDetails.php: -------------------------------------------------------------------------------- 1 | pid = $pid; 26 | $this->socket = $socket; 27 | $this->socket->annotation['pid'] = $pid; 28 | } 29 | 30 | /** 31 | * Sanitizes the process title format string 32 | * @param string $string the process title 33 | * @return string the process sanitized title 34 | * @throws \DomainException in case the $string value is not within the permitted range 35 | */ 36 | public static function sanitizeProcessTitleFormat(string $string) : string { 37 | $string = preg_replace( 38 | '/[^a-z0-9-_.:% \\\\\\]\\[]/i', 39 | '', 40 | $string 41 | ); 42 | $string = trim($string); 43 | return $string; 44 | } 45 | 46 | /** 47 | * Sets the proccess title 48 | * 49 | * This function call requires php5.5+ or the proctitle extension! 50 | * Empty title strings won't be set. 51 | * @param string $title the new process title 52 | * @param array $replacements an associative array of replacment values 53 | * @return void 54 | */ 55 | public static function setProcessTitle(string $title, array $replacements = array()) : void { 56 | // skip when empty title names or running on MacOS 57 | if (trim($title) == '' || PHP_OS == 'Darwin') { 58 | return; 59 | } 60 | // 1. replace the values 61 | $title = preg_replace_callback( 62 | '/\%([a-z0-9]+)\%/i', 63 | function ($match) use ($replacements) { 64 | if (isset($replacements[$match[1]])) { 65 | return $replacements[$match[1]]; 66 | } 67 | return $match[0]; 68 | }, 69 | $title 70 | ); 71 | // 2. remove forbidden chars 72 | $title = preg_replace( 73 | '/[^a-z0-9-_.: \\\\\\]\\[]/i', 74 | '', 75 | $title 76 | ); 77 | // 3. set the title 78 | if (function_exists('cli_set_process_title')) { 79 | cli_set_process_title($title); // PHP 5.5+ has a builtin function 80 | } 81 | } 82 | 83 | /** 84 | * Get the pid 85 | * @return int 86 | */ 87 | public function getPid() : int { 88 | return $this->pid; 89 | } 90 | 91 | /** 92 | * Get the socket 93 | * @return \QXS\WorkerPool\SimpleSocket 94 | */ 95 | public function getSocket() : SimpleSocket { 96 | return $this->socket; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/ProcessDetailsCollection.php: -------------------------------------------------------------------------------- 1 | getPid(); 30 | $this->processDetails[$pid] = $processDetails; 31 | $this->sockets[$pid] = $processDetails->getSocket(); 32 | $this->registerFreeProcess($processDetails); 33 | return $this; 34 | } 35 | 36 | /** 37 | * Removes the ProcessDetails from the list. 38 | * 39 | * @param ProcessDetails $processDetails 40 | * @throws \InvalidArgumentException 41 | * @return ProcessDetailsCollection 42 | */ 43 | public function remove(ProcessDetails $processDetails) : ProcessDetailsCollection { 44 | $pid = $processDetails->getPid(); 45 | 46 | if ($this->hasProcess($pid) === FALSE) { 47 | throw new \InvalidArgumentException(sprintf('Could not remove process. Process (%d) not in list.', $processDetails->getPid()), 1400761297); 48 | } 49 | 50 | if (isset($this->freeProcessIds[$pid])) { 51 | unset($this->freeProcessIds[$pid]); 52 | } 53 | 54 | if (isset($this->sockets[$pid])) { 55 | unset($this->sockets[$pid]); 56 | } 57 | 58 | unset($this->processDetails[$pid]); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Sends the kill signal to all processes and removes them from the list. 65 | * 66 | * @return void 67 | */ 68 | public function killAllProcesses() : void { 69 | foreach ($this->processDetails as $pid => $processDetails) { 70 | $this->remove($processDetails); 71 | posix_kill($pid, SIGKILL); 72 | } 73 | } 74 | 75 | /** 76 | * Register a ProcessDetails as free 77 | * 78 | * @param ProcessDetails $processDetails 79 | * @throws \InvalidArgumentException 80 | * @return ProcessDetailsCollection 81 | */ 82 | public function registerFreeProcess(ProcessDetails $processDetails) : ProcessDetailsCollection { 83 | $pid = $processDetails->getPid(); 84 | if ($this->hasProcess($pid) === FALSE) { 85 | throw new \InvalidArgumentException(sprintf('Could not register free process. Process (%d) not in list.', $processDetails->getPid()), 1400761296); 86 | } 87 | $this->freeProcessIds[$pid] = $pid; 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Register the ProcessDetails with the given PID as free. 94 | * 95 | * @param int $pid 96 | * @return ProcessDetailsCollection 97 | */ 98 | public function registerFreeProcessId(int $pid) : ProcessDetailsCollection { 99 | $processDetails = $this->getProcessDetails($pid); 100 | if ($processDetails !== NULL) { 101 | $this->registerFreeProcess($processDetails); 102 | } 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Get all ProcessDetails by reference 109 | * 110 | * @return ProcessDetails[] 111 | */ 112 | public function &getAllProcesssDetails() { 113 | return $this->processDetails; 114 | } 115 | 116 | /** 117 | * Takes one ProcessDetails from the list of free ProcessDetails. Returns NULL if no free process is available. 118 | * 119 | * @return ProcessDetails 120 | */ 121 | public function takeFreeProcess() : ?ProcessDetails { 122 | if ($this->getFreeProcessesCount() === 0) { 123 | return NULL; 124 | } 125 | $freePid = array_shift($this->freeProcessIds); 126 | if ($freePid === NULL) { 127 | return NULL; 128 | } 129 | return $this->getProcessDetails($freePid); 130 | } 131 | 132 | /** 133 | * Checks if the ProcessDetails with given PID is in the list. 134 | * 135 | * @param int $pid 136 | * @return bool 137 | */ 138 | public function hasProcess(int $pid) : bool { 139 | return isset($this->processDetails[$pid]); 140 | } 141 | 142 | /** 143 | * Get the count of free processes 144 | * 145 | * @return int 146 | */ 147 | public function getFreeProcessesCount() : int { 148 | return count($this->freeProcessIds); 149 | } 150 | 151 | /** 152 | * Get the count of processes 153 | * 154 | * @return int 155 | */ 156 | public function getProcessesCount() : int { 157 | return count($this->processDetails); 158 | } 159 | 160 | /** 161 | * Returns the associated sockets of all processes in the list. 162 | * 163 | * @return \QXS\WorkerPool\SimpleSocket[] 164 | */ 165 | public function &getSockets() { 166 | return $this->sockets; 167 | } 168 | 169 | /** 170 | * Get a ProcessDetails of the given PID 171 | * 172 | * @param int $pid 173 | * @return ProcessDetails 174 | */ 175 | public function getProcessDetails($pid) : ?ProcessDetails { 176 | if ($this->hasProcess($pid) === FALSE) { 177 | return NULL; 178 | } 179 | 180 | return $this->processDetails[$pid]; 181 | } 182 | 183 | /** 184 | * @inheritdoc 185 | */ 186 | public function getIterator() : \ArrayIterator { 187 | return new \ArrayIterator($this->processDetails); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Semaphore.php: -------------------------------------------------------------------------------- 1 | 12 | * $t=new Semaphore(); 13 | * $t->create(Semaphore::SEM_FTOK_KEY); 14 | * // acquire && release 15 | * $t->acquire(); 16 | * echo "We are in the sem\n"; 17 | * $t->release(); 18 | * // acquire && release (aliases) 19 | * $t->synchronizedBegin(); 20 | * echo "We are in the sem\n"; 21 | * $t->synchronizedEnd(); 22 | * 23 | * $t->destroy(); 24 | * 25 | */ 26 | class Semaphore { 27 | 28 | /** generate a random key */ 29 | const SEM_RAND_KEY = 'rand'; 30 | 31 | /** generate a key based on ftok */ 32 | const SEM_FTOK_KEY = 'ftok'; 33 | 34 | /** minimum semaphore int */ 35 | const SEM_MIN_INT=-2147483647; 36 | 37 | /** maximum semaphore int */ 38 | const SEM_MAX_INT=2147483647; 39 | 40 | /** @var resource|\SysvSemaphore the semaphore resource */ 41 | protected $semaphore = NULL; 42 | 43 | /** @var int the key that is used to access the semaphore */ 44 | protected $semKey = NULL; 45 | 46 | /** 47 | * Returns the key, that can be used to access the semaphore 48 | * @return null|int the key of the semaphore 49 | */ 50 | public function getSemaphoreKey() { 51 | return $this->semKey; 52 | } 53 | 54 | /** 55 | * Create a semaphore 56 | * @param string|int $semKey the key of the semaphore - use a specific number or Semaphore::SEM_RAND_KEY or Semaphore::SEM_FTOK_KEY 57 | * @param int $maxAcquire the maximum number of processes, that can acquire the semaphore 58 | * @param int $perms the unix permissions for (user,group,others) - valid range from 0 to 0777 59 | * @throws SemaphoreException 60 | * @return \QXS\WorkerPool\Semaphore the current object 61 | */ 62 | public function create($semKey = Semaphore::SEM_FTOK_KEY, int $maxAcquire = 1, int $perms=0666) : Semaphore { 63 | if ($this->isCreated()) { 64 | throw new SemaphoreException('Semaphore has already been created.'); 65 | } 66 | 67 | if (!is_int($maxAcquire)) { 68 | $maxAcquire = 1; 69 | } 70 | $perms=(int)$perms; 71 | if ($perms < 0 || $perms > 0777) { 72 | $perms = 0666; 73 | } 74 | 75 | // randomly generate semaphore, without collision 76 | if ($semKey == Semaphore::SEM_RAND_KEY) { 77 | $retries = 5; 78 | mt_srand((int)(microtime(true)*10000)); 79 | } else { 80 | $retries = 1; 81 | } 82 | // try to generate a semaphore 83 | while (!$this->isCreated() && $retries > 0) { 84 | $retries--; 85 | // generate a semKey 86 | if (!is_int($semKey)) { 87 | if ($semKey == Semaphore::SEM_RAND_KEY) { 88 | $this->semKey = mt_rand(Semaphore::SEM_MIN_INT, Semaphore::SEM_MAX_INT); 89 | } else { 90 | $this->semKey = ftok(__FILE__, 's'); 91 | } 92 | } else { 93 | $this->semKey = $semKey; 94 | } 95 | // check the range 96 | if($this->semKey < Semaphore::SEM_MIN_INT || $this->semKey > Semaphore::SEM_MAX_INT) { 97 | $this->semKey = ftok(__FILE__, 's'); 98 | } 99 | $this->semaphore = sem_get($this->semKey, $maxAcquire, $perms, 0); 100 | } 101 | if (!$this->isCreated()) { 102 | $this->semaphore = NULL; 103 | $this->semKey = NULL; 104 | throw new SemaphoreException('Cannot create the semaphore.'); 105 | } 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * Acquire the semaphore 112 | * @throws SemaphoreException in case of an error 113 | * @return \QXS\WorkerPool\Semaphore the current object 114 | */ 115 | public function acquire() : Semaphore { 116 | if (!sem_acquire($this->semaphore)) { 117 | throw new SemaphoreException('Cannot acquire the semaphore.'); 118 | } 119 | return $this; 120 | } 121 | 122 | /** 123 | * Releases the semaphore 124 | * @throws SemaphoreException in case of an error 125 | * @return \QXS\WorkerPool\Semaphore the current object 126 | */ 127 | public function release() : Semaphore { 128 | if (!sem_release($this->semaphore)) { 129 | throw new SemaphoreException('Cannot release the semaphore.'); 130 | } 131 | return $this; 132 | } 133 | 134 | /** 135 | * Acquire the semaphore 136 | * @throws SemaphoreException in case of an error 137 | * @return \QXS\WorkerPool\Semaphore the current object 138 | * @see \QXS\WorkerPool\Semaphore::acquire() 139 | */ 140 | public function synchronizedBegin() : Semaphore { 141 | return $this->acquire(); 142 | } 143 | 144 | /** 145 | * Releases the semaphore 146 | * @throws SemaphoreException in case of an error 147 | * @return \QXS\WorkerPool\Semaphore the current object 148 | * @see \QXS\WorkerPool\Semaphore::release() 149 | */ 150 | public function synchronizedEnd() : Semaphore { 151 | return $this->release(); 152 | } 153 | 154 | /** 155 | * Run something synchronized 156 | * @param \Closure $closure the closure, that should be run synchronized 157 | * @throws SemaphoreException in case of an error 158 | * @return \QXS\WorkerPool\Semaphore the current object 159 | */ 160 | public function synchronize(\Closure $closure) : Semaphore { 161 | $this->acquire(); 162 | try { 163 | call_user_func($closure); 164 | } 165 | finally { 166 | $this->release(); 167 | } 168 | return $this; 169 | } 170 | 171 | /** 172 | * Has the semaphore been created? 173 | * @return bool true in case the semaphore has been created 174 | */ 175 | public function isCreated() : bool { 176 | return is_resource($this->semaphore) || $this->semaphore instanceof \SysvSemaphore; 177 | } 178 | 179 | /** 180 | * Destroys the semaphore 181 | * @throws SemaphoreException in case of an error 182 | * @return \QXS\WorkerPool\Semaphore the current object 183 | */ 184 | public function destroy() : Semaphore { 185 | if (!$this->isCreated()) { 186 | throw new SemaphoreException('Semaphore hasn\'t yet been created.'); 187 | } 188 | if (!sem_remove($this->semaphore)) { 189 | throw new SemaphoreException('Cannot remove the semaphore.'); 190 | } 191 | 192 | $this->semaphore = NULL; 193 | $this->semKey = NULL; 194 | return $this; 195 | } 196 | } 197 | 198 | -------------------------------------------------------------------------------- /src/SemaphoreException.php: -------------------------------------------------------------------------------- 1 | socket = $socket; 32 | } 33 | 34 | /** 35 | * The destructor 36 | */ 37 | public function __destruct() { 38 | socket_close($this->socket); 39 | } 40 | 41 | /** 42 | * Selects active sockets with a timeout 43 | * @param SimpleSocket[] $readSockets Array of \QXS\WorkerPool\SimpleSocket Objects, that should be monitored for read activity 44 | * @param SimpleSocket[] $writeSockets Array of \QXS\WorkerPool\SimpleSocket Objects, that should be monitored for write activity 45 | * @param SimpleSocket[] $exceptSockets Array of \QXS\WorkerPool\SimpleSocket Objects, that should be monitored for except activity 46 | * @param int $sec seconds to wait until a timeout is reached 47 | * @param int $usec microseconds to wait a timeout is reached 48 | * @return array Associative Array of \QXS\WorkerPool\SimpleSocket Objects, that matched the monitoring, with the following keys 'read', 'write', 'except' 49 | */ 50 | public static function select(array $readSockets = array(), array $writeSockets = array(), array $exceptSockets = array(), $sec = 0, $usec = 0) : array { 51 | $out = array(); 52 | $out['read'] = array(); 53 | $out['write'] = array(); 54 | $out['except'] = array(); 55 | 56 | if(count($readSockets) === 0){ 57 | return $out; 58 | } 59 | 60 | $readSocketsResources = array(); 61 | $writeSocketsResources = array(); 62 | $exceptSocketsResources = array(); 63 | $readSockets = self::createSocketsIndex($readSockets, $readSocketsResources); 64 | $writeSockets = self::createSocketsIndex($writeSockets, $writeSocketsResources); 65 | $exceptSockets = self::createSocketsIndex($exceptSockets, $exceptSocketsResources); 66 | 67 | $socketsSelected = @socket_select($readSocketsResources, $writeSocketsResources, $exceptSocketsResources, $sec, $usec); 68 | if ($socketsSelected === FALSE) { 69 | $socketError = socket_last_error(); 70 | // 1 more retry https://stackoverflow.com/questions/2933343/php-can-pcntl-alarm-and-socket-select-peacefully-exist-in-the-same-thread/2938156#2938156 71 | if ($socketError === SOCKET_EINTR) { 72 | socket_clear_error(); 73 | 74 | $socketsSelected = socket_select($readSocketsResources, $writeSocketsResources, $exceptSocketsResources, $sec, $usec); 75 | if ($socketsSelected === FALSE) { 76 | return $out; 77 | } 78 | } else { 79 | trigger_error( 80 | sprintf('socket_select(): unable to select [%d]: %s', $socketError, socket_strerror($socketError)), 81 | E_USER_WARNING 82 | ); 83 | return $out; 84 | } 85 | } 86 | 87 | foreach ($readSocketsResources as $socketResource) { 88 | $out['read'][] = $readSockets[self::getSocketId($socketResource)]; 89 | } 90 | foreach ($writeSocketsResources as $socketResource) { 91 | $out['write'][] = $writeSockets[self::getSocketId($socketResource)]; 92 | } 93 | foreach ($exceptSocketsResources as $socketResource) { 94 | $out['except'][] = $exceptSockets[self::getSocketId($socketResource)]; 95 | } 96 | 97 | return $out; 98 | } 99 | 100 | /** 101 | * @param SimpleSocket[] $sockets 102 | * @param array $socketsResources 103 | * @return SimpleSocket[] 104 | */ 105 | protected static function createSocketsIndex($sockets, &$socketsResources) { 106 | $socketsIndex = array(); 107 | foreach ($sockets as $socket) { 108 | if (!$socket instanceof SimpleSocket) { 109 | continue; 110 | } 111 | $resourceId = $socket->getResourceId(); 112 | $socketsIndex[$resourceId] = $socket; 113 | $socketsResources[$resourceId] = $socket->getSocket(); 114 | } 115 | 116 | return $socketsIndex; 117 | } 118 | 119 | /** 120 | * Get the id of the socket resource 121 | * @return int the id of the socket resource 122 | */ 123 | public function getResourceId() : int { 124 | return self::getSocketId($this->socket); 125 | } 126 | 127 | /** 128 | * Get the id of the socket 129 | * @param $socket 130 | * @return int the id of the socket 131 | */ 132 | protected static function getSocketId($socket) : int { 133 | if ($socket instanceof \Socket) { 134 | return spl_object_id($socket); 135 | } 136 | 137 | return intval($socket); 138 | } 139 | 140 | /** 141 | * Get the socket resource 142 | * @return resource the socket resource 143 | */ 144 | public function getSocket() { 145 | return $this->socket; 146 | } 147 | 148 | /** 149 | * Check if there is any data available 150 | * @param int $sec seconds to wait until a timeout is reached 151 | * @param int $usec microseconds to wait a timeout is reached 152 | * @return bool true, in case there is data, that can be red 153 | */ 154 | public function hasData($sec = 0, $usec = 0) : bool { 155 | $sec = (int)$sec; 156 | $usec = (int)$usec; 157 | if ($sec < 0) { 158 | $sec = 0; 159 | } 160 | if ($usec < 0) { 161 | $usec = 0; 162 | } 163 | 164 | $read = array($this->socket); 165 | $write = array(); 166 | $except = array(); 167 | $sockets = socket_select($read, $write, $except, $sec, $usec); 168 | 169 | if ($sockets === FALSE) { 170 | return FALSE; 171 | } 172 | return $sockets > 0; 173 | } 174 | 175 | /** 176 | * Write the data to the socket in a predetermined format 177 | * @param mixed $data the data, that should be sent 178 | * @throws \QXS\WorkerPool\SimpleSocketException in case of an error 179 | */ 180 | public function send($data) : void { 181 | $serialized = serialize($data); 182 | $hdr = pack('N', strlen($serialized)); // 4 byte length 183 | $buffer = $hdr . $serialized; 184 | unset($serialized); 185 | unset($hdr); 186 | $total = strlen($buffer); 187 | while ($total > 0) { 188 | $sent = socket_write($this->socket, $buffer); 189 | if ($sent === FALSE) { 190 | throw new SimpleSocketException('Sending failed with: ' . socket_strerror(socket_last_error($this->socket))); 191 | } 192 | $total -= $sent; 193 | $buffer = substr($buffer, $sent); 194 | } 195 | } 196 | 197 | /** 198 | * Read a data packet from the socket in a predetermined format. 199 | * @throws \QXS\WorkerPool\SimpleSocketException in case of an error 200 | * @return mixed the data, that has been received 201 | */ 202 | public function receive() { 203 | // read 4 byte length first 204 | $hdr = ''; 205 | do { 206 | $read = socket_read($this->socket, 4 - strlen($hdr)); 207 | if ($read === FALSE) { 208 | throw new SimpleSocketException('Reception failed with: ' . socket_strerror(socket_last_error($this->socket))); 209 | } 210 | elseif ($read === '' || $read === NULL) { 211 | return NULL; 212 | } 213 | $hdr .= $read; 214 | } while (strlen($hdr) < 4); 215 | 216 | list($len) = array_values(unpack("N", $hdr)); 217 | 218 | // read the full buffer 219 | $buffer = ''; 220 | do { 221 | $read = socket_read($this->socket, $len - strlen($buffer)); 222 | if ($read === FALSE || $read == '') { 223 | throw new SimpleSocketException('Reception failed with: ' . socket_strerror(socket_last_error($this->socket))); 224 | } 225 | elseif ($read == '') { 226 | return NULL; 227 | } 228 | $buffer .= $read; 229 | } while (strlen($buffer) < $len); 230 | 231 | $data = unserialize($buffer); 232 | return $data; 233 | } 234 | } 235 | 236 | -------------------------------------------------------------------------------- /src/SimpleSocketException.php: -------------------------------------------------------------------------------- 1 | /etc/php.d/proctitle.ini 14 | */ 15 | 16 | namespace QXS\WorkerPool; 17 | 18 | /** 19 | * The Worker Pool class runs worker processes in parallel 20 | * 21 | */ 22 | class WorkerPool implements \Iterator, \Countable { 23 | 24 | /** Default child timeout in seconds */ 25 | const CHILD_TIMEOUT_SEC = 10; 26 | 27 | /** @var array signals, that should be watched */ 28 | protected $signals = array( 29 | SIGCHLD, SIGTERM, SIGHUP, SIGUSR1, SIGINT 30 | ); 31 | 32 | /** @var bool is the pool created? (children forked) */ 33 | private $created = FALSE; 34 | 35 | /** @var int number of children in the pool */ 36 | private $workerPoolSize = 2; 37 | 38 | /** @var int number of children initially in the pool */ 39 | private $initialPoolSize; 40 | 41 | /** @var int Current index for the last worker created in the pool */ 42 | private $currentWorkerIndex = 0; 43 | 44 | /** @var int id of the parent */ 45 | protected $parentPid = 0; 46 | 47 | /** @var \QXS\WorkerPool\WorkerInterface the worker class, that is used to run the tasks */ 48 | protected $worker; 49 | 50 | /** @var \QXS\WorkerPool\Semaphore the semaphore, that is used to synchronizd tasks across all processes */ 51 | protected $semaphore; 52 | 53 | /** @var ProcessDetailsCollection|ProcessDetails[] Collection of the worker processes */ 54 | protected $workerProcesses; 55 | 56 | /** @var array received results from the workers */ 57 | protected $results = array(); 58 | 59 | /** @var int number of received results */ 60 | protected $resultPosition = 0; 61 | 62 | /** @var string process title of the parent */ 63 | protected $parentProcessTitleFormat = '%basename%: Parent'; 64 | 65 | /** @var string process title of the children */ 66 | protected $childProcessTitleFormat = '%basename%: Worker %i% of %class% [%state%]'; 67 | 68 | /** @var boolean Respawn dead workers automatically if set to TRUE */ 69 | private $respawnAutomatically = false; 70 | 71 | /** @var int Default child timeout in seconds */ 72 | private $child_timeout_sec = self::CHILD_TIMEOUT_SEC; 73 | 74 | /** 75 | * The constructor 76 | */ 77 | public function __construct() { 78 | $this->workerProcesses = new ProcessDetailsCollection(); 79 | register_shutdown_function(array($this, 'onShutDown')); 80 | } 81 | 82 | /** 83 | * The destructor 84 | */ 85 | public function __destruct() { 86 | if ($this->created) { 87 | $this->destroy(); 88 | } 89 | } 90 | 91 | /** 92 | * Returns the process title of the child 93 | * @return string the process title of the child 94 | */ 95 | public function getChildProcessTitleFormat() : string { 96 | return $this->childProcessTitleFormat; 97 | } 98 | 99 | /** 100 | * Sets the process title of the child 101 | * 102 | * Listing permitted replacments 103 | * %i% The Child's Number 104 | * %basename% The base name of PHPSELF 105 | * %fullname% The value of PHPSELF 106 | * %class% The Worker's Classname 107 | * %state% The Worker's State 108 | * 109 | * @param string $string the process title of the child 110 | * @return WorkerPool 111 | * @throws \QXS\WorkerPool\WorkerPoolException in case the WorkerPool has already been created 112 | * @throws \DomainException in case the $string value is not within the permitted range 113 | */ 114 | public function setChildProcessTitleFormat($string) : WorkerPool { 115 | if ($this->created) { 116 | throw new WorkerPoolException('Cannot set the Parent\'s Process Title Format for a created pool.'); 117 | } 118 | $this->childProcessTitleFormat = ProcessDetails::sanitizeProcessTitleFormat($string); 119 | return $this; 120 | } 121 | 122 | /** 123 | * Returns the process title of the parent 124 | * @return string the process title of the parent 125 | */ 126 | public function getParentProcessTitleFormat() { 127 | return $this->parentProcessTitleFormat; 128 | } 129 | 130 | /** 131 | * Sets the process title of the parent 132 | * 133 | * Listing permitted replacments 134 | * %basename% The base name of PHPSELF 135 | * %fullname% The value of PHPSELF 136 | * %class% The WorkerPool's Classname 137 | * 138 | * @param string $string the process title of the parent 139 | * @return WorkerPool 140 | * @throws \QXS\WorkerPool\WorkerPoolException in case the WorkerPool has already been created 141 | * @throws \DomainException in case the $string value is not within the permitted range 142 | */ 143 | public function setParentProcessTitleFormat($string) { 144 | if ($this->created) { 145 | throw new WorkerPoolException('Cannot set the Children\'s Process Title Format for a created pool.'); 146 | } 147 | $this->parentProcessTitleFormat = ProcessDetails::sanitizeProcessTitleFormat($string); 148 | return $this; 149 | } 150 | 151 | /** 152 | * Returns the current size of the worker pool 153 | * 154 | * In case the pool hasn't yet been created, this method returns the value of the currently set pool size. 155 | * In case of a created pool, this method reports the real pool size (number of alive worker processes). 156 | * @return int the number of processes 157 | */ 158 | public function getWorkerPoolSize() { 159 | return $this->workerPoolSize; 160 | } 161 | 162 | /** 163 | * Sets the current size of the worker pool 164 | * @param int $size the new worker pool size 165 | * @return WorkerPool 166 | * @throws \QXS\WorkerPool\WorkerPoolException in case the WorkerPool has already been created 167 | * @throws \InvalidArgumentException in case the $size value is not within the permitted range 168 | */ 169 | public function setWorkerPoolSize($size) { 170 | if ($this->created) { 171 | throw new WorkerPoolException('Cannot set the Worker Pool Size for a created pool.'); 172 | } 173 | $size = (int)$size; 174 | if ($size <= 0) { 175 | throw new \InvalidArgumentException('"' . $size . '" is not an integer greater than 0.'); 176 | } 177 | $this->workerPoolSize = $size; 178 | return $this; 179 | } 180 | 181 | /** 182 | * Gets the Semaphore, that will be used within the worker processes 183 | * @return null|\QXS\WorkerPool\Semaphore $semaphore the Semaphore, that should be used for the workers 184 | */ 185 | public function getSemaphore() : Semaphore { 186 | return $this->semaphore; 187 | } 188 | 189 | /** 190 | * Sets the Semaphore, that will be used within the worker processes 191 | * @param \QXS\WorkerPool\Semaphore $semaphore the Semaphore, that should be used for the workers 192 | * @return WorkerPool 193 | * @throws \QXS\WorkerPool\WorkerPoolException in case the WorkerPool has already been created 194 | * @throws \InvalidArgumentException in case the semaphre hasn't been created 195 | */ 196 | public function setSemaphore(Semaphore $semaphore) : WorkerPool { 197 | if ($this->created) { 198 | throw new WorkerPoolException('Cannot set the Worker Pool Size for a created pool.'); 199 | } 200 | if (!$semaphore->isCreated()) { 201 | throw new \InvalidArgumentException('The Semaphore hasn\'t yet been created.'); 202 | } 203 | $this->semaphore = $semaphore; 204 | return $this; 205 | } 206 | 207 | /** 208 | * Disables the semaphore feature in the workerpool 209 | * 210 | * Attention: You will lose the possibility to synchronize worker processes 211 | * 212 | * @throws \QXS\WorkerPool\WorkerPoolException in case the WorkerPool has already been created 213 | * @throws \InvalidArgumentException in case the semaphre hasn't been created 214 | */ 215 | public function disableSemaphore() : WorkerPool { 216 | $sem = new NoSemaphore(); 217 | $sem->create(); 218 | $this->setSemaphore($sem); 219 | return $this; 220 | } 221 | 222 | /** 223 | * Sets default child timeout in seconds. 224 | * @param int $child_timeout_sec 225 | * @return WorkerPool 226 | */ 227 | public function setChildTimeoutSec($child_timeout_sec) : WorkerPool { 228 | $this->child_timeout_sec = $child_timeout_sec; 229 | return $this; 230 | } 231 | 232 | /** 233 | * Terminates the current process 234 | * @param int $code the exit code 235 | */ 236 | public function exitPhp($code) { 237 | exit($code); 238 | } 239 | 240 | /** 241 | * Creates the worker pool (forks the children) 242 | * 243 | * Please close all open resources before running this function. 244 | * Child processes are going to close all open resources uppon exit, 245 | * leaving the parent process behind with invalid resource handles. 246 | * @param \QXS\WorkerPool\WorkerInterface $worker the worker, that runs future tasks 247 | * @throws \RuntimeException 248 | * @throws WorkerPoolException 249 | * @return WorkerPool 250 | */ 251 | public function create(WorkerInterface $worker) : WorkerPool { 252 | $this->initialPoolSize = $this->workerPoolSize; 253 | $this->parentPid = getmypid(); 254 | $this->worker = $worker; 255 | if ($this->created) { 256 | throw new WorkerPoolException('The pool has already been created.'); 257 | } 258 | 259 | $this->created = TRUE; 260 | // when adding signals use pcntl_signal_dispatch(); or declare ticks 261 | foreach ($this->signals as $signo) { 262 | pcntl_signal($signo, array($this, 'signalHandler')); 263 | } 264 | 265 | // no Semaphore attached? -> create one 266 | if (!($this->semaphore instanceof Semaphore)) { 267 | $this->semaphore = new Semaphore(); 268 | $this->semaphore->create(Semaphore::SEM_RAND_KEY); 269 | } 270 | elseif(!$this->semaphore->isCreated()) { 271 | $this->semaphore->create(Semaphore::SEM_RAND_KEY); 272 | } 273 | 274 | ProcessDetails::setProcessTitle( 275 | $this->parentProcessTitleFormat, 276 | array( 277 | 'basename' => basename($_SERVER['PHP_SELF']), 278 | 'fullname' => $_SERVER['PHP_SELF'], 279 | 'class' => get_class($this) 280 | ) 281 | ); 282 | 283 | for ($this->currentWorkerIndex = 1; $this->currentWorkerIndex <= $this->workerPoolSize; $this->currentWorkerIndex++) { 284 | $this->createWorker($this->currentWorkerIndex); 285 | } 286 | 287 | return $this; 288 | } 289 | 290 | /** 291 | * Creates the worker 292 | * @param int $i 293 | * @throws \RuntimeException 294 | */ 295 | private function createWorker($i) { 296 | $sockets = array(); 297 | if (socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets) === FALSE) { 298 | // clean_up using posix_kill & pcntl_wait 299 | throw new \RuntimeException('socket_create_pair failed.'); 300 | return; 301 | } 302 | $processId = pcntl_fork(); 303 | if ($processId < 0) { 304 | // cleanup using posix_kill & pcntl_wait 305 | throw new \RuntimeException('pcntl_fork failed.'); 306 | return; 307 | } 308 | elseif ($processId === 0) { 309 | // WE ARE IN THE CHILD 310 | $this->workerProcesses = new ProcessDetailsCollection(); // we do not have any children 311 | $this->workerPoolSize = 0; // we do not have any children 312 | socket_close($sockets[1]); // close the parent socket 313 | $this->runWorkerProcess($this->worker, new SimpleSocket($sockets[0]), $i); 314 | } 315 | else { 316 | // WE ARE IN THE PARENT 317 | socket_close($sockets[0]); // close child socket 318 | // create the child 319 | $this->workerProcesses->addFree(new ProcessDetails($processId, new SimpleSocket($sockets[1]))); 320 | } 321 | } 322 | 323 | /** 324 | * Run the worker process 325 | * @param \QXS\WorkerPool\WorkerInterface $worker the worker, that runs the tasks 326 | * @param \QXS\WorkerPool\SimpleSocket $simpleSocket the simpleSocket, that is used for the communication 327 | * @param int $i the number of the child 328 | */ 329 | protected function runWorkerProcess(WorkerInterface $worker, SimpleSocket $simpleSocket, $i) { 330 | $replacements = array( 331 | 'basename' => basename($_SERVER['PHP_SELF']), 332 | 'fullname' => $_SERVER['PHP_SELF'], 333 | 'class' => get_class($worker), 334 | 'i' => $i, 335 | 'state' => 'free' 336 | ); 337 | ProcessDetails::setProcessTitle($this->childProcessTitleFormat, $replacements); 338 | $this->worker->onProcessCreate($this->semaphore); 339 | while (TRUE) { 340 | $output = array('pid' => getmypid()); 341 | try { 342 | pcntl_signal_dispatch(); 343 | $replacements['state'] = 'free'; 344 | ProcessDetails::setProcessTitle($this->childProcessTitleFormat, $replacements); 345 | $cmd = $simpleSocket->receive(); 346 | // invalid response from parent? 347 | if (!isset($cmd['cmd'])) { 348 | break; 349 | } 350 | $replacements['state'] = 'busy'; 351 | ProcessDetails::setProcessTitle($this->childProcessTitleFormat, $replacements); 352 | if ($cmd['cmd'] == 'run') { 353 | try { 354 | $output['data'] = $this->worker->run($cmd['data']); 355 | } catch (\Exception $e) { 356 | $output['workerException'] = array( 357 | 'class' => get_class($e), 358 | 'message' => $e->getMessage(), 359 | 'trace' => $e->getTraceAsString() 360 | ); 361 | } 362 | // send back the output 363 | $simpleSocket->send($output); 364 | } elseif ($cmd['cmd'] == 'exit') { 365 | break; 366 | } 367 | } catch (SimpleSocketException $e) { 368 | break; 369 | } catch (\Exception $e) { 370 | // send Back the exception 371 | $output['poolException'] = array( 372 | 'class' => get_class($e), 373 | 'message' => $e->getMessage(), 374 | 'trace' => $e->getTraceAsString() 375 | ); 376 | $simpleSocket->send($output); 377 | } 378 | } 379 | $this->worker->onProcessDestroy(); 380 | $this->exitPhp(0); 381 | } 382 | 383 | /** 384 | * This runs on shutdown to prevent the system from semaphore leaks 385 | */ 386 | public function onShutDown() { 387 | // are we in the parent? 388 | if ($this->parentPid === getmypid()) { 389 | if($this->created) { 390 | $this->destroy(); 391 | } 392 | } 393 | } 394 | 395 | /** 396 | * Destroy the WorkerPool with all its children 397 | * @param int $maxWaitSecs a timeout to wait for the children, before killing them 398 | * @throws WorkerPoolException 399 | * @return WorkerPool 400 | */ 401 | public function destroy($maxWaitSecs = null) : WorkerPool { 402 | if ($maxWaitSecs === null) { 403 | $maxWaitSecs = $this->child_timeout_sec; 404 | } 405 | if (!$this->created) { 406 | throw new WorkerPoolException('The pool hasn\'t yet been created.'); 407 | } 408 | $this->created = FALSE; 409 | 410 | if ($this->parentPid === getmypid()) { 411 | $maxWaitSecs = ((int)$maxWaitSecs) * 2; 412 | if ($maxWaitSecs <= 1) { 413 | $maxWaitSecs = 2; 414 | } 415 | // send the exit instruction 416 | foreach ($this->workerProcesses as $processDetails) { 417 | try { 418 | $processDetails->getSocket()->send(array('cmd' => 'exit')); 419 | } catch (\Exception $e) { 420 | } 421 | } 422 | // wait up to 10 seconds 423 | for ($i = 0; $i < $maxWaitSecs; $i++) { 424 | usleep(500000); // 0.5 seconds 425 | pcntl_signal_dispatch(); 426 | if ($this->workerPoolSize == 0) { 427 | break; 428 | } 429 | } 430 | 431 | // reset signals 432 | foreach ($this->signals as $signo) { 433 | pcntl_signal($signo, SIG_DFL); 434 | } 435 | 436 | // kill all remaining processes 437 | $this->workerProcesses->killAllProcesses(); 438 | 439 | usleep(500000); // 0.5 seconds 440 | // reap the remaining signals 441 | $this->reaper(); 442 | // destroy the semaphore 443 | $this->semaphore->destroy(); 444 | 445 | unset($this->workerProcesses); 446 | } 447 | 448 | return $this; 449 | } 450 | 451 | /** 452 | * Receives signals 453 | * 454 | * DO NOT MANUALLY CALL THIS METHOD! 455 | * pcntl_signal_dispatch() will be calling this method. 456 | * @param int $signo the signal number 457 | * @see pcntl_signal_dispatch 458 | * @see pcntl_signal 459 | */ 460 | public function signalHandler($signo) { 461 | switch ($signo) { 462 | case SIGCHLD: 463 | $this->reaper(); 464 | break; 465 | case SIGTERM: 466 | // handle shutdown tasks 467 | $this->exitPhp(0); 468 | break; 469 | case SIGHUP: 470 | // handle restart tasks 471 | break; 472 | case SIGUSR1: 473 | // handle sigusr 474 | break; 475 | default: // handle all other signals 476 | } 477 | // more signals to dispatch? 478 | pcntl_signal_dispatch(); 479 | } 480 | 481 | /** 482 | * Respawn workers automatically if they died 483 | * @param boolean $respawn 484 | * @return WorkerPool 485 | */ 486 | public function respawnAutomatically($respawn = true) : WorkerPool { 487 | if ($this->respawnAutomatically = $respawn) { 488 | $this->child_timeout_sec = 1; 489 | } 490 | return $this; 491 | } 492 | 493 | private function respawnIfRequired() { 494 | if (!$this->respawnAutomatically) { 495 | return; 496 | } 497 | while ($this->workerPoolSize < $this->initialPoolSize) { 498 | $this->createWorker(++$this->currentWorkerIndex); 499 | $this->workerPoolSize++; 500 | } 501 | } 502 | 503 | /** 504 | * Child process reaper 505 | * @param int $pid the process id 506 | * @see pcntl_waitpid 507 | */ 508 | protected function reaper($pid = -1) { 509 | if (!is_int($pid)) { 510 | $pid = -1; 511 | } 512 | $childpid = pcntl_waitpid($pid, $status, WNOHANG); 513 | while ($childpid > 0) { 514 | $stopSignal = pcntl_wstopsig($status); 515 | if (pcntl_wifexited($stopSignal) === FALSE) { 516 | array_push($this->results, new WorkerPoolResult(array( 517 | 'pid' => $childpid, 518 | 'abnormalChildReturnCode' => $stopSignal 519 | ))); 520 | } 521 | 522 | $processDetails = $this->workerProcesses->getProcessDetails($childpid); 523 | if ($processDetails !== NULL) { 524 | $this->workerPoolSize--; 525 | $this->workerProcesses->remove($processDetails); 526 | unset($processDetails); 527 | } 528 | $childpid = pcntl_waitpid($pid, $status, WNOHANG); 529 | } 530 | } 531 | 532 | 533 | /** 534 | * Trys to wait for one free worker within a fixed timeout 535 | * 536 | * This function blocks until a worker has finished its work or the timeout has been reached. 537 | * You can kill hanging child processes, so that the parent will be unblocked. 538 | * Note: the run method already blocks until a free worker is available. 539 | * @param int $timeout the timeout in seconds 540 | * @return bool true, in case there is a free worker or false, in case the timeout has been reached 541 | */ 542 | public function tryWaitForOneFreeWorker($timeout=10) : bool { 543 | $this->collectWorkerResults((int)abs($timeout)); 544 | return $this->getFreeWorkers() > 0; 545 | } 546 | /** 547 | * Waits for one free worker 548 | * 549 | * This function blocks until a worker has finished its work. 550 | * You can kill hanging child processes, so that the parent will be unblocked. 551 | * Note: the run method already blocks until a free worker is available. 552 | */ 553 | public function waitForOneFreeWorker() : void { 554 | while ($this->getFreeWorkers() == 0) { 555 | $this->collectWorkerResults($this->child_timeout_sec); 556 | } 557 | } 558 | /** 559 | * Waits for all children to finish their worker 560 | * 561 | * This function blocks until every worker has finished its work. 562 | * You can kill hanging child processes, so that the parent will be unblocked. 563 | */ 564 | public function waitForAllWorkers() : void { 565 | while ($this->getBusyWorkers() > 0) { 566 | $this->collectWorkerResults($this->child_timeout_sec); 567 | } 568 | } 569 | 570 | /** 571 | * Returns the number of busy and free workers 572 | * 573 | * This function collects all the information at once. 574 | * @return array with the keys 'free', 'busy', 'total' 575 | */ 576 | public function getFreeAndBusyWorkers() : array { 577 | $free = $this->getFreeWorkers(); 578 | return array( 579 | 'free' => $free, 580 | 'busy' => $this->workerPoolSize - $free, 581 | 'total' => $this->workerPoolSize 582 | ); 583 | } 584 | 585 | /** 586 | * Returns the number of free workers 587 | * 588 | * PAY ATTENTION WHEN USING THIS FUNCTION WITH A SUBSEQUENT CALL OF getBusyWorkers(). 589 | * IN THIS CASE THE SUM MIGHT NOT EQUAL TO THE CURRENT POOL SIZE. 590 | * USE getFreeAndBusyWorkers() TO GET CONSISTENT RESULTS. 591 | * @return int number of free workers 592 | */ 593 | public function getFreeWorkers() : int { 594 | $this->collectWorkerResults(); 595 | return $this->workerProcesses->getFreeProcessesCount(); 596 | } 597 | 598 | /** 599 | * Returns the number of busy workers 600 | * 601 | * PAY ATTENTION WHEN USING THIS FUNCTION WITH A SUBSEQUENT CALL OF getFreeWorkers(). 602 | * IN THIS CASE THE SUM MIGHT NOT EQUAL TO THE CURRENT POOL SIZE. 603 | * USE getFreeAndBusyWorkers() TO GET CONSISTENT RESULTS. 604 | * @return int number of free workers 605 | */ 606 | public function getBusyWorkers() : int { 607 | return $this->workerPoolSize - $this->getFreeWorkers(); 608 | } 609 | 610 | /** 611 | * Get the pid of the next free worker 612 | * 613 | * This function blocks until a worker has finished its work. 614 | * You can kill all child processes, so that the parent will be unblocked. 615 | * @throws WorkerPoolException 616 | * @return ProcessDetails the pid of the next free child 617 | */ 618 | protected function getNextFreeWorker(): ?ProcessDetails { 619 | $sec = 0; 620 | while (TRUE) { 621 | $this->collectWorkerResults($sec); 622 | 623 | $freeProcess = $this->workerProcesses->takeFreeProcess(); 624 | if ($freeProcess !== NULL) { 625 | return $freeProcess; 626 | } 627 | 628 | $sec = $this->child_timeout_sec; 629 | if ($this->workerPoolSize <= 0) { 630 | throw new WorkerPoolException('All workers were gone.'); 631 | } 632 | } 633 | 634 | return NULL; 635 | } 636 | 637 | /** 638 | * Collects the results form the workers and processes any pending signals 639 | * @param int $sec timeout to wait for new results from the workers 640 | * @throws WorkerPoolException 641 | */ 642 | protected function collectWorkerResults($sec = 0) { 643 | $this->respawnIfRequired(); 644 | 645 | // dispatch signals 646 | pcntl_signal_dispatch(); 647 | 648 | if (isset($this->workerProcesses) === FALSE) { 649 | throw new WorkerPoolException('There is no list of worker processes. Maybe you destroyed the worker pool?', 1401179881); 650 | } 651 | $result = SimpleSocket::select($this->workerProcesses->getSockets(), array(), array(), $sec); 652 | foreach ($result['read'] as $socket) { 653 | /** @var $socket SimpleSocket */ 654 | $processId = $socket->annotation['pid']; 655 | $result = $socket->receive(); 656 | 657 | $possibleArrayKeys = array('data', 'poolException', 'workerException'); 658 | if (is_array($result) && count(($resultTypes = array_intersect(array_keys($result), $possibleArrayKeys))) === 1) { 659 | // If the result has the expected format, free the worker and store the result. 660 | // Otherwise, the worker may be abnormally terminated (fatal error, exit(), ...) and will 661 | // fall in the reapers arms. 662 | $this->workerProcesses->registerFreeProcessId($processId); 663 | $result['pid'] = $processId; 664 | $resultType = reset($resultTypes); 665 | // Do not store NULL 666 | if ($resultType !== 'data' || $result['data'] !== NULL) { 667 | array_push($this->results, new WorkerPoolResult($result)); 668 | } 669 | } 670 | } 671 | // dispatch signals 672 | pcntl_signal_dispatch(); 673 | 674 | $this->respawnIfRequired(); 675 | } 676 | 677 | /** 678 | * Sends the input to the next free worker process 679 | * 680 | * This function blocks until a worker has finished its work. 681 | * You can kill all child processes, so that the parent will be unblocked. 682 | * @param mixed $input any serializable value 683 | * @throws WorkerPoolException 684 | * @return int The PID of the processing worker process 685 | */ 686 | public function run($input) : int { 687 | while ($this->workerPoolSize > 0) { 688 | try { 689 | $processDetailsOfFreeWorker = $this->getNextFreeWorker(); 690 | $processDetailsOfFreeWorker->getSocket()->send(array('cmd' => 'run', 'data' => $input)); 691 | return $processDetailsOfFreeWorker->getPid(); 692 | } catch (\Exception $e) { 693 | pcntl_signal_dispatch(); 694 | } 695 | } 696 | throw new WorkerPoolException('Unable to run the task.'); 697 | } 698 | 699 | /** 700 | * Clear all the results 701 | */ 702 | public function clearResults() : WorkerPool { 703 | $this->collectWorkerResults(); 704 | $this->results = array(); 705 | return $this; 706 | } 707 | 708 | /** 709 | * Is there any result available? 710 | * @return bool true, in case we have received some results 711 | */ 712 | public function hasResults() : bool { 713 | $this->collectWorkerResults(); 714 | return !empty($this->results); 715 | } 716 | 717 | /** 718 | * How many results did we receive? 719 | * @return int the number of results 720 | */ 721 | public function countResults() : int { 722 | $this->collectWorkerResults(); 723 | return $this->count(); 724 | } 725 | 726 | /** 727 | * Shifts the next result from the result queue 728 | * @return null|WorkerPoolResult gets the next result 729 | */ 730 | public function getNextResult() : ?WorkerPoolResult { 731 | $this->collectWorkerResults(); 732 | return array_shift($this->results); 733 | } 734 | 735 | /** 736 | * Countable Method count 737 | * @return int the number of results 738 | * @see \QXS\WorkerPool\WorkerPool::countResults() 739 | */ 740 | public function count() : int { 741 | $this->collectWorkerResults(); 742 | return count($this->results); 743 | } 744 | 745 | /** 746 | * Iterator Method current 747 | * @return null|WorkerPoolResult gets the current result 748 | */ 749 | public function current() : ?WorkerPoolResult { 750 | $c = reset($this->results); 751 | if(is_bool($c)) { 752 | return NULL; 753 | } 754 | return $c; 755 | } 756 | 757 | /** 758 | * Iterator Method key 759 | * @return int returns the current key 760 | */ 761 | public function key() : int { 762 | return $this->resultPosition; 763 | } 764 | 765 | /** 766 | * Iterator Method next() 767 | */ 768 | public function next() : void { 769 | $this->collectWorkerResults(); 770 | if (!empty($this->results)) { 771 | $this->resultPosition++; 772 | } 773 | array_shift($this->results); 774 | } 775 | 776 | /** 777 | * Iterator Method rewind() 778 | */ 779 | public function rewind() : void { 780 | } 781 | 782 | /** 783 | * Iterator Method valid() 784 | * @return bool true = there is a pending result 785 | */ 786 | public function valid() : bool { 787 | return !empty($this->results); 788 | } 789 | } 790 | -------------------------------------------------------------------------------- /src/WorkerPoolException.php: -------------------------------------------------------------------------------- 1 | result = $result; 18 | } 19 | 20 | /** 21 | * Does the offset exist? 22 | * @param string $offset can be 'class', 'message', 'trace' 23 | * @return bool true, if the offset estists 24 | */ 25 | public function offsetExists($offset): bool { 26 | $offset = (string)$offset; 27 | return array_key_exists($offset, $this->result); 28 | } 29 | /** 30 | * Get the offset 31 | * @param string $offset can be 'class', 'message', 'trace' 32 | * @return string the result 33 | */ 34 | public function offsetGet($offset) : string { 35 | $offset = (string)$offset; 36 | if($this->offsetExists($offset)) { 37 | return (string)$this->result[$offset]; 38 | } 39 | return ""; 40 | } 41 | /** 42 | * Set the offset 43 | * this will always throw an exception, because it is read only 44 | * @param string $offset can be 'class', 'message', 'trace' 45 | * @param string $value the value 46 | * @throws \LogicException always throws an exception becasue it is read only 47 | */ 48 | public function offsetSet($offset, $value): void { 49 | throw new \LogicException("Not allowed to add/modify keys"); 50 | } 51 | /** 52 | * Unset the offset 53 | * this will always throw an exception, because it is read only 54 | * @param string $offset can be 'class', 'message', 'trace' 55 | * @throws \LogicException always throws an exception becasue it is read only 56 | */ 57 | public function offsetUnset($offset): void { 58 | throw new \LogicException("Not allowed to add/modify keys"); 59 | } 60 | 61 | 62 | /** 63 | * Get the class name of the exception 64 | * @return string the class name of the exception 65 | */ 66 | public function getClass() : string { 67 | return (string)$this->offsetGet('class'); 68 | } 69 | 70 | /** 71 | * Get the message of the exception 72 | * @return string the messsage of the exception 73 | */ 74 | public function getMessage() : string { 75 | return (string)$this->offsetGet('message'); 76 | } 77 | 78 | /** 79 | * Get the stack trace of the exception 80 | * @return string the stack trace of the exception 81 | */ 82 | public function getTrace() : string { 83 | return (string)$this->offsetGet('trace'); 84 | } 85 | 86 | 87 | /** 88 | * Get the object as a human readable string 89 | * @return string the object as a human redable string 90 | */ 91 | public function __toString() : string { 92 | return 93 | 'Exception Class: ' . $this->offsetGet('class') . "\n" . 94 | 'Exception Message: ' . $this->offsetGet('message') . "\n" . 95 | 'Exception Trace:' . "\n" . $this->offsetGet('trace') 96 | ; 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /src/WorkerPoolResult.php: -------------------------------------------------------------------------------- 1 | resultType = $k; 24 | } 25 | } 26 | if(array_key_exists('abnormalChildReturnCode', $result)) { 27 | if(!array_key_exists('poolException', $result)) { 28 | $result['poolException'] = new WorkerPoolExceptionResult(array( 29 | 'class' => 'RuntimeException', 30 | 'message' => 'The WorkerPool process reaper discovered an abnormal child return code: ' . $result['abnormalChildReturnCode'], 31 | 'trace' => '' 32 | )); 33 | } 34 | } 35 | $this->result = $result; 36 | } 37 | 38 | /** 39 | * Does the offset exist? 40 | * @param string $offset can be 'pid', 'data', 'poolException', 'workerException' 41 | * @return bool true, if the offset estists 42 | */ 43 | public function offsetExists($offset): bool { 44 | $offset = (string)$offset; 45 | return array_key_exists($offset, $this->result); 46 | } 47 | /** 48 | * Get the offset 49 | * @param string $offset can be 'pid', 'data', 'poolException', 'workerException' 50 | * @return mixed any value depending on the result and offset 51 | */ 52 | #[\ReturnTypeWillChange] 53 | public function offsetGet($offset) { 54 | $offset = (string)$offset; 55 | if($this->offsetExists($offset)) { 56 | return $this->result[$offset]; 57 | } 58 | return NULL; 59 | } 60 | /** 61 | * Set the offset 62 | * this will always throw an exception, because it is read only 63 | * @param string $offset can be 'pid', 'data', 'poolException', 'workerException' 64 | * @param mixed $value any value depending on the result and offset 65 | * @throws \LogicException always throws an exception becasue it is read only 66 | */ 67 | public function offsetSet($offset,$value): void { 68 | throw new \LogicException("Not allowed to add/modify keys"); 69 | } 70 | /** 71 | * Unset the offset 72 | * this will always throw an exception, because it is read only 73 | * @param string $offset can be 'pid', 'data', 'poolException', 'workerException' 74 | * @throws \LogicException always throws an exception becasue it is read only 75 | */ 76 | public function offsetUnset($offset): void { 77 | throw new \LogicException("Not allowed to add/modify keys"); 78 | } 79 | 80 | /** 81 | * Get the 'pid' 82 | * @return int the value of 'pid' 83 | */ 84 | public function getPid() : int { 85 | return (int)$this->offsetGet('pid'); 86 | } 87 | 88 | /** 89 | * Get the result type 90 | * @return string one of the values 'data', 'poolException', 'workerException' 91 | */ 92 | public function getResultType() : string { 93 | return $this->resultType; 94 | } 95 | 96 | /** 97 | * Does the result have a workerpool exception? 98 | * @return bool true, if the result has a workerpool exception 99 | */ 100 | public function hasPoolException() : bool { 101 | return $this->offsetExists('poolException'); 102 | } 103 | 104 | /** 105 | * Get the workerpool exception 106 | * @return null|WorkerPoolExceptionResult the exception returned from the workerpool 107 | */ 108 | public function getPoolException() : ?WorkerPoolExceptionResult { 109 | return $this->offsetGet('poolException'); 110 | } 111 | 112 | /** 113 | * Does the result have a worker exception? 114 | * @return bool true, if the result has a worker exception 115 | */ 116 | public function hasWorkerException() : bool { 117 | return $this->offsetExists('workerException'); 118 | } 119 | 120 | /** 121 | * Get the worker exception 122 | * @return null|WorkerPoolExceptionResult the exception returned from the worker 123 | */ 124 | public function getWorkerException() : ?WorkerPoolExceptionResult { 125 | return $this->offsetGet('workerException'); 126 | } 127 | 128 | /** 129 | * Does the result have data? 130 | * @return bool true, if the result has data 131 | */ 132 | public function hasData() : bool { 133 | return $this->offsetExists('data'); 134 | } 135 | 136 | /** 137 | * Get the data 138 | * @return mixed the data returned from the worker 139 | */ 140 | public function getData() { 141 | return $this->offsetGet('data'); 142 | } 143 | 144 | /** 145 | * Uses var_dump to dump data 146 | * @return string the dumped output 147 | */ 148 | public function dump() : string { 149 | if($this->hasPoolException()) { 150 | // return pool exception 151 | return 152 | 'PID: ' . $this->offsetGet('pid') . "\n" . 153 | 'Pool Exception:' . "\n" . WorkerPoolResult::indentExceptionString($this->getPoolException()) . "\n" 154 | ; 155 | } 156 | elseif($this->hasWorkerException()) { 157 | // return worker exception 158 | return 159 | 'PID: ' . $this->offsetGet('pid') . "\n" . 160 | 'Worker Exception:' . "\n" . WorkerPoolResult::indentExceptionString($this->getWorkerException()) . "\n" 161 | ; 162 | } 163 | 164 | ob_start(); 165 | var_dump($this->offsetGet('data')); 166 | $content = trim(ob_get_contents()); 167 | ob_end_clean(); 168 | 169 | // return data 170 | return 171 | 'PID: ' . $this->offsetGet('pid') . "\n" . 172 | 'Data:' . "\n$content\n" 173 | ; 174 | 175 | } 176 | 177 | /** 178 | * Indents the exception string 179 | * @param string $string the exception string 180 | * @return string the indented exception string 181 | */ 182 | protected static function indentExceptionString(string $str) : string { 183 | $indent = " "; 184 | $str = $indent . str_replace("\n", "\n$indent", str_replace("\r", "", $str)); 185 | if(substr($str, -strlen($indent)) == $indent) { 186 | $str = substr($str, 0, -strlen($indent)); 187 | } 188 | return $str; 189 | } 190 | 191 | /** 192 | * Get the object as a human readable string 193 | * @return string the object as a human redable string 194 | */ 195 | public function __toString() : string { 196 | if($this->hasPoolException()) { 197 | // return pool exception 198 | return 199 | 'PID: ' . $this->offsetGet('pid') . "\n" . 200 | 'Pool Exception:' . "\n" . WorkerPoolResult::indentExceptionString($this->getPoolException()) . "\n" 201 | ; 202 | } 203 | elseif($this->hasWorkerException()) { 204 | // return worker exception 205 | return 206 | 'PID: ' . $this->offsetGet('pid') . "\n" . 207 | 'Worker Exception:' . "\n" . WorkerPoolResult::indentExceptionString($this->getWorkerException()) . "\n" 208 | ; 209 | } 210 | // return data 211 | return 212 | 'PID: ' . $this->offsetGet('pid') . "\n" . 213 | 'Data:' . "\n" . print_r($this->offsetGet('data'), TRUE) . "\n" 214 | ; 215 | } 216 | } 217 | 218 | -------------------------------------------------------------------------------- /tests/ClosureWorkerTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('QXS\WorkerPool\Semaphore', $semaphore); 24 | $that->assertInstanceOf('ArrayObject', $storage); 25 | return $input; 26 | }, 27 | function ($semaphore, $storage) use ($that, &$createRun) { 28 | $createRun = TRUE; 29 | $that->assertInstanceOf('QXS\WorkerPool\Semaphore', $semaphore); 30 | $that->assertInstanceOf('ArrayObject', $storage); 31 | }, 32 | function ($semaphore, $storage) use ($that, &$destroyRun) { 33 | $destroyRun = TRUE; 34 | $that->assertInstanceOf('QXS\WorkerPool\Semaphore', $semaphore); 35 | $that->assertInstanceOf('ArrayObject', $storage); 36 | } 37 | ); 38 | 39 | $worker->onProcessCreate($semaphore); 40 | $this->assertTrue( 41 | $createRun, 42 | 'Worker::onProcessCreate should call the create Closure.' 43 | ); 44 | $this->assertEquals( 45 | 1, 46 | $worker->run(1), 47 | 'Worker::run should return the same value.' 48 | ); 49 | $this->assertTrue( 50 | $runRun, 51 | 'Worker::run should call the run Closure.' 52 | ); 53 | $worker->onProcessDestroy(); 54 | $this->assertTrue( 55 | $destroyRun, 56 | 'Worker::onProcessDestroy should call the destroy Closure.' 57 | ); 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /tests/Fixtures/FatalFailingWorker.php: -------------------------------------------------------------------------------- 1 | abc(); // fatal error 17 | return "Hi $input"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Fixtures/PingWorker.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('The following extension are missing: ' . implode(', ', $missingExtensions)); 27 | } 28 | $this->sut = new WorkerPool(); 29 | } 30 | 31 | public function testFatalFailingWorker() { 32 | $this->markTestSkipped('Failing workers get respawned now.'); 33 | $exceptionMsg = NULL; 34 | $exception = NULL; 35 | $wp = new WorkerPool(); 36 | $wp->setWorkerPoolSize(10); 37 | $wp->create(new Fixtures\FatalFailingWorker()); 38 | try { 39 | for ($i = 0; $i < 20; $i++) { 40 | $wp->run($i); 41 | } 42 | } catch (\Exception $e) { 43 | $exceptionMsg = $e->getMessage(); 44 | $exception = $e; 45 | } 46 | $this->assertInstanceOf('QXS\WorkerPool\WorkerPoolException', $exception); 47 | $this->assertEquals( 48 | 'Unable to run the task.', 49 | $exceptionMsg, 50 | 'We have a wrong Exception Message.' 51 | ); 52 | $wp->destroy(); 53 | } 54 | 55 | public function testGetters() { 56 | $wp = new WorkerPool(); 57 | $wp->create(new Fixtures\PingWorker()); 58 | $this->assertTrue( 59 | is_int($wp->getWorkerPoolSize()), 60 | 'getWorkerPoolSize should return an int' 61 | ); 62 | $this->assertTrue( 63 | is_string($wp->getChildProcessTitleFormat()), 64 | 'getChildProcessTitleFormat should return a string' 65 | ); 66 | $this->assertTrue( 67 | is_string($wp->getParentProcessTitleFormat()), 68 | 'getParentProcessTitleFormat should return a string' 69 | ); 70 | $wp->destroy(); 71 | } 72 | 73 | public function testSetters() { 74 | $wp = new WorkerPool(); 75 | try { 76 | $wp->setWorkerPoolSize(5); 77 | } catch (\Exception $e) { 78 | $this->assertTrue( 79 | FALSE, 80 | 'setWorkerPoolSize shouldn\'t throw an exception.' 81 | ); 82 | } 83 | try { 84 | $wp->setChildProcessTitleFormat('X %basename% %class% Child %i% X'); 85 | } catch (\Exception $e) { 86 | $this->assertTrue( 87 | FALSE, 88 | 'setChildProcessTitleFormat shouldn\'t throw an exception.' 89 | ); 90 | } 91 | try { 92 | $wp->setParentProcessTitleFormat('X %basename% %class% Parent X'); 93 | } catch (\Exception $e) { 94 | $this->assertTrue( 95 | FALSE, 96 | 'setParentProcessTitleFormat shouldn\'t throw an exception.' 97 | ); 98 | } 99 | $this->assertEquals( 100 | 5, 101 | $wp->getWorkerPoolSize(), 102 | 'getWorkerPoolSize should return an int' 103 | ); 104 | $this->assertEquals( 105 | 'X %basename% %class% Child %i% X', 106 | $wp->getChildProcessTitleFormat(), 107 | 'getChildProcessTitleFormat should return a string' 108 | ); 109 | $this->assertEquals( 110 | 'X %basename% %class% Parent X', 111 | $wp->getParentProcessTitleFormat(), 112 | 'getParentProcessTitleFormat should return a string' 113 | ); 114 | 115 | $wp->create(new Fixtures\PingWorker()); 116 | 117 | try { 118 | $wp->setWorkerPoolSize(5); 119 | $this->assertTrue( 120 | FALSE, 121 | 'setWorkerPoolSize should throw an exception for a created pool.' 122 | ); 123 | } catch (\Exception $e) { 124 | } 125 | try { 126 | $wp->setChildProcessTitleFormat('%basename% %class% Child %i%'); 127 | $this->assertTrue( 128 | FALSE, 129 | 'setChildProcessTitleFormat should throw an exception for a created pool.' 130 | ); 131 | } catch (\Exception $e) { 132 | } 133 | try { 134 | $wp->setParentProcessTitleFormat('%basename% %class% Parent'); 135 | $this->assertTrue( 136 | FALSE, 137 | 'setParentProcessTitleFormat should throw an exception for a created pool.' 138 | ); 139 | } catch (\Exception $e) { 140 | } 141 | 142 | $wp->destroy(); 143 | } 144 | 145 | public function testDestroyException() { 146 | $wp = new WorkerPool(); 147 | $wp->setWorkerPoolSize(3); 148 | $wp->create(new Fixtures\FatalFailingWorker()); 149 | $failCount = 0; 150 | try { 151 | for ($i = 0; $i < 3; $i++) { 152 | $wp->run($i); 153 | $a = $wp->getFreeAndBusyWorkers(); 154 | } 155 | } catch (\Exception $e) { 156 | $this->assertTrue( 157 | FALSE, 158 | 'An unexpected exception was thrown.' 159 | ); 160 | } 161 | $result = TRUE; 162 | try { 163 | $wp->destroy(); 164 | } catch (\Exception $e) { 165 | $result = FALSE; 166 | } 167 | $this->assertTrue( 168 | $result, 169 | 'WorkerPool::Destroy shouldn\t throw an exception.' 170 | ); 171 | } 172 | 173 | public function testPingWorkers() { 174 | try { 175 | $wp = new WorkerPool(); 176 | $wp->setWorkerPoolSize(5); 177 | $wp->create(new Fixtures\PingWorker()); 178 | $failCount = 0; 179 | for ($i = 0; $i < 50; $i++) { 180 | $wp->run($i); 181 | $a = $wp->getFreeAndBusyWorkers(); 182 | if ($a['free'] + $a['busy'] != $a['total']) { 183 | $failCount++; 184 | } 185 | } 186 | $wp->waitForAllWorkers(); 187 | $this->assertLessThanOrEqual( 188 | 0, 189 | $failCount, 190 | 'Sometimes the sum of free and busy workers does not equal to the pool size.' 191 | ); 192 | $this->assertEquals( 193 | 50, 194 | count($wp), 195 | 'The result count should be 50.' 196 | ); 197 | $i = 0; 198 | foreach ($wp as $val) { 199 | $i++; 200 | } 201 | $this->assertEquals( 202 | 50, 203 | $i, 204 | 'We should have 50 results in the pool.' 205 | ); 206 | $this->assertEquals( 207 | 0, 208 | count($wp), 209 | 'The result count should be 0 now.' 210 | ); 211 | } catch (\Exception $e) { 212 | $this->assertTrue( 213 | FALSE, 214 | 'An unexpected exception was thrown.' 215 | ); 216 | } 217 | 218 | try { 219 | $wp->destroy(); 220 | } catch (\Exception $e) { 221 | $this->assertTrue( 222 | FALSE, 223 | 'WorkerPool::Destroy shouldn\t throw an exception of type ' . get_class($e) . ' with message:' . $e->getMessage() . "\n" . $e->getTraceAsString() 224 | ); 225 | } 226 | } 227 | } 228 | 229 | -------------------------------------------------------------------------------- /travisphp.ini: -------------------------------------------------------------------------------- 1 | extension=sockets.so 2 | extension=posix.so 3 | extension=sysvsem.so 4 | extension=pcntl.so 5 | --------------------------------------------------------------------------------