Skip to content

LockingResourceStore.setRepresentation (PUT) and deleteResource (DELETE) race on parent container dcterms:modified — causes 500 on subsequent reads #2185

Description

@elf-pavlik

AI conclusion but applying the workaround fixed my failing tests

I don't have time at this moment to check this diagnosis myself.


LockingResourceStore.setRepresentation() (PUT) and deleteResource() (DELETE) only acquire a write lock on the child resource, but both operations modify the parent container's metadata (dcterms:modified) via DataAccessorBasedStore.updateContainerModifiedDate(). This read-modify-write on the parent runs outside any parent lock, creating a race condition.

Two concurrent PUTs to different children in the same container can both read the parent's metadata before either writes, producing duplicate dcterms:modified triples in the SPARQL store. Any subsequent RepresentationMetadata.get(DC.terms.modified) then throws "Multiple results for http://purl.org/dc/terms/modified", returning a 500 to the client and effectively bricking the container until the duplicate is manually removed.

Affected methods in LockingResourceStore:

Method Locks Should also lock
addResource (POST) Container ✅ Correct
setRepresentation (PUT) Child only ❌ Parent container
deleteResource (DELETE) Child only ❌ Parent container

Root cause:

LockingResourceStore.setRepresentation() at dist/storage/LockingResourceStore.js calls this.source.setRepresentation(identifier, ...) inside this.locks.withWriteLock(identifier), where identifier is the child resource. Inside the store, DataAccessorBasedStore.writeData() (called for new resources created via PUT) calls this.updateContainerModifiedDate(parent) — this reads, modifies, and writes back the parent container's dcterms:modified — all while holding only the child's lock.

The same pattern exists in deleteResource(), which also calls updateContainerModifiedDate(parent) inside the child's lock.

Contrast: addResource() (POST) correctly locks the container (this.locks.withWriteLock(container)), so the parent metadata update is serialized.

Steps to reproduce:

  1. Configure CSS 8.0.0-alpha.2 with any storage backend (SPARQL, file, etc.)
  2. PUT two or more new resources into the same container concurrently (e.g. Promise.all([PUT /container/a, PUT /container/b]))
  3. GET the container — returns 500

Expected behavior:

Either:

  • setRepresentation and deleteResource should acquire a write lock on the parent container before modifying its metadata, or
  • updateContainerModifiedDate should use an atomic DELETE/INSERT SPARQL update or equivalent atomic metadata write instead of a read-modify-write cycle

Server-side logs:

RepresentationMetadata {Primary} error: Multiple results for http://purl.org/dc/terms/modified
WrappedExpiringReadWriteLocker {Primary} error: Lock expired after 6000ms on

(The lock expiration is a downstream symptom — the long-running container metadata write is blocked by or racing with concurrent operations.)

Environment:

  • CSS version: 8.0.0-alpha.2
  • Affected lockers: all (memory.json, file.json, redis.json) — the locking scope bug is in LockingResourceStore, not the locker implementation
  • Storage backends: all (SPARQL, file, etc.) — the race is in the lock boundary, not the storage layer

Workaround:

Serialize PUTs and DELETEs to children of the same container (no concurrency), or switch to POST (which correctly locks the parent) where possible.

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

    🐛 bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions