← 1.0.0 scope SAPE-18

Reads share a database instead of queueing behind each other

Status
Done
Version
1.0.0
Component
server, store
Commits
d5a9390, 390e976, 6da5ef3
Opened
ISS-7, ISS-9

Description

database.mutex was a sync.Mutex: every call to one database queued behind every other, read or write. It is now sync.RWMutex (internal/server/server.go:95).

Which of the two a call is cannot be known before the operation is looked up, and the lookup is itself a read of the tree. So invoke takes the read lock, asks store.SharedRead, and a call that turns out to read runs right there on the operation already in hand (server.go:736-750). One that writes drops the read lock, takes the write lock, and looks the operation up again — a declaration could have changed in between, and the one it runs must be the one it holds the lock for.

Store.SharedRead (internal/store/invoke.go:462) shares only when two things hold. The action must not change anything, which is what writes() already decides. And the collection must not be partitioned: for a partitioned collection even a get goes through Collection.into, which opens the partition file if it is not open, creates it if it is new, and then runs expiry, which drops files. A read there is a writer wearing a reader's name.

Why it was in 1.0.0

One reader at a time per database is a ceiling a stranger hits on the first load test, and nobody had measured it. It also had to land before composed operations: the read/write split is what decides which lock a composed call takes, and getting that wrong after the wire is public is not a change you can make quietly.

How it was verified

  1. TestReadsRunTogether (internal/server/parallel_reads_test.go:20) is a timing measurement, not an assertion about locks — a lock is not what a caller pays. Four counts over 4000 documents, run solo and then four at once over a real socket. Serialized measured 3.9–4.0× on this machine; shared measured 1.6–2.1×. The bar sits at 3×, so it fails on queueing rather than on a loaded box. A 2× bar was tried and measured flaky.
  2. TestReadsRunTogetherAtN32 (parallel_reads_test.go:148) repeats it at 32 readers.
  3. TestPartitionedReadsAreWrites (partition_reads_test.go:20) pins the exception, so the partition carve-out is measured rather than commented.
  4. The count is deliberately not a single-key get: a get spends most of its time in the protocol, which is parallel either way and would hide the thing being measured.

390e976 closed a leak this work turned up. Server.Close closed db.pages, which lets go of the leader file only; the per-partition files stayed locked for the life of the process, and the next Server on the same directory was refused with vfs.ErrLocked on a .part file until a finalizer happened to run. It now closes the partitions first, then the leader (server.go:349-363).

What it did not do