Skip to content

JAMES-4231 S3 Object Compaction - #3193

Draft
HesandaLiyanage wants to merge 8 commits into
apache:masterfrom
HesandaLiyanage:JAMES-4231
Draft

HesandaLiyanage wants to merge 8 commits into
apache:masterfrom
HesandaLiyanage:JAMES-4231

Conversation

@HesandaLiyanage

Copy link
Copy Markdown
Contributor

JIRA: https://issues.apache.org/jira/browse/JAMES-4231

Context & Problem

High email ingest volumes in Apache James deployments utilizing S3/MinIO object storage result in hundreds of millions of small S3 objects (average body/header size ~20–50KB). Operating at this granularity introduces high S3 API request fees (PUT/GET/LIST), elevated bucket listing latency during storage maintenance and garbage collection, and reduced overall object storage throughput.

Solution Overview

This PR implements autonomous S3 object compaction for Apache James, packing historical standalone blobs into large, immutable chunk files (~100MB target size) with slot-level virtual addressing and transparent single-request HTTP ranged read support.

Key Components & Architecture

  1. Chunk Format (ChunkFormat):

    • Single binary chunk containing concatenated slots and a trailer footer:
      [Header (16B)][Slot 0]...[Slot N-1][Footer][Footer Size (4B)][End Magic 0x454F4643]
    • Per-slot independent Zstandard compression: Each slot has its own Zstd codec (content-encoding=zstd\n), allowing single-slot ranged reads without decompressing or buffering the full 100MB chunk.
    • Suffix-indexed 64KB footer: Suffix range read (readRange(..., -65536, -1)) fetches chunk slot metadata in $O(1)$ without scanning payloads.
    • Fully documented in src/adr/0076-s3-object-compaction.md.
  2. Virtual Slot Addressing & Ranged Reads (ChunkedBlobStoreDAO & ChunkId):

    • Format: <family>_<generation>_chunk<hash>~<offset>~<limit>.
    • ChunkedBlobStoreDAO transparently intercepts slot references and issues HTTP byte-range requests directly against the underlying storage connector (readRange(bucket, chunkId, offset, offset + limit - 1)).
    • Conforms strictly to ChunkMarker regex (^\d+_\d+_chunk[A-Za-z0-9_-]{16,}(~\d+~\d+)?$) to ensure bloom-filter GC never misclassifies chunks as unreferenced garbage.
  3. Compaction Pipeline & Crash Safety (BlobCompactionAlgorithm):

    • Initial Compaction (initialCompact):
      1. Candidate scanning windowed into batches of 1,000 blobs (DEFAULT_CANDIDATE_BATCH_SIZE = 1000) so candidate byte arrays are packed and freed window-by-window without buffering the generation's payloads in heap.
      2. Chunk assembled and saved to raw S3 storage.
      3. Metadata references updated in Cassandra (messageV3, messageIdToImapUid, messageIdTable).
      4. Original standalone blobs deleted from S3 only for candidates whose references updated successfully. If an update fails mid-batch, deletion is skipped for that candidate, guaranteeing zero dangling references.
    • GC Compaction (gcCompact):
      • Inspects existing chunks via footer-only ranged reads (metadata-only).
      • Orphan chunks (100% dead slots) deleted with 0 payload bytes read.
      • Chunks exceeding dead ratio rewritten; small adjacent chunks merged. Live slots streamed individually via ranged reads, bounding GC heap to $O(\text{maxSlotSize})$.
    • Self-Healing Repairer (CassandraBlobIdRepairer):
      • On missing slot / 404, falls back to original blob ID from blobReferenceMapping and restores Cassandra references. Also heals any transient inconsistency window across Cassandra tables.
  4. Configuration Guards & Scope:

    • Compaction automatically disables itself with an informative log when client-side AES encryption or whole-blob compression is active (AES cipher blocks HTTP range slicing).
    • Generation-scoped: triggered via DELETE /blobs?scope=compaction&generation=<gen>&family=<fam>.
  5. Memory Characteristics & Operational Ceiling:

    • Candidate payload heap: strictly bounded by $O(\min(N \times \text{avgBlobSize}, \text{chunkTargetSize}))$.
    • GC payload heap: strictly bounded by $O(\text{maxSlotSize})$ (~1MB).
    • Reference mapping ceiling: $O(\text{totalLiveGenerationReferences} \times \approx 200\text{ bytes})$ (~200MB heap for 1M live references; ~2GB heap for 10M). Documented in class javadoc; future follow-up can introduce partition-paged lookups.

Verification & Tests

  • server/blob/blob-compaction: 47 tests passed (100%), including allocation-bound proof test and task serialization.
  • server/blob/blob-storage-strategy: 78 tests passed (100%).
  • mailbox/cassandra (CassandraBlobId*IntegrationTest): 3/3 passed with real Cassandra 5.0.9 testcontainers.
  • server/blob/blob-s3: Ranged read and MinIO S3 end-to-end compaction integration tests passed.
  • server/container/guice/distributed: 43 tests passed (100%).
  • Checkstyle: 0 errors across all 7 modified modules.

@Arsnael

Arsnael commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Hi @HesandaLiyanage !

First of all thank you very much for trying to solve this.

However 6k lines changes in one commit is too much for code readability and reviewing properly. Can you cut this work down to smaller step commits?

Thank you :)

@Arsnael

Arsnael commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

And test errors are related to this PR I believe, please have a look too : https://ci-builds.apache.org/job/james/job/ApacheJames/job/PR-3193/1/testReport/

@HesandaLiyanage

Copy link
Copy Markdown
Contributor Author

Hi @Arsnael,

Thank you very much for the review and guidance!

1. CI Test Failures Fixed

The failures in CassandraBlobIdRepairerIntegrationTest were caused by an overzealous loop guard in ChunkedBlobStoreDAO.readChunkSlotWithRepair. When blobIdRepairer.repair() resolved a corrupted slot reference to its canonical counterpart slot reference, the method rejected it because ChunkId.isChunkRef(repairedBlobId) was true, re-throwing ObjectNotFoundException.

  • Updated ChunkedBlobStoreDAO to directly resolve repaired chunk slot references without re-entering the repair loop, preserving loop protection only when repairedBlobId.equals(originalBlobId).
  • Added unit test coverage (readChunkSlotShouldTriggerRepairWhenRepairedToAnotherSlotRef) in ChunkedBlobStoreDAOTest.
  • Verified both CassandraBlobIdRepairerIntegrationTest and CassandraBlobIdUpdaterIntegrationTest pass locally against Cassandra testcontainers.

2. Broken Down Into 6 Logical Step Commits

I have restructured the work from a single monolithic commit into 6 focused, incremental commits:

  1. Commit 1 (eadb532d): JAMES-4231 ADR: Architecture Decision Record for S3 Object Compaction
    • Documents the architectural context, chunk binary layout, Zstd per-slot compression, crash-safety guarantees, and Bloom filter GC invariants.
  2. Commit 2 (c3a6b84c): JAMES-4231 Blob API: Add range read contract and ChunkMarker support
    • Adds BlobStoreDAO.readRange API contract, RangeByteSlice, and ChunkMarker discriminator.
  3. Commit 3 (366bcd22): JAMES-4231 S3 BlobStore: Implement byte range reads for S3BlobStoreDAO
    • Implements S3 HTTP range requests with S3 416 (RequestedRangeNotSatisfiable) handling, contract tests, and MinIO integration tests.
  4. Commit 4 (fa6091bd): JAMES-4231 Storage Strategy: Exclude compacted chunks in BloomFilter GC
    • Updates BloomFilterGCAlgorithm to safely ignore compacted chunk objects during GC sweeps.
  5. Commit 5 (6413283e): JAMES-4231 Compaction Engine: Add chunk format, ChunkedBlobStoreDAO, and compaction tasks
    • Implements ChunkFormat, ChunkId, ChunkedBlobStoreDAO, windowed candidate streaming BlobCompactionAlgorithm, BlobCompactionTask, and unit test suite (48 tests).
  6. Commit 6 (71d52345): JAMES-4231 Cassandra & WebAdmin: Add metadata updater/repairer and WebAdmin API
    • Implements Cassandra reference mapping, updater, and self-healing repairer; wires BlobCompactionModule in Guice; exposes DELETE /blobs?scope=compaction in WebAdmin.

Please let me know if you would like any further adjustments!

@chibenwa chibenwa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hello,

Thanks a lot for this initial contribution that is going overall in the good direction.

I did do a quick initial review of this work, and have already significant structural comments that I would like to see addressed.

Also I believe we would need to have more integration tests:

Given X, Y Z 200 mail population, compaction yield x, y, z object layout afterward, and we matches integrity constraints (no metadata -> 404). We would need that to be performed on ~5 different layouts, probably on top of memory blob store for decent testing time.

We would also need tests that account for failures updating references and ensure that we remain coherent.

I to not think I saw mailbox/cassandra ObjectStorageNotFoundException upon reading blobs from imapUidTable / messageIdTable - this detail is important for correctness as these table can create entry with past generation blobs (COPY / MOVE)

We are missing /docs documentation for this.

Wouldn't you mind addressing those feedbacks?

Comment thread server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java Outdated
@Arsnael

Arsnael commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

hello,

The structure looks better yes thank you @HesandaLiyanage

https://ci-builds.apache.org/job/james/job/ApacheJames/job/PR-3193/2/

New test failures still related to this work. It seems some guice bindings are missing:

  1. [Guice/MissingImplementation]: No implementation for BlobIdUpdater was bound.

Requested by:
1 : BlobCompactionModule.blobCompactionAlgorithm(BlobCompactionModule.java:63)
_ for 4th parameter blobIdUpdater
at BlobCompactionModule.blobCompactionAlgorithm(BlobCompactionModule.java:63)
_ installed by: Modules$OverrideModule -> Modules$OverrideModule -> Modules$OverrideModule -> Modules$OverrideModule -> Modules$OverrideModule -> Modules$OverrideModule -> Modules$CombinedModule -> Modules$CombinedModule -> Modules$CombinedModule -> BlobCompactionModule

Learn more:
https://github.com/google/guice/wiki/MISSING_IMPLEMENTATION

  1. [Guice/MissingImplementation]: No implementation for BlobReferenceMappingSource was bound.

Requested by:
1 : BlobCompactionModule.blobCompactionAlgorithm(BlobCompactionModule.java:63)
_ for 3rd parameter mappingSource
at BlobCompactionModule.blobCompactionAlgorithm(BlobCompactionModule.java:63)
_ installed by: Modules$OverrideModule -> Modules$OverrideModule -> Modules$OverrideModule -> Modules$OverrideModule -> Modules$OverrideModule -> Modules$OverrideModule -> Modules$CombinedModule -> Modules$CombinedModule -> Modules$CombinedModule -> BlobCompactionModule

Learn more:
https://github.com/google/guice/wiki/MISSING_IMPLEMENTATION

2 errors

@HesandaLiyanage

Copy link
Copy Markdown
Contributor Author

Hi @chibenwa and @Arsnael,

All requested changes and feedbacks have been addressed and pushed as individual atomic commits:

1. Structural & Algorithmic Improvements

  • BlobStoreDAO API: readRange now returns Publisher<Blob> / Mono<Blob>, allowing ChunkedBlobStoreDAO to propagate blob metadata and slot CRC checks through the blob store hierarchy.
  • Zstd Delegation: Removed decompression from BlobSlot / ChunkedBlobStoreDAO; chunk slots preserve content-encoding and content-original-size so ZstdBlobStoreDAO handles decompression transparently.
  • Metadata Preservation: Chunk format and slot serialization now preserve and restore all blob metadata in chunk slots.
  • Prefix Pushdown: Implemented generation prefix pushdown when listing candidate blobs with BlobStoreDAO.listBlobs(bucket, prefix).
  • Pre-Windowing Size Filtering: Candidate blobs exceeding maxPackableSize are filtered out prior to windowing and kept as standalone objects.
  • Size-Based Windowing: Candidate windowing is now driven by cumulative byte size up to chunkTargetSize rather than pure item count.
  • Single Candidate Handling: Batches with a single candidate are skipped without packing into chunks.
  • Dedicated Deletion Method: Extracted reference replacement and original blob deletion into updateReferencesAndDeleteOriginalBlobs.
  • Streaming References: Replaced in-memory reference loading with on-demand windowed queries against BlobReferenceMappingSource to avoid heap exhaustion.
  • Two Distinct Tasks: Split compaction into InitialBlobCompactionTask (for standalone blobs of a completed generation) and GCBlobCompactionTask (for reclaiming space from dead slots and merging small chunks), each with its own DTO and WebAdmin scope.

2. Guice Dependencies in Non-Cassandra Environments

  • Fixed Guice wiring in BlobCompactionModule to make BlobIdUpdater and BlobReferenceMappingSource optional when non-Cassandra backends (e.g. Postgres, Memory) are used, resolving the CI failure noted by @Arsnael.

3. Cassandra Mailbox Mappers Fallback

  • Added onErrorResume(ObjectNotFoundException.class, ...) in both CassandraMessageMapper and CassandraMessageIdMapper on the FetchType.HEADERS fast path, falling back to messageDAOV3 which retains canonical compacted chunk slot references.

4. Coherence Under Failure Integration Tests

  • Added integration tests in BlobCompactionAlgorithmTest verifying store coherence when reference updates fail during initial compaction and GC recompaction (original standalone blobs and chunks are preserved, readable, and consistent).

5. Layout Matrix Integration Tests (200 Mails)

  • Added BlobCompactionLayoutIntegrationTest on MemoryBlobStoreDAO covering 5 distinct layout scenarios (200 mails each):
    1. Uniform small population (all active): yields single chunk, standalone deleted, integrity constraints verified.
    2. Partially unreferenced population (50% unreferenced): compacts only active candidates, preserves uncompacted.
    3. Mixed sizes with oversized blobs: preserves oversized standalone blobs, compacts remaining.
    4. Multi-generation interleaved population: compacts only target generation blobs.
    5. Two-stage compaction with subsequent deletion and GC recompaction: purges dead slots, yields new compact chunk, and ensures non-existent metadata returns 404.

6. Documentation

  • Added documentation in docs/ covering:
    • Architecture: ChunkedBlobStoreDAO and S3 Object Compaction concepts (docs/modules/servers/partials/architecture/blobstore.adoc).
    • WebAdmin: endpoints for initial compaction, GC recompaction, and full compaction (docs/modules/servers/partials/operate/webadmin.adoc).

All changes are completed and tested. Thank you both for the detailed review!

@chibenwa

chibenwa commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Thanks for all the fixes.

That's a major changeset. I really woupd support (out of the CI) Property Based Testing for this so that we can develop confidence on it. ( https://www.scalatest.org/user_guide/property_based_testing for instance ) it should be able to generate large set of object within a generation, perform compact and delete in random manner under various profiles (delete heavy or not - configurable and tested accross a wide range of value, possibly randomly set between each compacts) and ensure along several runs that 1- count of object decrease in a predicable fashion and 2- no dead references are created.

The review is massive, and I do not want to delegate this to an AI, I'll try to cover a review of this by the end of the week.

Cheers,

Benoit

@HesandaLiyanage

Copy link
Copy Markdown
Contributor Author

Thanks for all the fixes.

That's a major changeset. I really woupd support (out of the CI) Property Based Testing for this so that we can develop confidence on it. ( https://www.scalatest.org/user_guide/property_based_testing for instance ) it should be able to generate large set of object within a generation, perform compact and delete in random manner under various profiles (delete heavy or not - configurable and tested accross a wide range of value, possibly randomly set between each compacts) and ensure along several runs that 1- count of object decrease in a predicable fashion and 2- no dead references are created.

The review is massive, and I do not want to delegate this to an AI, I'll try to cover a review of this by the end of the week.

Cheers,

Benoit

Thanks so much, really appreciate you taking the time on this.

Yeah let's do the property based testing. that's way better than me just eyeballing it after a few manual runs. i'll start working around with that on my side in the meantime.

looking forward to your feedback by end of week, no rush though given how big this changeset is.

@chibenwa

Copy link
Copy Markdown
Contributor

@HesandaLiyanage I see you being a student in your github profile.

Would you be interested in a Google Summer of Code with the ASF James project ? If this is meaningful for you we could keep going on the S3 compaction stuff and plan eventually S3 GC / compaction implemented as Apache Spark jobs (for instance). I think the timing is not bad for 2027, though I recognize I do not have the precise schedule in mind.

@HesandaLiyanage

Copy link
Copy Markdown
Contributor Author

@HesandaLiyanage I see you being a student in your github profile.

Would you be interested in a Google Summer of Code with the ASF James project ? If this is meaningful for you we could keep going on the S3 compaction stuff and plan eventually S3 GC / compaction implemented as Apache Spark jobs (for instance). I think the timing is not bad for 2027, though I recognize I do not have the precise schedule in mind.

Yes, absolutely! I would love to participate in Google Summer of Code with James project.

Taking on S3 compaction and exploring large-scale S3 GC/compaction with Apache Spark sounds like a fantastic project, and I'd be honored to work on it with you and the community.

@chibenwa

Copy link
Copy Markdown
Contributor

Taking on S3 compaction and exploring large-scale S3 GC/compaction with Apache Spark sounds like a fantastic project, and I'd be honored to work on it with you and the community.

I'm glad to hear that.
Time schedule is not for now - Jan / february to have mentoring organisation which asf is generally part off.
Mid-march: opens for project.
Let's remember this schedule and get S3 compaction working first.


This discussion cannot stay solely on github. I propose you to introduce yourself on the DEV mailing list, quote your objectives, link this PR, and potentially say that you would eventually be interested by a GSOC proposed by Benoit to continue the work started on S3 compaction task but make it more scalable with Spark.

@HesandaLiyanage

HesandaLiyanage commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Taking on S3 compaction and exploring large-scale S3 GC/compaction with Apache Spark sounds like a fantastic project, and I'd be honored to work on it with you and the community.

I'm glad to hear that. Time schedule is not for now - Jan / february to have mentoring organisation which asf is generally part off. Mid-march: opens for project. Let's remember this schedule and get S3 compaction working first.

This discussion cannot stay solely on github. I propose you to introduce yourself on the DEV mailing list, quote your objectives, link this PR, and potentially say that you would eventually be interested by a GSOC proposed by Benoit to continue the work started on S3 compaction task but make it more scalable with Spark.

Sounds good! I'll introduce myself on the dev mailing list and keep working on the S3 compaction for now. Thank you so much for the opportunity .

@Arsnael

Arsnael commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

https://ci-builds.apache.org/job/james/job/ApacheJames/job/PR-3193/3/testReport/

Related errors on CassandraBlobIdRepairerIntegrationTest. Please help review and fix @HesandaLiyanage :)

@prosgarz35

Copy link
Copy Markdown
Contributor

Which S3 should be used now? SeaweedFS? Silo? RustFS? Garage? ofc i dont want get my data corrupted.

@Arsnael

Arsnael commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Which S3 should be used now? SeaweedFS? Silo? RustFS? Garage? ofc i dont want get my data corrupted.

That's a good question. We had a try at triaging alternatives to MinIO earlier this year, as you can see here: https://issues.apache.org/jira/browse/JAMES-4167

It wasn't very conclusive back then unfortunately. But maybe now it's worth taking an other look if you want a nice little extra challenge ;) Specially now that MinIO removed all their public images it seems...

I cans ee RustFS for example now released in a v1.0.0 not long ago. Maybe that means they are finally on a more stable and mature version of their product. It would be worth getting an other shot at it IMO

@prosgarz35

prosgarz35 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Which S3 should be used now? SeaweedFS? Silo? RustFS? Garage? ofc i dont want get my data corrupted.

That's a good question. We had a try at triaging alternatives to MinIO earlier this year, as you can see here: https://issues.apache.org/jira/browse/JAMES-4167

It wasn't very conclusive back then unfortunately. But maybe now it's worth taking an other look if you want a nice little extra challenge ;) Specially now that MinIO removed all their public images it seems...

I cans ee RustFS for example now released in a v1.0.0 not long ago. Maybe that means they are finally on a more stable and mature version of their product. It would be worth getting an other shot at it IMO

i tested with James RustFS and for now its bad, about 60-70% compability (used warp to test) also not even close to Silo (MinIO fork 99-100%) and SeaweedFS (which is about 85% combability) perfomance (but yeah much lower ram usage)

I tesing RustFS 1.0.0 version, both original and from sources. But you right... there NO real options for now, for standalone single server setup (postgres-app) i think need focus on RustFS combability, for clusters (distribution-app) CephFS i think? Because SeaweedFS dont have full claster support in opensources version, Garage much worse than Seaweed in any way (compability/perfomance, latency) and not working on Windows (but i DO custom version and make it WORK on windows from sourses and even THIS show better than RustFS...)

So what you think?
RustFS focus for postgres-app
CephFS focus for distribution-app
Seems good?

@prosgarz35

Copy link
Copy Markdown
Contributor

Also why only S3 or PostgreSQL? Why not filesystem like in Stalwart and Dovecot?

@HesandaLiyanage

Copy link
Copy Markdown
Contributor Author

https://ci-builds.apache.org/job/james/job/ApacheJames/job/PR-3193/3/testReport/

Related errors on CassandraBlobIdRepairerIntegrationTest. Please help review and fix @HesandaLiyanage :)

Done!. ChunkedBlobStoreDAO returns slot content tagged with ContentEncoding.ZSTD (decompression is delegated to ZstdBlobStoreDAO in production). Updated CassandraBlobIdRepairerIntegrationTest to decompress the slot payload before asserting content equality. Both tests pass locally and checkstyle is clean.

@prosgarz35

Copy link
Copy Markdown
Contributor

https://ci-builds.apache.org/job/james/job/ApacheJames/job/PR-3193/3/testReport/
Related errors on CassandraBlobIdRepairerIntegrationTest. Please help review and fix @HesandaLiyanage :)

Done!. ChunkedBlobStoreDAO returns slot content tagged with ContentEncoding.ZSTD (decompression is delegated to ZstdBlobStoreDAO in production). Updated CassandraBlobIdRepairerIntegrationTest to decompress the slot payload before asserting content equality. Both tests pass locally and checkstyle is clean.

so now not James itself makes compression - now it doings S3 storage?

@HesandaLiyanage

HesandaLiyanage commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

so now not James itself makes compression - now it doings S3 storage?

No, James itself still does all the compression. S3 is just an object store that receives and stores the bytes James sends it.

ZstdBlobStoreDAO (James) ➔ ChunkedBlobStoreDAO (James) ➔ S3BlobStoreDAO (S3)

@prosgarz35

Copy link
Copy Markdown
Contributor

https://ci-builds.apache.org/job/james/job/ApacheJames/job/PR-3193/3/testReport/
Related errors on CassandraBlobIdRepairerIntegrationTest. Please help review and fix @HesandaLiyanage :)

Done!. ChunkedBlobStoreDAO returns slot content tagged with ContentEncoding.ZSTD (decompression is delegated to ZstdBlobStoreDAO in production). Updated CassandraBlobIdRepairerIntegrationTest to decompress the slot payload before asserting content equality. Both tests pass locally and checkstyle is clean.

so now not James itself makes compression - now it doings S3 storage?

No, James itself still does all the compression. S3 is just an object store that receives and stores the bytes James sends it.

ZstdBlobStoreDAO (James) ➔ ChunkedBlobStoreDAO (James) ➔ S3BlobStoreDAO (S3)

well i looking forward to move this task to RustFS S3 for example which can compress by myself with zstd/lz4, just as for me the more mail server dont do others jobs the better it doing main job - mail exchanging.

@HesandaLiyanage

HesandaLiyanage commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

well i looking forward to move this task to RustFS S3 for example which can compress by myself with zstd/lz4, just as for me the more mail server dont do others jobs the better it doing main job - mail exchanging.

yeah fair enough, and compression in james isn't forced either way, if the underlying s3 engine you're using already handles it at the storage layer, ceph or rustfs, you can just turn it off on the james side with blobstore.compression.enabled=false and let the backend do its thing.

but for plain cloud s3 like aws, wasabi, backblaze b2 etc, none of those do any packing or compression on their own, so having james handle it out of the box still matters a lot there, otherwise your put costs and storage footprint climb pretty fast.

@prosgarz35

Copy link
Copy Markdown
Contributor

https://ci-builds.apache.org/job/james/job/ApacheJames/job/PR-3193/3/testReport/
Related errors on CassandraBlobIdRepairerIntegrationTest. Please help review and fix @HesandaLiyanage :)

Done!. ChunkedBlobStoreDAO returns slot content tagged with ContentEncoding.ZSTD (decompression is delegated to ZstdBlobStoreDAO in production). Updated CassandraBlobIdRepairerIntegrationTest to decompress the slot payload before asserting content equality. Both tests pass locally and checkstyle is clean.

so now not James itself makes compression - now it doings S3 storage?

No, James itself still does all the compression. S3 is just an object store that receives and stores the bytes James sends it.
ZstdBlobStoreDAO (James) ➔ ChunkedBlobStoreDAO (James) ➔ S3BlobStoreDAO (S3)

well i looking forward to move this task to RustFS S3 for example which can compress by myself with zstd/lz4, just as for me the more mail server dont do others jobs the better it doing main job - mail exchanging.

yeah fair enough, and compression in james isn't forced either way, if the underlying s3 engine you're using already handles it at the storage layer, ceph or rustfs, you can just turn it off on the james side with blobstore.compression.enabled=false and let the backend do its thing.

but for plain cloud s3 like aws, wasabi, backblaze b2 etc, none of those do any packing or compression on their own, so having james handle it out of the box still matters a lot there, otherwise your put costs and storage footprint climb pretty fast.

yeah customizations always wins! so yeah for cloud its sure userful, also only zstd for now? i think in some cases lz4 needed (lower latency). And deduplication methods its like in ZFS? Just Windows Post-processing dedup wins for now.

@HesandaLiyanage

Copy link
Copy Markdown
Contributor Author

ZFS?

Good questions, though i think this is kinda drifting outside the scope of this specific PR, lz4 vs zstd tradeoffs and dedup approaches would probably get a lot more visibility on the dev mailing list or as a github discussion rather than buried in a PR thread.

Really appreciate you thinking through this stuff though, i think its better if we keep this thread focused on the compaction review and the CI fixes for now

@chibenwa

Copy link
Copy Markdown
Contributor

Also why only S3 or PostgreSQL? Why not filesystem like in Stalwart and Dovecot?

We used to have a poorly performing file maildir backend.

We deprecated it: bad performance, directory traversal, no maintainer, no known deployment.

No, James itself still does all the compression. S3 is just an object store that receives and stores the bytes James sends it.

If options exist to tell s3 to compress and do the job that's nice. I am not aware TBH and we could add support for this in S3BlobStoreDAO

ZstdBlobStoreDAO should work out of the box atop file implem. If this is not the case a separated PR is welcomed.

lz4

Bad choice, it won't compress base64 as it doesn't do entropy coding. Most benefit of compression is the base64 overhead on large attachment, which is capped at ~33%.

RustFS S3 for example which can compress by myself with zstd/lz4

If it is standard (ie amazon s3 supports this) then it is welcomed in S3BlobStoreDAO

yeah fair enough, and compression in james isn't forced either way, if the underlying s3 engine you're using already handles it at the storage layer, ceph or rustfs, you can just turn it off on the james side with blobstore.compression.enabled=false and let the backend do its thing.

Yes precisely.

Also bear in mind that one can implement out-of-band-compression, and solely compress old 3+ month old mail as a tiering strategy... I do not know if RustFS for instance is able to automate that with a S3 policy.

but for plain cloud s3 like aws, wasabi, backblaze b2 etc

Event worse if one operate a on prem S3 compatible system like Ceph Rados Gateway.

also only zstd for now ?

Lz4 is not fit cf earlier entropy coding point.

Zstd had decent java port.

More niche algorithm would need to have good perf java implems.

I confess I do not have in mind viable java alternative, but with hashing I did bench SHA-256 vs BLAKE3 and fornow JVM implenm of BLAKE3 is worse than SHA-256. I'd be careful with exotic implem and have their viability backed with a JMH micro benchmark.

And deduplication methods its like in ZFS?

You are free to drop james dedup and experiment with FS level dedup.

My take on it is that FS do not have all the infos James have and would be doing more work to achieve partial results. I'm expecting it not to deduplicate as well as James can do but I do not have solid comparison to answer you and the question indeed is interesting.

Good questions, though i think this is kinda drifting outside the scope of this specific PR

Completly, but this is a nice discussion!

Cheers,

Benoit

@chibenwa

Copy link
Copy Markdown
Contributor

would probably get a lot more visibility on the dev mailing list

I confirm this is the right way to go!

@chibenwa

Copy link
Copy Markdown
Contributor

So what you think?
RustFS focus for postgres-app
CephFS focus for distribution-app
Seems good?

While I 100% subscribe to the point as a James integrator,
The ASF james project users should be able to make their own choice on this complicated topic.
And we can discuss this important matter and exchange opinions on eg the user mailing list.

@HesandaLiyanage

Copy link
Copy Markdown
Contributor Author

thanks for laying all that out, @chibenwa , appreciated.

for file/postgres, since ZstdBlobStoreDAO is just a decorator, getting it properly wired and tested on top of FileBlobStoreDAO and PostgresBlobStoreDAO seems like its own clean follow up PR too. I can work on that

@chibenwa chibenwa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One hour review...

I did not finished reviewing BlobCompactionAlgorithm - it is not conform to the design - I thus stopped.

There's already a lot to fix.

Please ask explicitly for a review (reviewer feature on Github) if needed.

Cheers.

Benoit (tired)

Comment thread docs/modules/servers/partials/architecture/blobstore.adoc
Comment thread docs/modules/servers/partials/architecture/blobstore.adoc
Comment thread docs/modules/servers/partials/operate/webadmin.adoc Outdated
Comment thread docs/modules/servers/partials/operate/webadmin.adoc Outdated
Comment thread docs/modules/servers/partials/operate/webadmin.adoc Outdated
@HesandaLiyanage

HesandaLiyanage commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Benoit (tired)

thanks benoit, appreciate you pushing through this even while tired.
fixed everything from your reviews so far, gonna run another pass with copilot and clean up more before pinging you again. take your time.

cheers,
hesanda (🙏)

@HesandaLiyanage

Copy link
Copy Markdown
Contributor Author

Hello @chibenwa , I’ve completed several rounds of reviews and polished the pr more.

@HesandaLiyanage

Copy link
Copy Markdown
Contributor Author

@chibenwa Ping

@Arsnael

Arsnael commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Can we have a cleanup of the git commits into something more understandable for a clear history of the PR implem? It would help following for a better review as well

@HesandaLiyanage

Copy link
Copy Markdown
Contributor Author

Can we have a cleanup of the git commits into something more understandable for a clear history of the PR implem? It would help following for a better review as well

Hello !!
Done. Restructured the PR down to 8 commits.

@chibenwa

chibenwa commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Hello @HesandaLiyanage

Thanks for your work.

I'll try to commit to do a review this weekend.

Cheers,

Benoit

@chibenwa chibenwa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Kid woke up I did not go to the heart of it
(BlobCompactionAlgoritm)
We are clearly improving in terms of quality but this change set is way to big to get though as is. And 100+ comments is too much to keep tracking efficiently.
I would advocate to split it in subparts that each make sense. Can you please propose on server-dev@james.apache.org mailing list or on the JIRA ticket a list of small/smaller PRs ?
We can keep this PR open as a draft / a reference.
I'd have a reference branch JAMES-4231 on the repo to handle this as a eature branch.
Best regards,
Benoit

Comment on lines +80 to +82
or server-side encryption (SSE-S3 / SSE-KMS / SSE-C); it is incompatible with client-side
`AESBlobStoreDAO` encryption because random byte ranges in AES ciphertext cannot be decrypted
in isolation. If server-side encryption with customer-provided keys (SSE-C) is used, ensure

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is false.

AES cannot be appled after chunking but can be applied before.

It is the order of composition of the Blob stores that matters.

Indeed AES needs to be applied before (each blob independently encrypted)

To compact standalone blobs of a completed generation into chunk objects:

....
curl -XDELETE "http://ip:port/blobs?action=initial-compaction&generation=1"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The verb "DELETE" looks weird, how about "POST" ?

}

private final CassandraAsyncExecutor cassandraAsyncExecutor;
private final Map<BlobId, Set<UUID>> references;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This won't fit in memory.

We could envision a separated table IMO to store this...

.then();
}

private Mono<Void> updateMessageReferences(UUID messageUuid, String oldIdStr, String newIdStr) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This method is too complex and deserves to be extracted in small parts

.then();
}

private Mono<Void> updateMessageReferences(UUID messageUuid, String oldIdStr, String newIdStr) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This method is too complex and deserves to be extracted in small parts

* A chunk consists of sequential slot payloads followed by a trailing footer:
* [FORMAT_BYTE (1B)] [SLOT_1] [SLOT_2] ... [SLOT_N] [FOOTER_TEXT] [FOOTER_LEN (4B)] [FOOTER_POS (8B)]
*/
public class ChunkFormat {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Long complicated methods in here too. Code extraction needed...

Comment on lines +150 to +164
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof InitialBlobCompactionTask that) {
return Objects.equals(request, that.request);
}
return false;
}

@Override
public int hashCode() {
return Objects.hash(request);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need equals and hashcode for tasks ?

Comment on lines +143 to +157
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof GCBlobCompactionTask that) {
return Objects.equals(request, that.request);
}
return false;
}

@Override
public int hashCode() {
return Objects.hash(request);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not needed for tasks IMO


@Provides
@Singleton
public Optional<BlobCompactionAlgorithm> optionalBlobCompactionAlgorithm(Injector injector) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pass the wanted fields as parameter instead of injector

return new BlobCompactionTask(algorithm, buildCompactionRequest(request), clock);
}

private CompactionRequest buildCompactionRequest(Request request) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code extraction...

@HesandaLiyanage
HesandaLiyanage marked this pull request as draft October 8, 2026 06:01
@HesandaLiyanage

Copy link
Copy Markdown
Contributor Author

Kid woke up I did not go to the heart of it (BlobCompactionAlgoritm) We are clearly improving in terms of quality but this change set is way to big to get though as is. And 100+ comments is too much to keep tracking efficiently. I would advocate to split it in subparts that each make sense. Can you please propose on server-dev@james.apache.org mailing list or on the JIRA ticket a list of small/smaller PRs ? We can keep this PR open as a draft / a reference. I'd have a reference branch JAMES-4231 on the repo to handle this as a eature branch. Best regards, Benoit

Hey @chibenwa , Yes agree, 8k+ lines in one PR is way too much to follow. Im switching #3193 to draft so it stays around as the reference implementation.

im going to split it into 8 smaller sub PRs, most under 300 lines, with the core engine in its own module. i'm putting the breakdown on server-dev and the JIRA ticket now so you can check it before i start.

once the JAMES-4231 feature branch exists on the repo i'll open the sub PRs against it in order.

Thanks for the guidance !!!

@HesandaLiyanage

Copy link
Copy Markdown
Contributor Author

@chibenwa

Based on the feedback, I propose splitting the work into these incremental PRs:

  1. Documentation: ADR-0076 and compaction documentation.
  2. Blob API: range-read contract, ChunkMarker, and BlobIdUpdater SPI.
  3. S3: byte-range reads in S3BlobStoreDAO, with integration coverage.
  4. GC: exclude virtual chunk addresses from BloomFilter GC.
  5. Compaction core: a self-contained blob-compaction module, including the requested property-based tests.
  6. Cassandra: reference updates and fallback reconciliation.
  7. PostgreSQL: reference updater and paginated updates.
  8. Wiring and integration: Guice, WebAdmin endpoints, and end-to-end tests.

Does this breakdown and ordering look good? Would it be possible to create a clean JAMES-4231 feature branch in apache/james-project, based on master, for the sub-PRs to target, with a final PR from that branch to master? The current JAMES-4231 branch contains the full implementation, so it can remain the reference but doesn’t provide a shared upstream base.

@chibenwa

chibenwa commented Oct 9, 2026

Copy link
Copy Markdown
Contributor
  1. ADR -> Documentation hould describe what is implemented so let's not do it at the same time.
  2. Blob API I would do range-read in a first PR. Including s3 implem and tests
  3. ChunkMarker & GC: exclude virtual chunk addresses from BloomFilter GC.
  4. BlobIdUpdater SPI and storage API + Cassandra
  5. Postgres
  6. Compaction core: a self-contained blob-compaction module, including the requested property-based tests.
  7. Wiring and integration: Guice, WebAdmin endpoints, and end-to-end tests.
  8. Documentation

With little refinement yes it looks great to me.

@HesandaLiyanage

Copy link
Copy Markdown
Contributor Author
  1. ADR -> Documentation hould describe what is implemented so let's not do it at the same time.
  2. Blob API I would do range-read in a first PR. Including s3 implem and tests
  3. ChunkMarker & GC: exclude virtual chunk addresses from BloomFilter GC.
  4. BlobIdUpdater SPI and storage API + Cassandra
  5. Postgres
  6. Compaction core: a self-contained blob-compaction module, including the requested property-based tests.
  7. Wiring and integration: Guice, WebAdmin endpoints, and end-to-end tests.
  8. Documentation

With little refinement yes it looks great to me.

Thank you. I'll start working on it

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.

4 participants