Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
074f5da
feat: add LaunchSpec DTO
Flytachi Jul 2, 2026
61a5cfd
temp
Flytachi Jul 2, 2026
9acea9f
feat: add PayloadTransport contract and StagedPayload DTO
Flytachi Jul 2, 2026
40b26ad
feat: add PipeTransport
Flytachi Jul 2, 2026
aa5e331
feat: add TempFileTransport
Flytachi Jul 2, 2026
b0d02f5
feat: add ShmTransport
Flytachi Jul 2, 2026
b05ba80
feat: add ProcessHandle and Launcher interface
Flytachi Jul 2, 2026
bbacb35
feat: add Runner interface, ProcessRunner, and Engine interface
Flytachi Jul 2, 2026
65ba229
feat: add CliLauncher
Flytachi Jul 2, 2026
54c4da4
feat: add AdaptiveEngine and ManualEngine with injectable Launcher
Flytachi Jul 2, 2026
3aeb8d0
style: normalize file headers per phpcs
Flytachi Jul 2, 2026
aef82f5
featv2 Thread facade over Engine; wExecutor -> wRunner, PHP >=8.4
Flytachi Jul 2, 2026
7dd2d49
test: restructure into base/working suites + group excludes (winter-k…
Flytachi Jul 2, 2026
54e3136
test: working scenario matrix (transports, pool reap-loop, detached) …
Flytachi Jul 2, 2026
cd298c6
test: containerized runner (Dockerfile + compose + run-container.sh)
Flytachi Jul 2, 2026
0041b03
fix
Flytachi Jul 2, 2026
26a88a8
test: containerized leak/security suite (cli/fpm/swoole) + testsuite-…
Flytachi Jul 2, 2026
06a625f
test: startup-overhead timing regression detector
Flytachi Jul 2, 2026
a0768a5
test: nested Thread-in-Thread + diverse parallel battle-run workloads
Flytachi Jul 2, 2026
d21d679
feat: fool-proofing guards + fault-tolerance & swoole-scenario tests,…
Flytachi Jul 2, 2026
f01572a
fix
Flytachi Jul 2, 2026
d350f51
fix tests
Flytachi Jul 2, 2026
facad91
ci: cache docker build (gha); test: large-payload + concurrency-stres…
Flytachi Jul 2, 2026
6830a0f
test: report worker RSS footprint + stress summary to console
Flytachi Jul 2, 2026
02c1dc2
test: concurrency scaling stress (40/100 at-once, 300 pooled) with th…
Flytachi Jul 2, 2026
572ac93
test: heavy-vs-lean worker comparison (RSS + throughput) printed to c…
Flytachi Jul 2, 2026
8b91b82
perf: free the serialized payload after deserialization (lowers peak …
Flytachi Jul 2, 2026
75af83f
security: escapeshellarg transport cliArgs (defense-in-depth for comm…
Flytachi Jul 2, 2026
dfa5f83
Aikido bag helped - fix
Flytachi Jul 2, 2026
588bc1b
docs: thorough PHPDoc on interfaces, DTOs, transports and engines
Flytachi Jul 2, 2026
f4778e4
docs: full v2 documentation rebuild (12 chapters) + README repositioning
Flytachi Jul 2, 2026
f138413
refactor code to improve readability
Flytachi Jul 2, 2026
f10e7e0
chore: update composer description and bump PHP_CodeSniffer to ^4.0
Flytachi Jul 2, 2026
a54712b
docs: add CI status badge to README
Flytachi Jul 2, 2026
9e31944
docs: redoc
Flytachi Jul 2, 2026
c5dd3a6
docs: redoc
Flytachi Jul 2, 2026
e8cc693
refactor: decouple Runner
Flytachi Jul 2, 2026
074ceca
docs: expand process handling, transport customization, and usage cav…
Flytachi Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: CI

on:
push:
pull_request:

jobs:
default:
name: Default suite (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.4', '8.5']
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ matrix.php }}
extensions: pcntl, posix, shmop
coverage: none

- name: Install dependencies
run: composer install --no-interaction --no-progress

- name: Code style
run: composer cs-check

- name: Default suite (base + working)
run: composer test

container:
name: Container suite (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.4', '8.5']
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0

- name: Build test image (cached; pcntl/posix/shmop/swoole)
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: tests/docker/Dockerfile
build-args: PHP_VERSION=${{ matrix.php }}
tags: wt-test:${{ matrix.php }}
load: true
cache-from: type=gha,scope=wt-${{ matrix.php }}
cache-to: type=gha,mode=max,scope=wt-${{ matrix.php }}

- name: Run default + container suites
run: |
docker run --init --rm -v "$PWD":/app -w /app wt-test:${{ matrix.php }} bash -lc '
composer install --no-interaction --no-progress
composer test
vendor/bin/phpunit --testsuite container
'
148 changes: 106 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,44 @@
# Winter Thread: A Modern Process Control Library for PHP

[![Tests](https://img.shields.io/github/actions/workflow/status/flytachi/winter-thread/ci.yml?branch=main&label=tests)](https://github.com/flytachi/winter-thread/actions/workflows/ci.yml)
[![Latest Version on Packagist](https://img.shields.io/packagist/v/flytachi/winter-thread.svg)](https://packagist.org/packages/flytachi/winter-thread)
[![PHP Version Require](https://img.shields.io/packagist/php-v/flytachi/winter-thread.svg?style=flat-square)](https://packagist.org/packages/flytachi/winter-thread)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE)

**Winter Thread** provides a clean, object-oriented API for running and controlling background processes in PHP, simulating a traditional threading model for parallel and long-running tasks.
**Winter Thread** is a *process engine* for PHP: a clean, object-oriented,
Java-like API for running and controlling background tasks as isolated OS
processes — for parallel and long-running work.

It abstracts away the complexities of `proc_open` and POSIX signals into a powerful and easy-to-use interface.
**No heavy extensions.** Unlike `pthreads`, `ext-parallel`, or Swoole, it needs
no ZTS build and no exotic runtime — just `proc_open` and the standard POSIX
extensions (`ext-pcntl`, `ext-posix`) that ship with nearly every PHP install.
Each task runs in a fresh, isolated PHP process, so there is no shared state to
corrupt and no inherited connections to break.

It is deliberately a low-level **engine** — a small, dependable core to build
pools, queues and schedulers on — that abstracts away `proc_open` and POSIX
signals behind a friendly API.

## Key Features

- **No heavy extensions**: No swoole / parallel / pthreads, no ZTS build — just `proc_open` + standard POSIX. Runs on a normal PHP install.
- **Clean process isolation**: Each task runs in a brand-new PHP process — no inherited DB connections, sockets, or global state to corrupt.
- **Fluent, Object-Oriented API**: Manage background processes as objects.
- **Full Process Control**: `start()`, `join()`, `pause()`, `resume()`, `terminate()`, and `kill()`.
- **Advanced Process Naming**: Identify your processes easily with namespaces, names, and tags.
- **Safe by Default**: Output goes to `/dev/null` by default — no Broken pipe risk for fire-and-forget jobs.
- **Swoole / Event-Loop Compatible**: Three payload delivery modes (`PIPE`, `TEMP_FILE`, `SHM`) to avoid fd corruption under `SWOOLE_HOOK_ALL`.
- **Extensible**: Easily override the runner script for deep framework integration.
- **Swoole / Event-Loop Compatible**: Pluggable payload transports (pipe, temp-file, shared-memory); the default engine auto-detects an active Swoole runtime and avoids fd corruption under `SWOOLE_HOOK_ALL`.
- **Zombie-free fire-and-forget**: Optional detached mode (`fork` + `setsid`) reparents workers to init, so long-lived parents (FPM, daemons) never accumulate zombies.
- **Pluggable Engine**: Swap transport, launcher, or child-side runner through a single `Engine` — build custom backends (Docker, SSH, …) without touching `Thread`.
- **Java-like API**: Familiar method names like `isAlive()` and `join()` for an easy learning curve.

## Requirements

- PHP >= 8.1
- PHP >= 8.4
- `ext-pcntl`
- `ext-posix`
- `opis/closure` ^4.5 (required; enables safe serialization of anonymous classes and closures)
- `ext-shmop` (optional; only for the shared-memory transport)

## Installation

Expand Down Expand Up @@ -76,39 +91,58 @@ $exitCode = $thread->join();
echo "Task finished with exit code: $exitCode\n";
```

## Swoole / Event-Loop Compatibility

Under **Swoole** with `SWOOLE_HOOK_ALL`, stdin pipes created by `proc_open` are intercepted
and their file descriptors leak into Swoole's internal table, causing `Bad file descriptor`
errors on subsequent requests.
## Configuration — the Engine

Configure the payload mode **once at bootstrap** to avoid pipe fds entirely:
All configuration goes through a single `Engine`, bound **once at bootstrap** with
`Thread::bindEngine()`. When you bind nothing, the **`AdaptiveEngine`** is used and
self-configures for the current environment (CLI / FPM / Swoole).

```php
use Flytachi\Winter\Thread\Thread;

// bootstrap.php — loaded by every Swoole worker process
if (extension_loaded('swoole') && \Swoole\Coroutine::getCid() !== -1) {
// PAYLOAD_TEMP_FILE: payload written to a temp file, unlinked immediately — no ext needed
// PAYLOAD_SHM: payload in shared memory (RAM only) — requires ext-shmop
Thread::bindPayloadMode(
extension_loaded('shmop')
? Thread::PAYLOAD_SHM
: Thread::PAYLOAD_TEMP_FILE
);
}
use Flytachi\Winter\Thread\Engine\ManualEngine;
use Flytachi\Winter\Thread\Payload\TempFileTransport;

// Zero-config: AdaptiveEngine is the default — nothing to do.
$thread = new Thread(new MyTask());
$thread->start();

// Explicit configuration when you need it:
Thread::bindEngine(
(new ManualEngine())
->withTransport(new TempFileTransport())
->withBinaryPath('/usr/bin/php')
->withRunnerPath(__DIR__ . '/vendor/flytachi/winter-thread/wRunner')
->withSecurity('your-signing-secret') // signs serialized closures
->withLauncher(new MyCustomLauncher()) // optional: custom backend
);
```

After that, `Thread::start()` works identically — no other code changes needed.
### Swoole / Event-Loop Compatibility

Under **Swoole** with `SWOOLE_HOOK_ALL`, stdin pipes created by `proc_open` are intercepted
and their file descriptors leak into Swoole's internal table, causing `Bad file descriptor`
errors. The `AdaptiveEngine` **detects an active Swoole runtime automatically** and switches
to a file/shared-memory transport, so no configuration is needed. Under Swoole, also prefer
file output over `outputTarget: null` (the output pipes are subject to the same hooks).

| Mode | Delivery | Parent pipe fd | Requires |
| Transport | Delivery | Parent pipe fd | Requires |
|---|---|---|---|
| `PAYLOAD_PIPE` | stdin pipe (default) | yes | — |
| `PAYLOAD_TEMP_FILE` | temp file as stdin | **none** | — |
| `PAYLOAD_SHM` | shared memory | **none** | `ext-shmop` |
| `PipeTransport` | stdin pipe (default in CLI) | yes | — |
| `TempFileTransport` | temp file as stdin | **none** | — |
| `ShmTransport` | shared memory | **none** | `ext-shmop` |

## Detached (zombie-free) fire-and-forget

For a long-lived parent (FPM worker, daemon) that dispatches background tasks and never
joins them, pass `detached: true`. The launcher exits immediately and the real worker is
reparented to init (`pid 1`), so no zombie ever accumulates under the parent:

See [6. Payload Modes](docs/06-payload-modes.md) for full details, security notes, and the
isolation guarantee for concurrent calls.
```php
$thread = new Thread(new SendEmailBatch($ids));
$thread->start(detached: true); // returns at once; worker owned by init
```

Signal control still works via the worker's self-reported PID (write `getmypid()` from
inside the task to your own store), since the engine's control model is PID-based.

---

Expand Down Expand Up @@ -136,28 +170,58 @@ $thread->isAlive(); // bool — check if still running

## Running Tests

Tests come in two tiers (mirroring the `winter-kernel` layout):

**Default** — runs on any machine; unsupported extensions self-skip:

```bash
composer install
composer test
# or with human-readable output:
composer test-detail
composer test # base (class correctness) + working (scenarios)
composer test-base # only unit-level class correctness
composer test-working # only end-to-end scenarios
composer test-detail # human-readable (testdox) output
```

## Documentation
**Containered** — heavy, environment-specific checks (leak / timing / nested / battle-run,
with Cli / FPM / Swoole), run inside Docker across a list of PHP versions:

Full documentation is in the [/docs](docs) directory:
```bash
tests/run-container.sh # default versions: 8.4 8.5
tests/run-container.sh 8.4 # a single version
tests/run-container.sh 8.4 8.5 8.6 # a custom list

- [1. Introduction](docs/01-introduction.md)
- [2. Installation and Requirements](docs/02-installation-and-requirements.md)
- [3. Basic Usage](docs/03-basic-usage.md)
- [4. Debugging and Output Handling](docs/04-debugging-and-output.md)
- [5. API Reference](docs/05-api-reference.md)
- [6. Payload Modes (Swoole / Event-Loop Compatibility)](docs/06-payload-modes.md)
# Or, inside an environment that already has swoole/shmop:
composer test-container # phpunit --testsuite container
```

CI (`.github/workflows/ci.yml`) runs the default suite via `setup-php` and the container
suite via the bundled `tests/docker/Dockerfile`, on a PHP 8.4 / 8.5 matrix.

## Documentation

Full documentation lives in [`/docs`](docs/README.md):

1. [Introduction](docs/01-introduction.md) — philosophy, the no-heavy-ext story, when to use it
2. [Installation & Requirements](docs/02-installation-and-requirements.md)
3. [Quickstart](docs/03-quickstart.md) — a complete parallel example in 5 minutes
4. [Basic Usage](docs/04-basic-usage.md)
5. [Output & Debugging](docs/05-output-and-debugging.md)
6. [Process Control & Lifecycle](docs/06-process-control.md) — signals, graceful shutdown
7. [The Engine](docs/07-the-engine.md)
8. [Payload Transports](docs/08-payload-transports.md)
9. [Detached Mode](docs/09-detached-mode.md)
10. [Security](docs/10-security.md)
11. [Architecture & Internals](docs/11-architecture.md)
12. [Patterns](docs/12-patterns.md) — pools, returning results, retries
13. [Troubleshooting](docs/13-troubleshooting.md)
14. [API Reference](docs/14-api-reference.md)
15. [Testing](docs/15-testing.md)

## Contributing

Contributions are welcome! Please submit a pull request or open an issue for bugs, questions, or feature requests.


## License

This library is open-source software licensed under the [MIT license](LICENSE).
This library is open-source software licensed under the [MIT license](LICENSE).
13 changes: 8 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{
"name": "flytachi/winter-thread",
"description": "Object-oriented background process control for PHP — a Java-like threading model over OS processes.",
"description": "A lightweight process engine for PHP — Java-like control of background tasks as isolated OS processes, without heavy extensions (no swoole/pthreads/parallel).",
"type": "library",
"license": "MIT",
"scripts": {
"test": "phpunit",
"test-detail": "phpunit --testdox",
"test-base": "phpunit --testsuite base",
"test-working": "phpunit --testsuite working",
"test-container": "phpunit --testsuite container",
"cs-check": "phpcs .",
"cs-fix": "phpcbf ."
},
Expand All @@ -19,19 +22,19 @@
"Flytachi\\Winter\\Thread\\Tests\\": "tests/"
}
},
"bin": ["wExecutor"],
"bin": ["wRunner"],
"require": {
"php": ">=8.1",
"php": ">=8.4",
"ext-pcntl": "*",
"ext-posix": "*",
"opis/closure": "^4.5"
},
"require-dev": {
"phpunit/phpunit": "^13.1",
"squizlabs/php_codesniffer": "@stable"
"squizlabs/php_codesniffer": "^4.0"
},
"suggest": {
"ext-shmop": "Required for Thread::PAYLOAD_SHM mode (Swoole-compatible shared memory payload delivery)"
"ext-shmop": "Required for ShmTransport (Swoole-compatible shared-memory payload delivery)"
},
"homepage": "https://winterframe.net",
"support": {
Expand Down
Loading
Loading