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.

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