Skip to content

[Draft] S3 asset storage spike: shared SHA blobs and filesystem/NFS parity - #37585

Draft
swicken wants to merge 4 commits into
mainfrom
spike/s3-asset-storage
Draft

swicken wants to merge 4 commits into
mainfrom
spike/s3-asset-storage

Conversation

@swicken

@swicken swicken commented Sep 16, 2026

Copy link
Copy Markdown
Member

Proposed changes

This is a draft PR for an ongoing S3 asset-storage spike. It is not ready to merge or presented as production-complete. The to-do list below is active work and will be worked through as this draft progresses. The immediate goal is a demonstrable filesystem/NFS-parity POC, with remaining gaps made explicit.

The spike lets dotCMS keep durable asset bytes in S3-compatible storage and restore local files on demand. Existing content URLs, filenames, permissions and content/version ownership remain in dotCMS. Local disk still provides working space and a cache for consumers that require files.

The feature is off by default, enabled with FEATURE_FLAG_S3_ASSET_STORAGE=true (DOT_FEATURE_FLAG_S3_ASSET_STORAGE=true in Docker). Filesystem/NFS remains supported; configuring BINARY_CHAIN alone does not enable the feature.

Implemented so far

  • Shared content-addressed bytes: originals and completed renditions use SHA-256 blobs at asset-blobs/sha256/ab/cd/ef/01/<full-64-character-hex-sha>. Existing content-owned keys contain small references. Equal bytes share a blob; deleting one owner removes its reference without deleting another owner's bytes. Legacy raw objects and the earlier one-level blob layout remain readable.
  • Independent environment namespaces: owned references, editorial metadata and supporting artifacts can be scoped under asset-namespaces/<namespace>/..., while SHA blobs and byte-derived extraction remain shared. Namespaces separate application keys; they are not an IAM security boundary.
  • Replacement and cache safety: immutable revision references, verified publication, SHA validation on cold downloads, atomic local restoration, guarded cache eviction and leases around covered consumers. Operational files and incomplete/staging files are excluded from eviction.
  • Metadata and renditions: shared Tika extraction by content/configuration, separate per-use metadata and focal points, cold metadata regeneration, revision-aware rendition keys and warm-rendition reads during storage outages.
  • Lifecycle integration: gated binary consumers, temporary uploads/editor metadata, WebDAV staging, templates, compiled CSS, starter export/import, integrity relocation and publishing archive storage. These paths have implementation and focused coverage; the remaining end-to-end gaps are listed below.
  • Migration and recovery: conditional, resumable backfill through the existing job system, including retired binary fields and legacy physical Image/File values. Verified deleted-content and field-trash recovery ZIPs precede covered cleanup operations; cleanup uses existing durable jobs. ZIP recovery/retention is currently operator-controlled.
  • Configuration and fixtures: explicit credentials or the SDK credential chain, endpoint/region validation, local MinIO POC, temporary-credential and real NFS acceptance fixtures.

Validation so far

  • After rebasing onto main: uncached core build/install succeeded; 130 targeted tests, 128 passed / 2 fixture-dependent skips, no failures. Includes real MinIO checks for four-level keys, legacy fallback, corruption, eviction and disabled-mode isolation.
  • Earlier CMS acceptance: enabled suite 70 passed / 2 existing skips; disabled suite 52 passed / 18 expected skips. These CMS runs predate the final rebase/four-level change.
  • Two independent local CMS environments sharing one MinIO bucket: separate databases, search instances, cache roots and namespaces. Verified shared original/extraction/rendition blobs, cold reads, replacement/deletion isolation and cold reads after restart.
  • Database recovery exercise: restored a pg_dump checkpoint and reindexed, then retrieved the earlier asset bytes from retained S3 data without restoring the bucket. This was not WAL/timestamp PIT recovery or automatic ownership reconciliation.
  • Migration recovery: a live migration job resumed after an application process kill and verified 545 originals, including legacy Image assets.
  • NFS baseline: 30 focused tests and five provider phases passed across two kernel NFS 4.1 mounts/JVMs with the feature disabled. This does not establish full CMS-cluster failover behavior.

The two-environment demo predates the four-level layout and latest rebase; it has not been redeployed to this branch tip. Native libvips acceptance also remains open.

To do — active work, in priority order

  • Finish parity acceptance on the final branch: rebuild/redeploy the demo, rerun enabled/disabled CMS checks, and exercise the same-environment multi-node paths as well as independent environments. Preserve filesystem/NFS and operational-state behavior.
  • Close lifecycle concurrency and recovery gaps: restore/delete/invalidation races across processes, uncovered direct-file consumers, cleanup-worker restart/retry exhaustion, metadata versus check-in races, and concurrent native rendition generation.
  • Finish migration/export/recovery coverage: remaining legacy metadata/language formats, local-only staging and backup/trash migration, consistent exports during edits, populated restore failure/atomicity behavior, and documented retention/recovery boundaries. Migration-job restart is already verified.
  • Finish remaining user-facing paths: editor save/cancel and permissions, WebDAV tree concurrency, publishing retry/receiver round-trip across nodes, legacy/static publishing staging and endpoint separation, preview/plugin/direct-path consumers, and native image/focal-point behavior.
  • Validate deployment and operating conditions: target-provider conditional-write behavior, IAM/role/OIDC configuration, readiness/outage recovery, KMS/encrypted and large-object behavior, HTTP range/conditional/permission semantics, NFS failover, and representative throughput/request costs.
  • Refresh the acceptance documentation: consolidate historical checkpoints and record the final deployed version, reproducible checks and outstanding limits.

Deliberate limits and follow-on work

Shared blobs and extraction records are retained today. Safe reclamation, abandoned-reference reconciliation, automatic namespace decommissioning/PIT ownership reconciliation, WebDAV tombstone reclamation and shared transformation-recipe reuse are not complete. Their retention/operational limits must be documented before broader rollout; they are not prerequisites to demonstrating the scoped POC.

Reference-only/job-based push publishing is a future opportunity, not implemented by this PR. Publishing still uses bundles. Time Machine remains excluded.

Checklist and supporting documentation

  • Targeted tests and build pass; broader acceptance remains tracked above.
  • Security implications identified: existing dotCMS permissions remain, namespaces are not credential isolation, and deployment IAM/permission acceptance is still required.
  • Translations/screenshots: not applicable to this backend spike.

See storage behavior and test instructions, the completion ledger, and the local POC fixture. The ledger contains historical checkpoints; newer results above supersede older pending/deployment notes.

… assets

New API layer for binary file storage that delegates to StoragePersistenceAPI.
Registered in APILocator, defaults to FILE_SYSTEM mode with zero behavior change.
Includes getBinaryFile, storeBinary (with hard-link support), copyBinary,
deleteBinary, deleteAllBinaries, and existsBinary operations.
Migrate all 6 hardcoded filesystem binary access sites to use
BinaryAssetStorageAPI. Deprecate getRealAssetPath methods with migration
guidance. Add deleteAllBinaries for inode-level cleanup. Integration
tests written for end-to-end verification.

Migrated callers:
- Contentlet.getBinary → BinaryAssetStorageAPI.getBinaryFile
- ESContentletAPIImpl.handleBinaries → storeBinary/copyBinary
- ESContentletAPIImpl.getBinaryFile → getBinaryFile
- ContentletTransformer → getBinaryFile
- ContentletJsonAPIImpl → getBinaryFile
- WebAPI, FileAssetBundler, DropOldContentletRunner → getBinaryFile/deleteAllBinaries
… cache eviction

Phase 3 of S3 binary asset storage:
- Config-driven BINARY_CHAIN mode (FILE_SYSTEM → S3) with S3 init fallback
- Binary S3 uses PathEncryptionMode.NONE for prefix-based listing
- Added listObjectPaths(group, prefix) to StoragePersistenceAPI with FS/S3/Chain impls
- getBinaryFile(inode, fieldVarName) discovers filename through chain on cold cache
- deleteAllBinaries(inode) cleans all chain providers with partial-failure handling
- BinaryCacheEvictionJob (StatefulJob) with LRU eviction, min-age protection
- Registered eviction job in DotInitScheduler (cron-gated)
- 10 new unit tests across storage providers and eviction job
@github-actions

Copy link
Copy Markdown
Contributor

❌ Issue Linking Required

This PR could not be linked to an issue. All PRs must be linked to an issue for tracking purposes.

How to fix this:

Option 1: Add keyword to PR body (Recommended - auto-removes this comment)

Edit this PR description and add one of these lines:

  • Fixes #123 or Closes #123 — same-repo issue
  • Fixes org/repo#123 or Closes org/repo#123 — cross-repo or private issue
  • Closes https://github.com/org/repo/issues/123 — full GitHub URL
  • Other supported keywords: fix, fixed, close, closed, resolve, resolved
  • Refs #123 or Part of #123 — links the issue without closing it on merge (same-repo only)
  • End the PR title with (#123) — same-repo link without closing it on merge

Option 2: Link via GitHub UI (Note: won't clear the failed check)

  1. Go to the PR → Development section (right sidebar)
  2. Click "Link issue" and select an existing issue
  3. Push a new commit or re-run the workflow to clear the failed check

Option 3: Use branch naming

Create a new branch with one of these patterns:

  • 123-feature-description (number at start)
  • user/123-feature-description (worktree style: number after the namespace)
  • issue-123-feature-description (issue-number at start)
  • feature-issue-123 (issue-number anywhere)

Why is this required?

Issue linking ensures proper tracking, documentation, and helps maintain project history. It connects your code changes to the problem they solve.


This comment was automatically generated by the issue linking workflow

@github-actions github-actions Bot added Area : Backend PR changes Java/Maven backend code Area : Documentation PR changes documentation files labels Sep 16, 2026
@claude

claude Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Claude finished @swicken's task in 2m 3s —— View job


Reviewing this S3 asset storage spike (~16k lines across 100 files)...

  • Read core storage orchestration (ChainableStoragePersistenceAPI, BinaryAssetStorageAPIImpl, S3ContentAddressedStorage, BinaryAssetReference)
  • Read filesystem persistence + rendition exporter changes
  • Reviewing cache eviction, backfill, temp/webdav/publishing, content-persistence, and S3/metadata layers (in progress)
  • Post consolidated review

This is a draft POC with an explicit to-do list; I'm focusing on provable correctness/security/race findings in the new code, not re-flagging the acknowledged open gaps.

@swicken swicken added the PR: docker image Build & push a per-PR test image to dotcms/dotcms-test label Sep 16, 2026
@semgrep-dotcms

Copy link
Copy Markdown
Contributor

Semgrep found 17 CUSTOM_INJECTION-2 findings:

The method identified is susceptible to injection. The input should be validated and properly
escaped.

If this is a critical or high severity finding, please also link this issue in the #security channel in Slack.

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

Labels

Area : Backend PR changes Java/Maven backend code Area : Documentation PR changes documentation files PR: docker image Build & push a per-PR test image to dotcms/dotcms-test

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant