Retrieval, retain & release¶
Not yet on Maven Central
dedup4j 0.1.0 has not been published. These coordinates are the
intended release coordinates and cannot be resolved from Maven Central
today. The Maven Central namespace is confirmed in release gate G0 and
this file is the single place it is edited.
Deduplication means one stored object can serve many of your records. That raises a question the library has to answer: when is content safe to delete? dedup4j answers it with reference counting.
The model¶
dedup4j owns physical content. Your application owns meaning.
your records dedup4j
─────────────────────────────────────────────────
Attachment #1 ──┐
Attachment #2 ──┼──► assetContentId ──► one stored object
Document #7 ──┘ refCount: 3
Three of your records, one copy of the bytes, one counter. The counter is dedup4j's only knowledge of how many of your records exist.
Retrieval¶
BlobDeduplicationService reads content back:
try (BlobResource resource = blobs.get(assetContentId)) {
resource.content(); // open InputStream — you close it
resource.sizeBytes();
resource.contentType();
resource.metadata();
}
BlobResource implements AutoCloseable and holds an open stream. Use
try-with-resources; a leaked resource here is a leaked connection to your
object store.
For location without content:
BlobLocation location = blobs.location(assetContentId);
location.provider();
location.bucketOrContainer();
location.objectKey();
Why there are no presigned URLs¶
dedup4j does not issue presigned URLs
Content streams through your application. There is no
getDownloadUrl(id).
This is a deliberate boundary, not a missing feature. A presigned URL is an authorization decision — who may read this, for how long, under what audit trail — and dedup4j does not know your authorization model. Issuing one on your behalf would mean guessing it.
location() gives you everything needed to generate one with your provider's
SDK, at the point in your code where you do know who is asking.
The cost is real: streaming through the application uses application bandwidth
where a presigned URL would not. For large files behind an authorization
check, build the URL yourself from location().
Retain and release¶
Both take a pessimistic row lock for the duration of the caller's transaction. Two concurrent requests touching the same content serialise rather than race.
What happens at zero¶
release decrements, and when the count reaches zero it deletes the stored
object immediately, within the same transaction as the decrement.
There is no grace period, no soft delete, and no background sweeper. A release that takes the count to zero destroys the bytes.
There is no switch for this
Deletion at zero is unconditional. No configuration disables it, so the reference count is the only thing that decides whether bytes survive.
Rolling back does not restore the object
The delete goes to the object store, which has no transaction. If your transaction rolls back after the delete, the database row returns and the bytes do not.
This is the same non-atomicity described in Architecture & limitations, seen from the deletion side.
Under-release is rejected¶
Releasing content already at zero throws ReferenceCountUnderflowException
rather than silently doing nothing. A count that has lost track of reality is
a bug worth surfacing.
The rule¶
Every logical reference needs exactly one count.
store already handles the common case: new content is created at a count of
one, and a duplicate store retains the existing content. So one store = one
reference, needing one release when that record goes away.
Call retain only when you create a record pointing at content you did not
just store — copying an existing attachment onto a second document, for
example.
| You did | Then |
|---|---|
store (new or duplicate) |
already counted — just release later |
| Added a record without storing | retain now, release later |
| Deleted a record | release |
Count too low and content is deleted while records still point at it. Count too high and content accumulates that nothing will ever collect.
Reconciliation¶
Counts drift. A crash between your write and your retain, a bug, a manual
database fix — and dedup4j's count no longer matches your records.
ReconciliationService compares the two.
Not auto-configured
Unlike BlobStore and BlobDeduplicationService, the starter does not
register a ReconciliationService bean. You construct it yourself, which
is also where you decide whether repair is enabled.
@Bean
ReconciliationService reconciliationService(
AssetContentRepository repository,
ReferenceCountService referenceCountService) {
return new ReconciliationService(repository, referenceCountService, false);
// repair enabled ─────┘
}
ReconciliationReport report = reconciliationService.reconcile(source);
report.checkedContentCount();
report.mismatches();
You supply a LogicalReferenceCountSource — dedup4j cannot enumerate your
records, so you tell it what the counts should be.
Reporting is read-only by default
reconcile only reports. Repair is a separate operation, disabled
unless explicitly enabled when the service is constructed.
That default is deliberate: automatic repair against a faulty
LogicalReferenceCountSource would corrupt correct counts at machine
speed. Look at a report before enabling repair.
When repair is enabled, adjustments go through the same lock-aware reference-count service, and the caller must keep a transaction open for the duration.
Next¶
- Uploading & deduplication — the store side
- Observability & dashboards — watching counts and drift
- Architecture & limitations — why atomicity is not offered