I took a few days off work this week and wanted to write some fun code while I was out. Learning more about concurrency management has been on my list for a while, so letβs go with that. Broadly, databases tend to manage concurrency with either (e.g.Β Two-Phase Locking) or (e.g.Β Timestamp Ordering) protocols. In a textbook implementation of OCC, a DB is made serializable by taking no locks during a preliminary working phase and then write-locking the DB during a short validation and write phase.
Today, I roughly implemented Kung and Robinsonβs On Optimistic Methods for Concurrency Control (1981) and wrote a small ( LOC) Go package that can be used to wrap an arbitrary data store and give it OCC semantics. Because direct ports of papers are hardly ever fun, I made the following changes:
I collapse , , and into . These ops are all writes, treating them as such gives us a much tighter implementation.
(π¨) I record for each read a transaction executes, and only validate itβs against committed transactions with up to the last read. This should give us a consistently faster validation phase.
Rather than maintaining a and of object IDs, I represent either as a slice of bits (really ). On read or write, I set a bit in the corresponding array by hashing the objectβs ID. With this optimization, the validation phase is just a few bitwise s. This is effectively a bloom filter, and regrettably means that transactions with many reads will have an inflated commit failure rate.
I bound the size of the log by . Because of , I can allocate one slice proportional to . This gives us access to a committed transactionβs hash. In exchange, we must fail all transactions more than commits old. Fair trade, we take thatβ¦
β I am not strictly anti-Github, but Iβd prefer to share large code snippets without (1) linking to another domain (2) wasting a ton of space inline, or (3) having my code shoveled into the maw of Copilot. Working out some ways to get a gist-like interface onto my website. For the time being, full code for the OCC manager is here locally and on GitHub.
In the example below I initialize a which satisfies and use it to manage several dozen concurrent readers and writers. Acknowledging that this is a trivial example, weβre looking at the ability to verify several million operations per second.
package main
import (
"math/rand"
"sync"
"occ"
)
type noopStore int
func (s noopStore) Write(ds []interface{}) error {
return nil
}
func (s noopStore) Read(id uint64) (interface{}, error) {
return 1, nil
}
func (s noopStore) Merge(old, new interface{}) (interface{}, error) {
return 1, nil
}
func main() {
var (
numEvents, numWorkers int = 1000000, 48
hashBits, txnRetention uint64 = 1024, 10000
numObjects uint64 = 1024 * 1024
workCh = make(chan uint64)
wg = sync.WaitGroup{}
)
DB := occ.NewManager(noopStore(1), hashBits, txnRetention)
wg.Add(numWorkers)
for workerID := 0; workerID < numWorkers; workerID++ {
go func() {
defer wg.Done()
c := DB.NewConnection()
for r := range workCh {
c.Begin()
c.Read(r)
c.Write(r, 1)
c.Commit()
}
}()
}
for i := 0; i < numEvents; i++ {
workCh <- (rand.Uint64() % numObjects)
}
close(workCh)
wg.Wait()
// A little contrived example, `commitRate` subject to fluctuate on quite a few factors,
// number of items read, max concurrency, hashBits, variation in exec time, etc. This
// is as favorable as it gets...
//
// 2024/08/23 13:38:20 commitRate: 0.994779, execTime: 387.828209ms
}Still need a lot of testing for correctness.
Iβm not sure that change above is totally valid, or that we really have serializability. Because of how I separate read and write sets, we donβt even have snapshot isolation right now. Come to think of it, this paper was back when βconsistentβ meant βlinearlizableβ, very hard to align my 2024 vision of things with 1981β¦
Iβm also not sure that has all the properties Iβm looking for in a hash function. Nevertheless, we can always construct our family of hash functions insteadβ¦
Edit 09/01/2024 β Oh no! A new Kyle Kingsbury video hit the internet and it has me considering all the places that I might have fumbled even lower isolation levels π¬. A personal note - I applied to (and was rejected from) Recurse Center last week. My intention was to use the cohort to 1. learn enough Clojure to work effectively with Jepsen and 2. learn more about formal isolation levels. Alas, Iβll just do it on my own time. Nights and Weekends, brother!