Skip to content

Fix multi-embed cache dependency losing all but the last child#177

Merged
koriym merged 4 commits into
bearsunday:1.xfrom
koriym:fix-multi-embed-cache-dependency
May 31, 2026
Merged

Fix multi-embed cache dependency losing all but the last child#177
koriym merged 4 commits into
bearsunday:1.xfrom
koriym:fix-multi-embed-cache-dependency

Conversation

@koriym

@koriym koriym commented May 31, 2026

Copy link
Copy Markdown
Member

概要 / Summary

#[Cacheable] なリソースが複数の子リソースを埋め込む場合に、親リソースのキャッシュ依存関係が正しく保持されない不具合を修正します。

This fixes a bug where cache dependencies were not preserved correctly when a #[Cacheable] resource embedded multiple child resources.

これまで CacheDependency::depends() は、子リソースのタグを追加するたびに親リソースの Surrogate-Key ヘッダーを上書きしていました。そのため、複数の子を持つ親リソースでは最後に処理された子の依存関係だけが残り、それ以前の子リソースを purge / refresh しても親リソースが無効化されませんでした。

Previously, CacheDependency::depends() overwrote the parent's Surrogate-Key header on each child dependency, so only the last child dependency was retained.

発生条件 / Condition

この不具合は、キャッシュされる親リソースが複数の cacheable な子リソースを embed している場合に発生します。

This bug occurs when a cached parent resource embeds multiple cacheable child resources.

実装上は、embed された子リソースを materialize した後、その子が ETag ヘッダーを持つ場合だけ依存関係を登録します。非 cacheable な embed は ETag を持たないため、この依存関係登録の対象外です。

In implementation terms, dependency registration only happens when the embedded child has an ETag header. Non-cacheable embedded resources are not part of this dependency graph.

影響 / Impact

  • 本番環境など assertion が無効な環境では、先に埋め込まれた子リソースを更新しても親のキャッシュが残り、古い内容が配信される可能性がありました。
  • With assertions disabled, purging or refreshing an earlier child could leave the parent cache stale.
  • 開発・テスト環境など assertion が有効な環境では、2 回目の depends() 呼び出しで AssertionError が発生し、キャッシュ処理が失敗する可能性がありました。
  • With assertions enabled, the second depends() call could fail with an AssertionError.

修正内容 / Fix

  • CacheDependency::depends() で、親リソースの既存の Surrogate-Key に子リソースのタグを蓄積するように変更しました。
  • CacheDependency::depends() now accumulates child tags into the parent's existing Surrogate-Key.
  • 「親に既存タグがないこと」を前提にした誤った assertion を削除しました。
  • Removed the incorrect assertion that assumed the parent had no existing tags.
  • 複数子リソースと共有依存を持つ diamond dependency のテストを追加しました。
  • Added a diamond dependency test covering multiple children and a shared dependency.

テスト / Tests

追加した ComplexCacheDependencyTest で以下を確認しています。

ComplexCacheDependencyTest verifies:

  • DiamondTopDiamondLeft / DiamondRight / DiamondBottom を含む全依存タグを保持すること
  • DiamondTop retains dependency tags for DiamondLeft, DiamondRight, and DiamondBottom.
  • 共有下位リソースである DiamondBottom を purge すると、親の DiamondTop も正しく無効化されること
  • Purging shared DiamondBottom correctly invalidates parent DiamondTop.
  • DiamondLeft または DiamondRight を個別に purge しても、親の DiamondTop が正しく無効化されること
  • Purging either DiamondLeft or DiamondRight also invalidates DiamondTop.

AI を活用して、単一 embed の既存 fixture では見落としやすい複雑な条件を洗い出し、複数 child・共有 dependency・個別 purge・rebuild 後の再伝播を検証するテストを追加しました。これにより、今回の修正だけでなく今後の回帰検出の信頼性も高めています。

AI-assisted test exploration was used to add complex dependency scenarios covering multiple children, shared dependencies, selective purges, and dependency propagation after rebuilds. This improves confidence in the fix and future regression detection.

品質チェック結果 / Quality checks:

  • 126 tests / 243 assertions
  • coding standards
  • Psalm
  • PHPStan

いずれもエラーなしです。

All checks passed.

koriym added 3 commits May 31, 2026 23:40
A #[Cacheable] resource that embeds more than one child (DiamondTop embeds both
DiamondLeft and DiamondRight, which both embed the shared DiamondBottom) is not
handled: CacheDependency::depends() overwrites the parent's Surrogate-Key and
asserts the parent has none yet. The build aborts (assertion) / loses all but the
last child's dependency, so this test currently FAILS — it documents the bug
before the fix in the next commit.
CacheDependency::depends() overwrote the parent's Surrogate-Key (and asserted the
parent had none yet), so a #[Cacheable] resource that embeds more than one child
kept only the LAST child's dependency. Purging an earlier child then failed to
invalidate the parent, which kept serving a stale page (under-invalidation); with
assertions enabled it threw instead and the page was not cached at all.

Accumulate the child tags across every embedded child and drop the assertion.
Turns the diamond-dependency test from the previous commit green.
@koriym

koriym commented May 31, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ffb35ef7-3c24-4b9b-9782-258818ed8148

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
📝 Walkthrough

Walkthrough

This PR fixes a cache invalidation bug where CacheDependency::depends() was overwriting surrogate keys from earlier embedded children. The method now accumulates dependency tags by concatenating them, preventing stale cache entries. A comprehensive test suite with a diamond-shaped dependency graph validates the fix across multi-embed parents, cascading invalidation, and rebuild scenarios.

Changes

Cache Dependency Accumulation Fix

Layer / File(s) Summary
Core cache dependency accumulation fix
src/CacheDependency.php
CacheDependency::depends() removes the assertion about prior Header::SURROGATE_KEY state and now accumulates child surrogate tags by concatenating with existing values instead of overwriting, fixing the stale-cache bug for resources embedding multiple children.
Changelog entry documenting the fix
CHANGELOG.md
Unreleased section added with a Fixed entry describing the multi-embed cache dependency bug and the changes to CacheDependency::depends() behavior and assertion removal.
Diamond-shaped test fixture resources
tests/Fake/fake-app/src/Resource/Page/Dep/DiamondTop.php, DiamondLeft.php, DiamondRight.php, DiamondBottom.php
Four cacheable resources form a diamond: DiamondTop embeds both DiamondLeft and DiamondRight, and both embed the shared DiamondBottom. This structure enables testing multi-embed parents and shared-leaf invalidation cascades.
Comprehensive cache dependency regression test suite
tests/ComplexCacheDependencyTest.php
New test class with six scenarios: building the diamond caches all nodes, multi-embed parents depend on every child, purging a shared leaf cascades through both paths, purging one arm is precise and does not over-invalidate, rebuild after purge restores dependencies, and unrelated resources are unaffected by diamond purges.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • bearsunday/BEAR.QueryRepository#174: Both PRs touch the same cache-dependency mechanism for embedded resources—Resolve embed dependency before HAL rendering strips Request from body #174 changes when QueryRepository::put() discovers Request-based child deps and calls CacheDependency::depends(), while the main PR fixes CacheDependency::depends() to accumulate child surrogate tags instead of overwriting them.
  • bearsunday/BEAR.QueryRepository#176: Both PRs change how Header::SURROGATE_KEY is composed/merged across resources—main PR fixes CacheDependency::depends() to accumulate embedded child surrogate tags instead of overwriting, while retrieved PR fixes SurrogateKeys::setSurrogateHeader() to deduplicate when concatenating computed keys with any manually-set surrogate keys.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main fix: accumulating child cache dependency tags instead of overwriting them in multi-embed scenarios.
Description check ✅ Passed The description thoroughly explains the problem, the fix, and testing approach, clearly relating to the changeset modifications in CacheDependency and test files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codecov

codecov Bot commented May 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (41a9b84) to head (c02dc5a).
⚠️ Report is 2 commits behind head on 1.x.

Additional details and impacted files
@@             Coverage Diff             @@
##                 1.x      #177   +/-   ##
===========================================
  Coverage     100.00%   100.00%           
- Complexity       246       247    +1     
===========================================
  Files             53        53           
  Lines            750       749    -1     
===========================================
- Hits             750       749    -1     

☔ View full report in Codecov by Sentry.
📢 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.

@koriym
koriym merged commit a4cd9e8 into bearsunday:1.x May 31, 2026
20 checks passed
@koriym
koriym deleted the fix-multi-embed-cache-dependency branch May 31, 2026 16:54
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.

1 participant