⚠️ This issue respects the following points: ⚠️
Bug description
Describe the bug
When two requests concurrently generate a preview for the same file, same
size specification (width/height/crop/max), both write to the same
deterministic on-disk path (derived only from fileId + dimensions, not from
the preview's own DB id). Both then attempt PreviewMapper::insert(). One
succeeds; the other receives REASON_UNIQUE_CONSTRAINT_VIOLATION.
In Generator::savePreview(), the losing request's catch block calls:
$this->storageFactory->deletePreview($previewEntry);
Because the on-disk path is shared/deterministic, this deletes the file that
the winning request just wrote — not a file belonging to the loser. The
result: a valid oc_previews DB row survives (the winner's), but the file
on disk is gone. Every subsequent read fails with NotFoundException /
"Unable to open preview stream", and (prior to #63486-style self-healing)
this state is permanent until manually cleaned up.
This is almost certainly the underlying mechanism behind the pattern
reported in #58787, #63513, and related issues: "DB thinks a preview
exists, disk doesn't."
Why this matters for the #63486 fix
The self-healing approach in #63486 (detect missing file on read, drop the
stale row, regenerate) mitigates the symptom effectively, but does not
address this root cause. Since the regeneration triggered by the self-heal
can itself race with another concurrent request for the same spec (e.g.
multiple participants opening a Talk conversation with a freshly-uploaded
attachment at the same time), the same delete-the-winner's-file sequence can
recur immediately after a self-heal, producing a new stale record right
away. We reproduced this directly: after patching in the #63486-style
existence check, a still-missing icon-size preview needed two regeneration
cycles to converge, and disappeared for one participant while another
(whose request happened not to race) saw it fine.
Steps to reproduce
To Reproduce
Hard to reproduce deterministically (it's a race), but reliably triggered
in practice by: multiple Talk participants opening a conversation with a
recently-uploaded image attachment at roughly the same time, causing
concurrent preview-size requests for the same file/spec.
Expected behavior
Expected behavior
The losing request in the UNIQUE_CONSTRAINT_VIOLATION branch should not
delete the on-disk file, since the path is shared with the winner and the
image content is identical (same source file, same spec). It should simply
fetch and return the winner's already-persisted Preview record via
getPreviewForSpecification(), which the code already does immediately
after the (in our view, unnecessary and harmful) delete call.
Nextcloud Server version
34
Operating system
RHEL/CentOS
PHP engine version
None
Web server
Apache (supported)
Database engine version
MariaDB
Is this bug present after an update or on a fresh install?
None
Are you using the Nextcloud Server Encryption module?
Encryption is Disabled
What user-backends are you using?
Configuration report
List of activated Apps
Nextcloud Signing status
Nextcloud Logs
Additional info
Environment
- Nextcloud 34.0.3, on-premise (AlmaLinux, Apache reverse proxy, MariaDB,
Redis for locking/distributed cache)
- Reproduced consistently in a Talk deployment with multiple concurrent
participants receiving image attachments
Related
Bug description
Describe the bug
When two requests concurrently generate a preview for the same file, same
size specification (width/height/crop/max), both write to the same
deterministic on-disk path (derived only from fileId + dimensions, not from
the preview's own DB id). Both then attempt
PreviewMapper::insert(). Onesucceeds; the other receives
REASON_UNIQUE_CONSTRAINT_VIOLATION.In
Generator::savePreview(), the losing request's catch block calls:Because the on-disk path is shared/deterministic, this deletes the file that
the winning request just wrote — not a file belonging to the loser. The
result: a valid
oc_previewsDB row survives (the winner's), but the fileon disk is gone. Every subsequent read fails with
NotFoundException/"Unable to open preview stream", and (prior to #63486-style self-healing)
this state is permanent until manually cleaned up.
This is almost certainly the underlying mechanism behind the pattern
reported in #58787, #63513, and related issues: "DB thinks a preview
exists, disk doesn't."
Why this matters for the #63486 fix
The self-healing approach in #63486 (detect missing file on read, drop the
stale row, regenerate) mitigates the symptom effectively, but does not
address this root cause. Since the regeneration triggered by the self-heal
can itself race with another concurrent request for the same spec (e.g.
multiple participants opening a Talk conversation with a freshly-uploaded
attachment at the same time), the same delete-the-winner's-file sequence can
recur immediately after a self-heal, producing a new stale record right
away. We reproduced this directly: after patching in the #63486-style
existence check, a still-missing icon-size preview needed two regeneration
cycles to converge, and disappeared for one participant while another
(whose request happened not to race) saw it fine.
Steps to reproduce
To Reproduce
Hard to reproduce deterministically (it's a race), but reliably triggered
in practice by: multiple Talk participants opening a conversation with a
recently-uploaded image attachment at roughly the same time, causing
concurrent preview-size requests for the same file/spec.
Expected behavior
Expected behavior
The losing request in the UNIQUE_CONSTRAINT_VIOLATION branch should not
delete the on-disk file, since the path is shared with the winner and the
image content is identical (same source file, same spec). It should simply
fetch and return the winner's already-persisted
Previewrecord viagetPreviewForSpecification(), which the code already does immediatelyafter the (in our view, unnecessary and harmful) delete call.
Nextcloud Server version
34
Operating system
RHEL/CentOS
PHP engine version
None
Web server
Apache (supported)
Database engine version
MariaDB
Is this bug present after an update or on a fresh install?
None
Are you using the Nextcloud Server Encryption module?
Encryption is Disabled
What user-backends are you using?
Configuration report
List of activated Apps
Nextcloud Signing status
Nextcloud Logs
Additional info
Environment
Redis for locking/distributed cache)
participants receiving image attachments
Related