Skip to content

Text never releases its file lock when an editing session ends abnormally #9042

Description

@eric-lemesre

Describe the bug

When a document is opened in Text, the app acquires a lock through files_lock. If the editing session ends abnormally — tab closed without a clean teardown, browser crash, session expiry — the lock is never released.

The failure is silent. A desktop client holding a newer local copy retries the upload and gets HTTP 423 Locked forever, with no actionable error surfaced in the web UI or in the client. Files whose local copy happens to be unchanged simply stop syncing in both directions, with no visible sign at all.

Orphan rows are also left behind in oc_text_sessions, with last_contact frozen at the moment the session died. Every stale lock we found had exactly one matching stale session, which suggests both are leaked by the same teardown path.

To Reproduce

  1. Open a Markdown file in Text.
  2. Kill the session abruptly (close the tab while editing, or let the session expire).
  3. Inspect oc_files_lock: a row remains, with owner = "Text" and a negative ttl.
  4. On a desktop client syncing that folder, modify the same file locally.
  5. Watch the web server access log: PUT /remote.php/dav/files/<user>/<path>423, repeating indefinitely.

Expected behavior

The lock is released when the editing session ends, or at the latest when the stale session is garbage-collected.

Observed on a production instance

  • 8 stale locks accumulated over 3 weeks, all owner = "Text", the oldest 20 days old.
  • Each had a matching orphan row in oc_text_sessions.
  • One file had a newer local copy: its desktop client issued a PUT every 2 hours and received 423 each time, for 19 consecutive days.
  • The 7 other files were silently out of sync — discovered only while investigating the first one.

Why nothing cleans this up

files_lock ships a cleanup job, OCA\FilesLock\Cron\Unlock, but with default settings it is a no-op:

  • ConfigService defaults lock_timeout to '-1';
  • FileLock::ETA_INFINITE === -1;
  • LockService::getDeprecatedLocks() returns [] when the timeout equals ETA_INFINITE, so the job deletes nothing.

Setting a finite timeout re-arms the job and is an effective workaround:

occ config:app:set files_lock lock_timeout --value=1440

This bounds the damage but does not fix the leak: the lock is still orphaned, and the file stays unsyncable until the job runs.

Possible second issue, in files_lock

Even with a finite lock_timeout, expiry does not track activity. LocksRequest::update() — the "extending existing lock" path — writes token, ttl, user_id, owner and scope, but not creation, while LocksRequest::getLocksOlderThan() filters on creation. A lock actively extended by an ongoing session can therefore be purged based on its original creation time, so a long editing session may lose its lock mid-edit. This is why we chose a deliberately large timeout above.

Happy to split this into a separate issue against files_lock if you prefer.

Server details:

  • Nextcloud version: 34.0.3 (also observed on 32.x and 33.x before upgrading)
  • PHP Version: 8.4
  • Database: PostgreSQL 17.10 and MariaDB 11.8.6 — two separate instances, both affected
  • files_lock: 34.0.1

Client details:

  • OS: Debian (server), Linux desktop
  • Desktop sync client: 33.0.7
  • Browser: Firefox

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions