Skip to content

Make cache writes atomic and store expiry in cache files - #110

Open
samdark wants to merge 12 commits into
fix-dot-prefixed-keysfrom
atomic-no-locks
Open

samdark wants to merge 12 commits into
fix-dot-prefixed-keysfrom
atomic-no-locks

Conversation

@samdark

@samdark samdark commented Oct 14, 2026 •

Copy link
Copy Markdown
Member
Q A
Is bugfix? Yes
New feature? No
Breaks BC? The cache file format changes; older versions cannot read new entries.
Fixed issues —

Cache writes currently update an existing file and its expiry separately. Concurrent readers can observe a partially written value, mismatched expiry and contents, or a missing entry during replacement. A failed write can also destroy the previous value.

Write a complete entry to a temporary file in the destination directory, then rename it into place. Read the expiry and serialized value from the same open handle. Coordinate publication with GC so collection cannot delete a freshly replaced value, and coordinate readers with replacement on Windows. Keep the publication lock and active temporary files during cleanup.

The proposed YFC1 header stores expiry and serialized payload length. It also identifies dot-prefixed entries when the configured suffix is empty. Old entries remain readable, but mixed-version deployments and rollbacks require separate cache directories. See UPGRADE.md.

The independent bug fixes have been extracted into separate PRs:

These form a review stack starting from master, in the order listed above. This PR is based on #115, so its diff excludes the extracted fixes. The proposed file format remains unchanged by this split; the performance and format alternatives raised in review remain open for a separate revision.

Copilot AI balanced review requested due to automatic review settings October 14, 2026 07:23
@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1f49c285-5ee2-434b-abbb-4e54b4df0120

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Shared-user lock handling can block writes, and cleanup can delete unrelated hidden-directory contents.

2 open findings
What changed in this PR

Introduces atomic cache publication with in-file expiry metadata, safer garbage collection, and expanded verification and benchmarking.

Changes:

  • Adds the YFC1 cache format and atomic temporary-file replacement.
  • Coordinates publication and GC while improving cleanup and batch-write behavior.
  • Adds race tests, regression coverage, benchmarks, and upgrade documentation.
File Description
src/​FileCache.php Implements atomic records, locking, and cleanup.
tests/​TestCase.php Adds recursive test cleanup.
tests/​AtomicHeaderTest.php Tests format and atomic reads.
tests/​ConcurrencyTest.php Improves stress-test execution.
tests/​concurrency_test.php Adds concurrent reader/writer validation.
tests/​FileCacheTest.php Updates expiry and cleanup tests.
tests/​RaceConditionTest.php Tests filesystem races.
tests/​ReviewRegressionTest.php Adds regression scenarios.
benchmarks/​CacheFixture.php Provides benchmark fixtures.
benchmarks/​MaintenanceBench.php Benchmarks clear and GC.
benchmarks/​OperationsBench.php Benchmarks cache operations.
benchmarks/​WorkloadBench.php Benchmarks mixed workloads.
benchmarks/​README.md Documents benchmark methodology.
phpbench.json.dist Configures PHPBench.
composer.json Adds benchmark dependencies and scripts.
composer-dependency-analyser.php Includes benchmark sources.
.php-cs-fixer.dist.php Includes benchmark formatting.
.github/​workflows/​rector-cs.yml Watches benchmark changes.
.github/​workflows/​composer-dependency-analyser.yml Analyzes benchmark changes.
.gitignore Ignores benchmark output.
.gitattributes Exports the upgrade guide.
README.md Documents compatibility and benchmarks.
UPGRADE.md Adds deployment and rollback guidance.
docs/​internals.md Documents the new file format.
CHANGELOG.md Records the behavioral changes.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/FileCache.php Resolved
Comment thread src/FileCache.php Resolved
@samdark
samdark marked this pull request as draft October 14, 2026 21:09
@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.28058% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 99.29%. Comparing base (39f7929) to head (9c066ae).

Files with missing lines Patch % Lines
src/FileCache.php 99.28% 1 Missing ⚠️
Additional details and impacted files
@@                     Coverage Diff                     @@
##             fix-dot-prefixed-keys     #110      +/-   ##
===========================================================
+ Coverage                    98.82%   99.29%   +0.47%     
- Complexity                      78      146      +68     
===========================================================
  Files                            1        1              
  Lines                          170      284     +114     
===========================================================
+ Hits                           168      282     +114     
  Misses                           2        2              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@samdark
samdark marked this pull request as ready for review October 14, 2026 22:26
@samdark
samdark requested review from a team and arogachev and removed request for arogachev October 14, 2026 22:26

@vjik vjik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few thoughts:

  1. Split independent fixes into separate PRs. Some fixes don't depend on the new format or atomic writes:

    • write errors in set();
    • the setMultiple() result;
    • GC and clear() robustness under concurrency;
    • failed filemtime() in GC;
    • keys starting with a dot.

    They're easier to review separately and can go into a patch release.

  2. Performance. File cache is usually chosen for small projects with low load. These users get has(), overwrites and GC 2–4x slower with no noticeable benefit.

  3. The file format change breaks backward compatibility. It may not be needed for atomicity, though. We could write to a temporary file, set its expiry with touch(), then rename() it into place, since rename() preserves mtime. On read, use fstat() on the opened handle. The format stays the same. What do you think about this approach?

@samdark
samdark changed the base branch from master to fix-dot-prefixed-keys October 17, 2026 22:31
@samdark

samdark commented Oct 17, 2026

Copy link
Copy Markdown
Member Author
  1. Done.
  2. Low-traffic websites still regularly have concurrent requests and master doesn't behave correctly when there is concurrency.

@samdark

samdark commented Oct 17, 2026 •

Copy link
Copy Markdown
Member Author

As for performance, did mtime proof of concept and measured it:

Operation Master + bugfixes Header Mtime PoC Change vs Header
Get hit 18.68 µs 16.75 µs 20.43 µs 🔴 22.0% slower
Get miss 3.48 µs 9.11 µs 8.93 µs 🟢 2.0% faster
Get expired 4.24 µs 8.78 µs 14.91 µs 🔴 69.8% slower
Has hit 4.75 µs 16.95 µs 5.37 µs 🟢 68.3% faster
Set new 53.01 µs 90.19 µs 90.94 µs 🔴 0.8% slower
Set overwrite 43.53 µs 104.50 µs 108.51 µs 🔴 3.8% slower
95% reads / 5% overwrites 20.08 µs 22.91 µs 25.66 µs 🔴 12.0% slower
GC: 1,000 entries, none expired 8.28 ms 22.27 ms 12.44 ms 🟢 44.1% faster
GC: 1,000 entries, half expired 15.06 ms 54.30 ms 41.73 ms 🟢 23.1% faster
GC: 1,000 entries, all expired 21.75 ms 86.40 ms 71.11 ms 🟢 17.7% faster

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.

3 participants