Skip to content

[6.x] Fix stale asset listings across queued jobs#14617

Merged
jasonvarga merged 4 commits intostatamic:6.xfrom
ryanmitchell:fix/issue-14576
May 7, 2026
Merged

[6.x] Fix stale asset listings across queued jobs#14617
jasonvarga merged 4 commits intostatamic:6.xfrom
ryanmitchell:fix/issue-14576

Conversation

@ryanmitchell
Copy link
Copy Markdown
Contributor

I had Claude try and figure out Rob's issue, and it pinpointed the blink cache on the AssetContainer's contents().

This is its logic:

In a long-running queue:work worker, Blink persists for the entire process lifetime. All jobs share the same AssetContainerContents instance. The isWorker() guards on all(), filteredFilesIn(), and filteredDirectoriesIn() are there to prevent stale in-memory state from one job bleeding into the next. But metaFilesIn() has no isWorker() guard:

// AssetContainerContents.php:182
public function metaFilesIn($folder, $recursive)
{
if (isset($this->metaFiles[$key = $folder.($recursive ? '-recursive' : '')])) {
return $this->metaFiles[$key]; // ← No isWorker() check — always stale across jobs
}
// ...
return $this->metaFiles[$key] = $files;
}

More importantly, the filteredFiles/filteredDirectories instances are only cleared via add(). If some other code path reads from the container's contents after job A runs (before the next add() call in job B), it gets the Blink-cached instance with the stale $this->files.

Why Horizon works

With php artisan queue:work, a single process runs all jobs and Blink accumulates indefinitely. With Horizon, each horizon:work subprocess handles a bounded number of jobs before being replaced (via memory limits or maxProcesses). Fresh subprocesses get fresh Blink state. This is the most likely explanation for the Horizon/queue:work asymmetry.

Why private containers are affected more than public

This is the trickier part. The private() check only affects URL generation — it has no effect on listing or caching code paths. The actual difference is almost certainly how the assets are being queried in your front-end code:

  • Public assets may be queried/linked by direct URL (e.g. asset.url → /assets/image.jpg), which requires no Stache lookup — it just works if the file is on disk
  • Private assets must be served through a controller that calls Asset::find() or goes through queryAssets() — requiring the Stache to know about them

If the listing cache or Stache is stale, private assets simply don't exist from Statamic's perspective, while public assets might still be reachable by URL.

The clean fix

The real solution is to not Blink-cache the AssetContainerContents instance, since the whole point of isWorker() guards inside it is to defeat the in-memory caching anyway

This way, workers always get a fresh AssetContainerContents instance per call (no shared state between jobs), while normal web requests still get the per-request Blink cache benefit. The metaFilesIn() stale-data gap also disappears since there's no cross-job instance sharing.

Closes #14576

@ryanmitchell
Copy link
Copy Markdown
Contributor Author

Rob confirmed this fixed his issue.

jasonvarga and others added 2 commits May 7, 2026 09:19
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jasonvarga jasonvarga changed the title [6.x] Dont blink asset container contents() inside workers [6.x] Fix stale asset listings across queued jobs May 7, 2026
@jasonvarga jasonvarga merged commit 08fd273 into statamic:6.x May 7, 2026
17 checks passed
@robdekort
Copy link
Copy Markdown
Contributor

How nice. Thanks!

@ryanmitchell ryanmitchell deleted the fix/issue-14576 branch May 7, 2026 20:34
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.

Generating an asset in a private container programmatically on a regular queue worker requires a cache clear

3 participants