Skip to content

Read entries of one phar from several coroutines - #290

Merged
EdmondDantes merged 2 commits into
mainfrom
283-phar-shared-decompression
Sep 9, 2026
Merged

EdmondDantes merged 2 commits into
mainfrom
283-phar-shared-decompression

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

Closes #283.

Every compressed entry of an archive is decompressed into phar->ufp, one stream shared by every entry of that archive, at the offset the entry takes from it before it starts writing. Every call of that span parks — seek to the end, remember the offset, append the decompression filter, copy from the archive stream, flush, remove the filter — so two coroutines opening two compressed entries interleaved: the second appended its own filter to the same write chain and put its bytes at the offset the first had remembered. Four gzipped entries read by four coroutines answered internal corruption of phar "..." (actual filesize mismatch on file "...") for three of the four, on every run.

The engine side is php-src 353ccf7b070 (true-async, merged into true-async-stable). It holds the span as one unit: both sides of the archive stream around it and both sides of the uncompressed-file stream inside them, taken write-side first, the order php_stream_seek() and the rest of the stream layer take them. The archive stream is remembered once instead of read again after each park, and the entry is examined a second time under the lock, since another coroutine may have opened it while this one queued.

The call also holds an archive reference: phar_get_entry_data() takes its own only after phar_open_entry_fp() has returned, and without one the coroutine that finished first dropped the last reference and phar_archive_delref() closed the archive stream the others were queued on. The count is restored by hand — a teardown at zero closes the stream the caller is about to read — so a teardown due while the call was parked falls to whoever drops the count to zero next.

Reading one archive is serialized per archive as a result, stored entries included.

This PR carries the test and the changelog entry.

  • tests/io/101-phar_entries_decompressed_at_once.phpt: three broken entries of four before the engine change, 10 green runs of 10 after.
  • ext/phar/tests: 565 of 565.
  • ext/async/tests plus php-src file, streams, filters, zlib and phar: 3221 tests, 3 failures, all three the local symlink artefacts (curl 063, curl 064, io 082).

Left open, found while reviewing this change and not caused by it: phar_open_entry_fp() does not hold the entry itself across a park, so phar_entry_remove() from another coroutine can free the phar_entry_info under it; and two coroutines can both open the archive stream, since phar_open_archive_fp() does not re-check after php_stream_open_wrapper() parks.

Every compressed entry of an archive is decompressed into one stream shared by
the archive, at the offset the entry takes from that stream before it starts
writing. Every call of that span parks, so two coroutines opening two compressed
entries interleaved: the second appended its own decompression filter to the
same write chain and wrote at the offset the first had remembered. Three entries
of four came back as "internal corruption of phar ... filesize mismatch" on
every run.

The fix is in php-src 353ccf7b070, which holds the span as one unit and keeps an
archive reference across it. The test covers both halves: four coroutines
decompressing four entries, and a stored entry seeked from one coroutine while
another decompresses.
@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@EdmondDantes
EdmondDantes merged commit 7dd6250 into main Sep 9, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

phar decompresses into one shared stream across a span that coroutines can interleave

1 participant