Skip to content

[Bug]: Concurrent preview generation for the same spec deletes the winner's file (root cause behind recurring "preview DB record exists but file is missing") #64138

Description

@PeViz

⚠️ This issue respects the following points: ⚠️

  • This is not a troubleshooting question, general support matter, or webserver/proxy problem, but likely a bug (if unsure, ask the Community Help Forum).
  • This issue is not already reported on Github OR solved at the Community Help Forum (I've searched!).
  • I'm using a maintained major version of Nextcloud Server and tested against the latest patch level. (Supported major versions and current patch levels).
  • I agree to follow Nextcloud's Code of Conduct.
  • I've tried my best to provide clear reproduction steps that someone unfamiliar with this bug could use to reproduce it.

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?

  • Default user-backend (database)
  • LDAP/ Active Directory
  • SSO - SAML
  • Other

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

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    0. Needs triagePending check for reproducibility or if it fits our roadmap34-feedbackbug

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions