Skip to content

Make the cpx cache layer atomic and concurrent-safe#22

Merged
WendellAdriel merged 15 commits into
2.xfrom
feat/metadata-cache
Jul 1, 2026
Merged

Make the cpx cache layer atomic and concurrent-safe#22
WendellAdriel merged 15 commits into
2.xfrom
feat/metadata-cache

Conversation

@WendellAdriel

Copy link
Copy Markdown
Member

cpx keeps a shared metadata file next to its package and exec-sandbox caches. Concurrent runs could clobber each other's writes, a partially-finished install could leave a poisoned cache that never recovered on its own, and clean was a blunt all-or-nothing command.

This reworks the cache layer to be safe under concurrent use: metadata updates run inside a file lock with atomic temp-file-then-rename writes, and packages and exec sandboxes install into a staging directory that's atomically renamed into place, so a failed install can't leave anything broken behind. clean also gets an interactive Prompts flow and path-guarded deletion that stays inside the cpx cache root.

- Add Lock::run for flock-based exclusive critical sections
- Add Filesystem::writeAtomic for temp-write-then-rename writes
- Add Filesystem::deleteDirectoryWithin to refuse deletions outside a root
- Persist metadata under a lock with atomic writes via Metadata::transaction
- Introduce typed PackageMetadata and ExecSandboxMetadata records
- Version the schema and reserve an aliases section for forward compatibility
- Hash version constraints into safe cache-key folder names
- Stage installs in a temp directory and rename atomically to avoid partials
- Clean stale and orphaned package/exec caches with path-safety guards
- Route the exec sandbox through the typed ExecSandboxMetadata record
- Render the package list from the typed metadata accessors
Fall back to a package's last-updated time when it has never been run, so
a recent install is not reclaimed before its binary is ever used.
Extract CleanCommand::SECONDS_PER_DAY so the --days window conversion reads
clearly. No behavior change.
Bare `clean` now asks what to clean (all, sandbox, or a day window)
via Laravel Prompts, runs cleanup inside a task spinner, and reports a
callout summary. Adds a --sandbox flag and makes --days optional;
non-interactive runs still reclaim caches older than 30 days.
- Stage and atomically rename exec sandbox installs so a failed install can't poison the cache
- Mutate sandbox state inside the metadata transaction to keep concurrent writes safe
- Store all cache timestamps as unix ints, normalizing legacy date strings on read
- Share one staleness rule across packages and sandboxes, and skip malformed metadata entries
- Exit clean non-zero when a cache can't be removed
- Add a race-safe directory helper and drop the unsafe Metadata::save()

@joetannenbaum joetannenbaum left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just the one question about aliases, otherwise good to go 👍

Comment thread src/Cache/Metadata.php Outdated
public array $packages = [],
public array $execCache = [],
) {}
public array $aliases = [],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this just setup for another PR? Looks like it's just scaffolding in this context, should we hold off on this until we know how we're going to use it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, that was just scaffolding, I'll remove it!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Once removed merge away 🫡

The aliases section only round-tripped with no consumers. It'll come
back with the user-managed aliases work it was setting up for.
clean only deleted the leaf version directory, leaving empty
vendor/name skeletons behind. It now walks up and removes each
emptied parent, stopping at the cache root and skipping any a
concurrent install has repopulated.
ob_get_clean() can return false, which OutputInterface::write()
rejects, so restore the check before flushing captured output.
@WendellAdriel WendellAdriel merged commit a69e20a into 2.x Jul 1, 2026
3 checks passed
@WendellAdriel WendellAdriel deleted the feat/metadata-cache branch July 1, 2026 17:55
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.

2 participants