nextcloud / nextcloud/text

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

Open
#9,042 1 comment 4 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
659
Forks
133
Avg merge
1d 20h
Merged PRs (30d)
110

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the abrupt-session scenario and trace Text's teardown and stale-session garbage-collection paths. Then inspect files_lock's Cron\Unlock, ConfigService, FileLock, LockService::getDeprecatedLocks(), and LocksRequest::update() and getLocksOlderThan(). Done means stale sessions and their locks are released without breaking active locks or leaving clients stuck with HTTP 423.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, mariadb, php, postgresql
Domain
backend, databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.