Skip to content

Locking storage - #798

Draft
butonic wants to merge 7 commits into
mainfrom
locking-storage
Draft

Locking storage#798
butonic wants to merge 7 commits into
mainfrom
locking-storage

Conversation

@butonic

@butonic butonic commented Sep 10, 2026

Copy link
Copy Markdown
Member

This PR serves as a PoC to make share persistence fully
atomic by replacing etag-based optimistic
locking (compare-and-swap + retry loops) with pessimistic, locked
read-modify-write operations. This removes the fragile CAS retry machinery from the
jsoncs3 share caches (and appauth store) and fixes real lost-update races in the
publicshare manager. It also introduces a new disk-backed
json share-manager driver as an alternative to the CS3-backed jsoncs3
driver, with an in-place migration path between them.

The short version is that I extended the old Storage interface with an UpdateWithLock method and used an Option pattern to inject a memory or disk based lock implementation (mimicking dcomposedfs metadata access).

Why

The jsoncs3 share manager is known not to scale under load:

The retry/CAS approach was patched by opencloud incrementally over time to cope with
transient storage errors and cross-user share removal:

Rather than keep hardening a fundamentally racy pattern, this PR removes the
CAS loops entirely and makes each write a single atomic locked operation.

What changed (7 commits)

  1. new metadata/locks package (MemoryLocker, DiskLocker) +
    LockingStorage capability interface; storage constructors become variadic
    (no behavior change).
  2. Disk.UploadWithLock: lock held across read→fn→write; disk etags
    become content-based (md5) so they act as reliable cache-invalidation tokens.
  3. CS3.UploadWithLock: same atomic semantics over the CS3 provider.
  4. jsoncs3 provider/user/received share caches switch from CAS retry
    loops to one UploadWithLock; in-memory caches refresh in place.
  5. register a disk-backed json share-manager driver + add Dump
    (migration: dump from CS3 → load into disk).
  6. publicshare manager: atomic Update across all three persistence
    backends; fixes the lost-update race in create/update/revoke.
  7. appauth via metadatacache.Store: optional LockingStorage path
    (atomic) with the etag-CAS loop kept as fallback.

Configuration

New lock_backend option (disk default, or memory) on the publicshare and
appauth jsoncs3 managers. Disk locks use <key>.mlock sidecar files, so
multi-replica deployments need a shared (RWX) volume at the data root.

Testing

  • New concurrency tests per layer (N-goroutine same-key updates -> no lost
    updates), run with -race.

Introduce a leaf locks package (MemoryLocker, DiskLocker) and a
LockingStorage capability interface that embeds Storage. Convert the
storage constructors to variadic options so a locker can be injected;
Disk defaults to a DiskLocker rooted at its data dir and CS3 to a
MemoryLocker. No behavior change yet: UploadWithLock is not implemented.
Add UploadWithLock to the disk backend: it holds a lock on the logical
path across read->fn->write so the operation is fully atomic. fn returns
nil to abort without writing. Switch disk etags to be content-based (md5)
so they act as reliable cache-invalidation tokens. Add tests covering
create-only, abort-on-nil, and concurrent counter with both lockers.
Add UploadWithLock to the CS3 backend: it acquires a lock on the logical
path, downloads the existing content (nil if absent), invokes fn, and
uploads the result without etag preconditions since the lock guarantees
exclusivity. Returns a fresh etag for cache invalidation. Add tests for
lock serialization and concurrent same-key updates.
Convert the provider, user-share, and received-share caches from
etag-based compare-and-swap retry loops to a single atomic
storage.UploadWithLock read-modify-write. Caches now hold a
metadata.LockingStorage and refresh their in-memory state in place so
callers holding references observe updates. Add concurrent-add tests
proving no lost updates per cache.
Register a new 'json' share-manager driver backed by the disk storage
(with an injectable locker) alongside the existing CS3-backed 'jsoncs3'
driver. Add a Dump method to the Manager that walks all three caches and
exports shares/received-shares, enabling migration from CS3 to disk. Add
tests for dump export and concurrent-add atomicity on disk.
Add an Update method to the Persistence interface performing a single
atomic read-modify-write. The file backend uses a DiskLocker sidecar and
renameio; the CS3 backend uses LockingStorage.UploadWithLock on
publicshares.json; the memory backend uses an in-process mutex. Replace
the whole-file Read->mutate->Write in CreatePublicShare, UpdatePublicShare
and revokePublicShare with persistence.Update, eliminating the lost-update
race. Add a lock_backend config option and a concurrent create/revoke test.
Add an optional LockingStorage to metadatacache.Options. When set, Store.Update
performs a single atomic UploadWithLock instead of the etag-based CAS retry
loop; otherwise the existing CAS path is unchanged. Wire the CS3 storage as a
LockingStorage in the appauth jsoncs3 manager and add a lock_backend config
option. Add tests covering the locking path including concurrent same-key
updates with no lost updates.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant