/* * Copyright 2025 Casper Dalgaard Nielsen * Adam Yasser Tallouzi * * Use of this source code is governed by the Apache 2.0 license * that can be found in the LICENSE.md file. */////// Type of optimistic read-write locks, and its functions.///pubmod BPlusTree.Lock {import java.util.concurrent.locks.{StampedLock => JStampedLock}////// Type of optimistic read-write locks.////// Wraps a Java `StampedLock` without the IO effect. The lock is optimistic in the/// sense that reads are performed by reading the values and only afterwards verifying/// that no other thread has changed the values. Only annotating a region effect to StampedLock is safe, since it can/// only non-deterministically block a thread for some ( potentially infinite) amount/// of time. This is allowed under non-effectful behavior.///////// One possible danger, introduced by StampedLock, is handled by a null-check in/// binarySearch. Consider the following example where everything is initialized to 0:/// Thread 1 executes/// {a = 2; b = 3}/// Thread 2 executes/// {c = a; d = b}/// The question is what can `c` and `d` be? It depends on the JVM, but can be any of:/// {c = 2, d = 3}, {c = 0, d = 0}, {c = 2, d = 0}, {c = 0, d = 3}/// The last follows from the fact that the JVM can reorder statements under some/// requirements. The requirements are roughly that the new 'code' should be/// equivalent on a single-threaded machine and there are no happens-before relations/// which link to other threads preventing the reordering. If either variable were/// volatile we might avoid this, but volatile is expensive (and not in Flix).////// For us, this means that {array[size] = 5; size++} can be reordered to/// {oldSize = size++; array[oldSize] = 5}./// Another thread reading may then see the increase of size and try to access that/// position getting null. This has not happened in our experiments, but we wish to/// handle the case. The risk is in binarySearch and we therefore check whether any/// value read is null and if so 'aborts'.////// For Java's memory model see: https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4///pubenumLock[_: Region](JStampedLock)// Unchecked casts are here used to remove the IO effect. It is replaced with an// effect to a region to ensure that the code is not optimized away.////// Wait for `lock` to be unlocked by the current holder of the write-lock on it.////// Why:////// Virtual threads can exhibit weird behaviour when using all physical threads/// and using locking. Without this the threads using optimistic reading will/// never be removed from the physical thread. Meanwhile a thread that holds the/// lock will never get to run and we have a deadlock. Progress can happen, but it/// appears to be extremely unlikely. To fix this call a blocking operation which/// makes the JVM remove the virtual threads from the physical threads allowing/// the lock-holder to unlock.////// For a better explanation see Netflix's encounter of a similar problem:/// https://netflixtechblog.com/java-21-virtual-threads-dude-wheres-my-lock-3052540e231d?gi=128dba0ad426///pubdefyieldBasedOn(lock: Lock[r]): Unit \ r =let Lock(l) = lock;unsafeIOasr { l.unlockRead(l.readLock()) }////// Returns `true` if `lock` is currently held by a thread.////// Only supposed to be used for testing.///pubdefisLocked(lock: Lock[r]): Bool \ r =let Lock(l) = lock;unsafeIOasr { l.isWriteLocked() orl.isReadLocked() }////// Executes `onValid` if `stamp` is still valid on `lock`.////// Otherwise continues with `onInvalid` without yielding.////// Caution: callers often use `onInvalid` to restart a larger operation. In such/// cases, effectful callbacks passed here may be invoked an unexpected number of/// times across retries.///pubdefifValidElseNoYield(stamp: Int64,lock: Lock[r],onValid: Unit -> a \ ef1,onInvalid: Unit -> a \ ef2 ): a \ ef1 + ef2 + r =if (valid(stamp, lock))onValid()elseonInvalid()////// Executes `onValid` if `stamp` is still valid on `lock`.////// Otherwise yields and continues with `onInvalid`.////// Caution: callers often use `onInvalid` to restart a larger operation. In such/// uses, effectful callbacks passed here may be invoked an unexpected number of/// times across retries.///pubdefifValidElse(stamp: Int64,lock: Lock[r],onValid: Unit -> a \ ef1,onInvalid: Unit -> a \ ef2 ): a \ ef1 + ef2 + r =ifValidElseNoYield(stamp,lock,onValid,() -> {yieldBasedOn(lock);onInvalid() } )////// Returns a fresh lock.///pubdefmkLock(_: Region[r]): Lock[r] \ r = Lock(unsafeIOasr { new JStampedLock() })/// Attempt to upgrade `stamp` on `lock` to a write-lock on `lock`./// The returned stamp cannot be invalidated if it was valid when issued.////// This operation is non-blocking.///pubdeftryConvertToWrite(stamp: Int64, lock: Lock[r]): Int64 \ r =let Lock(l) = lock;unsafeIOasr { l.tryConvertToWriteLock(stamp) }////// Attempts to upgrade `stamp` to a write lock and executes `onWrite`/// with the resulting write stamp if successful.////// Otherwise yields and continues with `onInvalid`.////// Caution: callers often use `onInvalid` to restart a larger operation. In such/// uses, effectful callbacks passed here may be invoked an unexpected number of/// times across retries.///pubdefwithWriteLockOrElse(stamp: Int64,lock: Lock[r],onWrite: Int64 -> a \ ef1,onInvalid: Unit -> a \ ef2 ): a \ ef1 + ef2 + r =letwriteStamp = tryConvertToWrite(stamp, lock);ifValidElse(writeStamp, lock, () -> onWrite(writeStamp), onInvalid)////// Return an optimistic stamp on `lock` which can be used in `valid` to/// assert that no thread has taken a write lock since `stamp` was issued.////// This operation is non-blocking.///pubdeftryReadLock(lock: Lock[r]): Int64 \ r =let Lock(l) = lock;unsafeIOasr { l.tryOptimisticRead() }////// Unlock `lock` with the issued write-stamp, `stamp`.////// This operation is non-blocking.///pubdefunlockWrite(stamp: Int64, lock: Lock[r]): Unit \ r =let Lock(l) = lock;unsafeIOasr { l.unlockWrite(stamp) }////// Returns true if `stamp` is still valid on `lock`, meaning no thread/// has acquired a write-lock on `lock` since `stamp` was issued.////// This operation is non-blocking.///pubdefvalid(stamp: Int64, lock: Lock[r]): Bool \ r =let Lock(l) = lock;unsafeIOasr { l.validate(stamp) }////// Lock `lock` and returns the stamp.////// This operation is blocking.///pubdefwriteLock(lock: Lock[r]): Int64 \ r =let Lock(l) = lock;unsafeIOasr { l.writeLock() }}