Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
d3e9b2c
#98: Implement Async\spawn_thread() and Thread class
EdmondDantes Mar 15, 2026
f4822a9
#98: Simplify thread snapshot to closure-only transfer with zval copy…
EdmondDantes Mar 17, 2026
72edeb9
#98: Refactor thread memory management: rename allocs to persistent_p…
EdmondDantes Mar 19, 2026
ff747af
#98: Refactor async thread event handling to use zend_fcall_t for ent…
EdmondDantes Mar 20, 2026
af50216
#98: Refactor thread copy context to use a persistent map for dedupli…
EdmondDantes Mar 20, 2026
ca52a09
#98: Skip autoloader capture when bootloader is provided
EdmondDantes Mar 20, 2026
6ad4f99
#98: Remove autoloader capture from snapshot
EdmondDantes Mar 20, 2026
853f959
#98: Fix thread lifecycle bugs and improve code quality
EdmondDantes Mar 21, 2026
e3241c5
#98: Fix compilation errors in libuv_reactor.c
EdmondDantes Mar 21, 2026
d9bb6a9
#98: Fix missing closing brace in bootloader exception check
EdmondDantes Mar 21, 2026
d982e5f
#98: Decouple reactor from thread lifecycle via API function pointers
EdmondDantes Mar 21, 2026
152d53b
#98: Fix snapshot destroy crash and cleanup dead code
EdmondDantes Mar 22, 2026
e39c6a4
#98: Fix memory leaks in thread closure and script name
EdmondDantes Mar 22, 2026
c331e72
#98: Renumber thread tests to sequential 001-009
EdmondDantes Mar 22, 2026
e50e2fe
#98: Simplify object transfer and move result loading to thread module
EdmondDantes Mar 22, 2026
c180efd
#98: Add RemoteException and ThreadTransferException for thread safety
EdmondDantes Mar 22, 2026
1f3152b
#98: Fix exception handling in child threads, add bailout recovery
EdmondDantes Mar 23, 2026
6327b80
#98: Thread exception handling via API, new test coverage
EdmondDantes Mar 23, 2026
4d078cf
#98: Move snapshot destroy to child thread for earlier cleanup
EdmondDantes Mar 23, 2026
3cf186b
#98: Fix always_inline on mutually recursive thread_release functions
EdmondDantes Mar 23, 2026
e786be9
#98: Fix use-after-free in thread_copy_op_array_ex and thread_copy_at…
EdmondDantes Mar 23, 2026
d985394
#98: Fix double-free in thread transfer deduplication
EdmondDantes Mar 23, 2026
ec5196c
#98: Replace per-allocation pemalloc with bump arena for op_array copy
EdmondDantes Mar 23, 2026
b9135e8
#98: Move snapshot destroy after request shutdown
EdmondDantes Mar 23, 2026
e5ad962
#98: Add thread tests for functions, static vars, stress, exceptions,…
EdmondDantes Mar 23, 2026
d8e1f22
#98: Fix test 033 RemoteException API and simplify test 035
EdmondDantes Mar 23, 2026
ae6dff9
#98: Force $_SERVER and $_ENV initialization in child thread
EdmondDantes Mar 23, 2026
156c8d3
#98: Add ThreadChannel skeleton — thread-safe channel for cross-threa…
EdmondDantes Mar 25, 2026
8040505
Merge branch 'main' into 98-thread-pool
EdmondDantes Mar 31, 2026
2f9b659
#98: ThreadChannel — add trigger events for back-pressure, fix event_…
EdmondDantes Apr 1, 2026
bc69a54
#98: Add ThreadChannel tests — construct, send/recv, close, state, types
EdmondDantes Apr 1, 2026
e5230f1
#98: Add cross-thread ThreadChannel tests (currently failing — no tra…
EdmondDantes Apr 1, 2026
d40ecfe
Merge branch 'main' into 98-thread-pool
EdmondDantes Apr 5, 2026
560793a
#98: Implement transfer_obj for ThreadChannel cross-thread transfer
EdmondDantes Apr 5, 2026
a30c578
#98: Fix event loop hang on ThreadChannel by fixing trigger lifecycle
EdmondDantes Apr 6, 2026
9874944
#98: Add ThreadChannel edge case tests (019-036)
EdmondDantes Apr 6, 2026
93d8360
#98: Add shared future state for cross-thread future bridging + Threa…
EdmondDantes Apr 7, 2026
04e770b
#98: Fix shared state memory leaks and ownership semantics
EdmondDantes Apr 7, 2026
f1a71fd
#98: Forbid FutureState transfer to multiple threads
EdmondDantes Apr 7, 2026
5e6c40a
#98: Add remote future edge case tests + fix isCompleted() for remote…
EdmondDantes Apr 7, 2026
5dbe68f
#98: Add C-level thread entry point + ThreadPool test suite
EdmondDantes Apr 7, 2026
1f98acf
#98: ThreadPool infrastructure — internal_entry, channel API, worker …
EdmondDantes Apr 7, 2026
322d92d
Implement C-level send/receive/close on ThreadChannel
EdmondDantes Apr 9, 2026
e2e669c
Implement ThreadPool worker loop and submit via channel
EdmondDantes Apr 9, 2026
79ee0a6
Allow internal_entry threads to run without snapshot
EdmondDantes Apr 9, 2026
9c46cd1
Add Closure transfer_obj for cross-thread closure transfer
EdmondDantes Apr 9, 2026
9dea5ce
Fix ThreadPool worker lifecycle and interned strings race
EdmondDantes Apr 10, 2026
758d1dc
#98: Add const qualifiers and propagate bailout in thread_pool
EdmondDantes Apr 10, 2026
6d995d5
#98: Add event.dispose to thread channel, fix channel memory leak in …
EdmondDantes Apr 10, 2026
17e837b
#98: Refactor thread pool lifecycle and implement map()
EdmondDantes Apr 10, 2026
eaa9478
#98: Extract zend_async_thread_pool_t base struct into Zend async API
EdmondDantes Apr 10, 2026
abff2eb
#98: + zend_always_inline
EdmondDantes Apr 10, 2026
3e6cece
#98: Remove drain from pool vtable, drop mutex from drain, remove dra…
EdmondDantes Apr 10, 2026
1fa74d5
#98: Quiesce child threads before module shutdown + cancel rejects ba…
EdmondDantes Apr 13, 2026
88a7f36
#98: CHANGELOG entry for 0.7.0
EdmondDantes Apr 13, 2026
363bf94
#98: Share transfer ctx across bound vars (identity fix)
EdmondDantes Apr 13, 2026
f611426
#98: Wire thread transfer helpers for Zend core handlers
EdmondDantes Apr 13, 2026
033887b
Merge branch 'main' into 98-thread-pool
EdmondDantes Apr 14, 2026
7af3e59
#98: Port thread_channel and future shared-state to Windows
EdmondDantes Apr 14, 2026
f371ca3
#98: Hoist retval declaration in async_thread_run past goto cleanup
EdmondDantes Apr 14, 2026
9f56fb0
#98: Add scope coverage tests targeting uncovered branches
EdmondDantes Apr 15, 2026
bafb9f6
#98: Add exceptions coverage tests
EdmondDantes Apr 15, 2026
9fb1f30
#98: Add pool coverage tests for healthcheck and error paths
EdmondDantes Apr 15, 2026
cc5ea17
#98: Add coverage tests for thread, task_group and context
EdmondDantes Apr 15, 2026
e065c89
#98: Add async.c coverage tests for Timeout, signal, graceful_shutdow…
EdmondDantes Apr 15, 2026
a88ffc1
#98: Add COVERAGE_REPORT.md wrapping up the gcov test pass
EdmondDantes Apr 15, 2026
3bd11b2
#98: Fix three defects surfaced by coverage-report session
EdmondDantes Apr 15, 2026
28fbc3e
#98: Fix Async\Timeout::cancel() double-release of backing object
EdmondDantes Apr 15, 2026
3c8115c
#98: Coverage phase 2 — future.c 80.38% → 85.80%
EdmondDantes Apr 15, 2026
460db67
#98: Coverage phase 2 — async.c 84.49% → 86.07%
EdmondDantes Apr 15, 2026
bdf8cf7
#98: Coverage phase 2 — task_group.c 84.01% → 86.18%
EdmondDantes Apr 15, 2026
8a5b11e
#98: Coverage phase 2 — channel.c 86.70% → 89.00%
EdmondDantes Apr 15, 2026
07dad72
#98: Coverage phase 2 — targets #5-#8 (thread, fs_watcher, thread_poo…
EdmondDantes Apr 15, 2026
986752a
#98: Fix two latent segfaults surfaced by coverage phase 2
EdmondDantes Apr 15, 2026
890f802
#98: update proc tests
EdmondDantes Apr 15, 2026
ea1a57a
#98: update proc tests2
EdmondDantes Apr 15, 2026
ea351da
#98: Thread::finally() uses spawn-time parent scope instead of main s…
EdmondDantes Apr 15, 2026
08053aa
#98: CHANGELOG for 0.7.0 — coverage phase 2 fixes and new tests
EdmondDantes Apr 15, 2026
1a9a576
#98: Fix UAF when snapshot creation throws during spawn_thread
EdmondDantes Apr 15, 2026
152eddb
#98: Clear waiter->group on detach to avoid UAF from sync-settled paths
EdmondDantes Apr 15, 2026
8f4ba66
#98: Allocate opcodes+literals contiguously in op_array_to_emalloc
EdmondDantes Apr 15, 2026
31023ad
#98: Reset opcode handlers in thread op_array copy to strip JIT stubs
EdmondDantes Apr 15, 2026
7dd8394
#98: Fix memory leak in thread transfer error path
EdmondDantes Apr 16, 2026
d520904
#98: Fix map() on remote future using Scheduler scope instead of subs…
EdmondDantes Apr 16, 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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ All notable changes to the Async extension for PHP will be documented in this fi
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.7.0] -

### Added
- **`Async\ThreadPool`** (new class): pool of OS threads for executing PHP closures. `submit($callable, ...$args): Future`, `map(array $items, $callable): array`, `close()` (graceful), `cancel()` (rejects backlog with `Async\CancellationException`, running tasks still finish), `isClosed()`, `getWorkerCount()`, `getPendingCount()`, `getRunningCount()`. Implements `Countable`. Constructor `new ThreadPool(int $workers, int $queue_size = 0)`; queue is a thread-safe channel that suspends the submitting coroutine when full (backpressure).
- **`Async\ThreadPoolException`** (new class): thrown from `submit()` / `map()` when the pool is closed.
- **`Async\ThreadChannel`** (new class): thread-safe channel for transferring zvals between threads via deep-copy snapshot. `send()` / `receive()` suspend the calling coroutine instead of blocking the OS thread. Closures, including those with bound variables, transfer correctly through the snapshot machinery.
- **`Async\ThreadChannelException`** (new class).
- **Coverage phase 2** — targeted tests for `future.c`, `async.c`, `task_group.c`, `channel.c`, `thread.c`, `thread_pool.c`, `context.c`, `pool.c`. Aggregate ext/async coverage went from 77.45% to 78.34% lines (+104 lines) and from 88% to 89.1% functions (+10 functions). New tests cover Future status/cancel/getAwaitingInfo methods, FutureState double-resolve errors, finally() exception-chain propagation, non-callable argument rejection on map/catch/finally, TaskGroup synchronous-settled paths for `all()`/`race()`/`any()`, Channel unbuffered-iterator and foreach-by-ref branches, `Async\timeout(0)` ValueError, `Async\delay(0)` fast path, `Async\current_coroutine()` out-of-coroutine error, and `Context::get()` missing-key fallback. See `COVERAGE_PROGRESS.md` for the per-target breakdown.

### Fixed
- **`Async\Timeout::cancel()` double-released the backing object**: Calling `$t->cancel()` disposed the backing timer event, whose `async_timeout_event_dispose()` unconditionally ran `OBJ_RELEASE(object)` assuming the event held a counted reference. In the current architecture the event only stores a raw pointer (`async_timeout_ext_t::std`) without a matching `GC_ADDREF` at creation time, so the release actually decremented the caller's live refcount. The backing object was freed while the userland `$t` variable still pointed to it, and shutdown tripped `IS_OBJ_VALID(object_buckets[handle])` in `zend_objects_store_del()`. Fixed by mirroring `async_timeout_destroy_object()`: `cancel()` now clears `timeout_ext->std` before dispatching the dispose so `async_timeout_event_dispose()` sees a NULL `std` and skips the stray release.
- **`pool_strategy_report_failure()` captured a dangling exception pointer**: When no caller-provided error was available, the helper created a fresh `Exception` via `zend_throw_exception(NULL, "Resource validation failed", 0)` followed immediately by `zend_clear_exception()`. The throw set `EG(exception)` to a refcount-1 object; `clear_exception()` dropped that reference, freeing the exception. The subsequent `ZVAL_OBJ(&error_zval, ex)` captured a dangling pointer that was then handed to the userland `reportFailure()` handler, producing `zend_mm_heap corrupted` and SIGSEGV at shutdown on ZTS DEBUG. Fixed by constructing the exception directly via `object_init_ex(zend_ce_exception)` + `zend_update_property_ex(MESSAGE)`, which never touches `EG(exception)`, and managing the zval lifetime with an `owns_error` flag and an explicit `zval_ptr_dtor()` after the `reportFailure()` call.
- **`Async\Scope::disposeAfterTimeout()` leaked the scope refcount**: The timer callback bumped `callback->scope->scope.event.ref_count` once but nothing in `scope_timeout_callback()` or `scope_timeout_coroutine_entry()` ever released it, so the scope was always held above its natural lifetime — 4 `zend_mm` leaks per invocation in DEBUG. The raw `ref_count++` was replaced with `ZEND_ASYNC_EVENT_ADD_REF` and a custom `scope_timeout_callback_dispose` handler now releases the ref when the callback is freed without firing. On the fire path, `scope_timeout_callback()` transfers ownership to the spawned cancellation coroutine (via `extended_data`); `scope_timeout_coroutine_entry()` calls `ZEND_ASYNC_EVENT_RELEASE` after `SCOPE_CANCEL`. The previously-silent `add_callback` failure path also now releases the ref and frees the unclaimed callback.
- **`Async\CompositeException` wrote to hard-coded `properties_table[7]`**: `async_composite_exception_add_exception()` assumed the `private array $exceptions` typed property lived at slot 7 of the typed-property layout. The real offset for `CompositeException extends \Exception` did not match, so the helper was clobbering an unrelated typed slot: `getExceptions()` on an empty composite hit the "Typed property must not be accessed before initialization" fatal because it was reading the actual (uninitialized) `$exceptions` slot via `zend_read_property`; multiple `addException()` calls produced `var_dump` output with garbage pointer fields and implausible string lengths. Fixed by reading and writing `$exceptions` through `zend_read_property` / `zend_update_property` with the property name, so the engine resolves the correct typed-property slot regardless of inherited layout. `getExceptions()` switched from `silent=0` to `silent=1` (`BP_VAR_IS`) so an empty composite reads back as `[]` rather than triggering the typed-uninit fatal. A second latent bug surfaced while verifying: the PHP method `addException` was passing `transfer=true` to the C helper even though `Z_PARAM_OBJECT_OF_CLASS` only lends a borrowed reference, which caused the stored zval refcount to be one short and made repeated adds alias to the last-inserted object once the slot-7 corruption stopped masking it. Fixed by switching the method call site to `transfer=false` so the helper performs the `GC_ADDREF`.
- **`Async\Timeout::cancel()` assertion at shutdown (`IS_OBJ_VALID`)**: See the first entry above — this is the same bug; leaving it listed because `tests/common/timeout_class_methods.phpt` from coverage phase 2 is the test that exposed it.
- **`TaskGroup::all()`/`race()`/`any()` use-after-free in synchronous-settled path**: The synchronous fast paths created a waiter via `task_group_waiter_future_new()` (which pushes it into `group->waiter_events[]`), resolved it synchronously, wrapped it in a Future wrapper and returned — but never removed it from the `waiter_events[]` vector. The drain path in `task_group_try_complete()` always calls `task_group_waiter_event_remove()` after resolving; the sync path forgot to mirror that. At shutdown, `task_group_free_object()` force-disposed everything still in `waiter_events[]`, which `efree`'d the waiter. When the Future wrapper was then destroyed and released the waiter it had wrapped, it touched freed memory — "Future was never used" warning from a stale `zend_future_t` followed by a segfault whenever user code kept an intermediate `$future = $group->all()` variable across a `try`/`catch`. Fixed by calling `task_group_waiter_event_remove(waiter)` at the end of each synchronous-resolve branch, matching what `task_group_try_complete()` does.
- **`Thread::finally()` on a still-running thread NULL-scope crash**: `thread_object_dtor()` dispatches registered finally handlers via `async_call_finally_handlers()`, which unconditionally dereferences `context->scope` through `ZEND_ASYNC_NEW_SCOPE(context->scope)` and `ZEND_ASYNC_EVENT_ADD_REF(&context->scope->event)`. `thread.c` was passing `context->scope = NULL` because the Thread object had no PHP-side scope of its own, and registering a finally handler on a still-running thread then destroying the thread would segfault at dtor time. Fixed by capturing `ZEND_ASYNC_CURRENT_SCOPE` at spawn time (`async_thread_object_t::parent_scope`) and holding a refcount on the scope event so it outlives the Thread. `thread_object_dtor()` now passes `thread->parent_scope` to the finally dispatcher, so handlers inherit the caller's async context hierarchy (exception handlers, context values) just like `coroutine`/`task_group`/`scope` finally do. Released in `thread_object_free()`. Added `thread_finally_handlers_dtor()` to pair the `GC_ADDREF` that keeps the Thread alive during handler execution with an `OBJ_RELEASE` — previously `context->dtor` was `NULL` and the Thread object leaked 72 bytes every time dtor-time finally ran. A `ZEND_ASYNC_IS_OFF` safety net is kept for the edge case where a Thread object outlives the async subsystem (late `zend_call_destructors` after RSHUTDOWN).

## [0.6.7] - 2026-04-13

### Added
Expand Down
Loading
Loading