Skip to content

Traverser::filter() is shared across stores, so a nested traversal replaces the running one's filter #15550

Closed

Description

@joshdaugherty

Bug description

On a multisite install with a cold Stache cache, the first rebuild of a collection's entries store can keep only the first entry file and drop the rest. The truncated list is then cached as the store's paths, so the missing entries stay missing (404s, empty listings) until the Stache is cleared again.

The cause is that every store shares one Traverser. Store and TaxonomyTermsStore reach it through a real-time facade (use Facades\Statamic\Stache\Traverser), and Laravel caches a facade's resolved instance, so every call in a request gets the same object. Traverser::filter() stores the callback on that object, and traverse() reads $this->filter again for each file inside its loop.

On multisite, CollectionEntriesStore::getItemFilter() calls $this->collection()->sites(), which resolves the collection. On a cold cache that builds the collections store, which calls Traverser::filter([$this, 'getItemFilter']) on the same shared instance. When control returns, the entries traversal carries on with CollectionsStore::getItemFilter as its filter, and that filter rejects the remaining .md files.

Measured on a real site with 82 entry files in pages: the first traversal kept 1 and the collections store's filter rejected the other 81. A second traversal, with the collections store already built, kept all 82. Both passes used the same traverser instance.

Still present in v6.34.1 and on 6.x (1ae088399).

How to reproduce

  1. Enable multisite with two sites, en and fr.
  2. Create a collection blog with sites: [en, fr] and three entries in content/collections/blog/en/.
  3. Clear the Stache (php please stache:clear).
  4. Read the collection's entries store, for example Stache::store('entries')->store('blog')->paths(), or list the collection's entries.

Only the first entry file is returned. #15551 adds a test that reproduces this in Statamic's own suite; on 6.x it fails with Failed asserting that actual size 1 matches expected size 3.

Logs

No error is thrown; the entries are silently missing.

Environment

Environment
Laravel Version: 13.24.0
PHP Version: 8.4.23
Composer Version: 2.10.2
Environment: local
Debug Mode: ENABLED
Maintenance Mode: OFF
Timezone: UTC
Locale: en

Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: NOT CACHED

Drivers
Broadcasting: log
Cache: file
Database: sqlite
Logs: stack / single
Mail: smtp
Queue: sync
Session: file

Storage
public/storage: NOT LINKED

Statamic
Addons: 9
License Key: Not set
Sites: 255 (names redacted)
Stache Watcher: Enabled (auto)
Static Caching: Disabled
Version: 6.26.0 PRO

Statamic Addons
el-schneider/statamic-admin-bar: 0.3.2
jacksleight/statamic-bard-mutator: 3.0.5
jacksleight/statamic-bard-texstyle: 4.1.1
mitydigital/sitemapamic: 3.5.1
statamic/eloquent-driver: 5.10.0
statamic/ssg: 4.1.0
stillat/relationships: 2.4.0
studio1902/statamic-peak-tools: 9.2.0
uams-web/wordpress-importer: 0.23.1

Statamic Eloquent Driver
Addon Settings: file
Asset Containers: file
Assets: eloquent
Blueprints: file
Collection Trees: file
Collections: file
Entries: file
Fieldsets: file
Form Submissions: file
Forms: file
Global Sets: file
Global Variables: file
Navigation Trees: file
Navigations: file
Revisions: file
Sites: file
Taxonomies: file
Terms: file
Tokens: file

Installation

Existing Laravel app

Additional details

The environment above runs 6.26.0 with a local patch for this bug. The reproduction and the fix were checked against v6.34.1 and 6.x.

This may explain some of the reports in #10410. Several people there say the fix is to run stache:warm twice, and a second rebuild is exactly what recovers here, but that hasn't been confirmed against those sites.

The fix is to have filter() return a clone carrying the callback instead of changing the shared instance. All three core call sites (Store.php:193, Store.php:309 and TaxonomyTermsStore.php:166) already chain filter()->traverse(), so none of them depends on the old behaviour.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions