A hierarchical concurrency control library for Scala that builds upon cats-effect.
Cats Effect Tree Sync provides hierarchical synchronization primitives for Cats Effect applications. While Cats Effect offers excellent general-purpose concurrency primitives like semaphores and mutexes, it lacks built-in support for hierarchical synchronization.
This library introduces specialized synchronization primitives that understand hierarchical relationships between resources, allowing for more efficient concurrency control in systems where resources follow a tree-like structure (such as filesystems, document object models, or hierarchical caches).
Key improvements over standard Cats Effect synchronization:
- Path-based locking: Acquire locks on specific paths in a resource hierarchy
- Fine-grained concurrency: Independent branches can be accessed concurrently
- Hierarchical blocking: Block access to an entire subtree with a single lock operation
- Read/Write separation: Support for both shared (read) and exclusive (write) access patterns
Add the following dependency to your build.sbt
:
libraryDependencies += "io.github.nivox" %% "cats-effect-treesync" % "0.1.0"
Compatible with Scala 2.13 and Scala 3.
The TreeMutex
allows for exclusive access to resources organized in hierarchical paths.
This primitive ensures that:
- Only one fiber can hold a lock on a specific path or any of its ancestors/descendants at a time
- Independent paths can be accessed concurrently
import cats.effect.{IO, Resource}
import cats.effect.treesync.TreeMutex
object TreeMutexExample extends IOApp.Simple {
def run: IO[Unit] = {
// Create a TreeMutex instance
TreeMutex[IO]().flatMap { mutex =>
// Define a function that simulates work on a resource path
def accessResource(path: List[String], description: String): IO[Unit] = {
mutex.lock(path).use { _ =>
for {
_ <- IO.println(s"Accessing $description")
_ <- IO.sleep(1.second)
_ <- IO.println(s"Finished with $description")
} yield ()
}
}
// Access resources concurrently
val program = for {
// These will execute concurrently (independent paths)
fiber1 <- accessResource(List("users", "1", "profile"), "User 1 profile").start
fiber2 <- accessResource(List("users", "2", "profile"), "User 2 profile").start
// This will block until fiber1 completes (shared ancestry)
fiber3 <- accessResource(List("users", "1"), "User 1 data").start
// This will block until both fiber1 and fiber3 complete (locks the entire subtree)
fiber4 <- accessResource(List("users"), "All users").start
_ <- fiber1.join
_ <- fiber2.join
_ <- fiber3.join
_ <- fiber4.join
} yield ()
program
}
}
}
The RWTreeLock
extends the concept of TreeMutex
with read/write semantics, allowing:
- Multiple concurrent readers on the same path
- Exclusive write access to a path
- Fine-grained control over resource hierarchies
import cats.effect.{IO, Resource}
import cats.effect.treesync.RWTreeLock
object RWTreeLockExample extends IOApp.Simple {
def run: IO[Unit] = {
// Create a RWTreeLock instance
RWTreeLock[IO]().flatMap { rwLock =>
// Define functions that simulate reading and writing to resources
def readResource(path: List[String], description: String): IO[Unit] = {
rwLock.readLock(path).use { _ =>
for {
_ <- IO.println(s"Reading from $description")
_ <- IO.sleep(1.second)
_ <- IO.println(s"Finished reading from $description")
} yield ()
}
}
def writeResource(path: List[String], description: String): IO[Unit] = {
rwLock.writeLock(path).use { _ =>
for {
_ <- IO.println(s"Writing to $description")
_ <- IO.sleep(1.second)
_ <- IO.println(s"Finished writing to $description")
} yield ()
}
}
// Access resources with various read/write patterns
val program = for {
// These will execute concurrently (shared read access)
fiber1 <- readResource(List("docs", "manual"), "manual documentation").start
fiber2 <- readResource(List("docs", "manual"), "manual documentation (another reader)").start
// This will block until both fiber1 and fiber2 complete (write blocks reads)
fiber3 <- writeResource(List("docs", "manual"), "manual documentation").start
// This can execute concurrently with fiber1 and fiber2 (independent path)
fiber4 <- writeResource(List("docs", "api"), "API documentation").start
// This will block until fiber3 and fiber4 complete (affects entire docs subtree)
fiber5 <- writeResource(List("docs"), "all documentation").start
_ <- fiber1.join
_ <- fiber2.join
_ <- fiber3.join
_ <- fiber4.join
_ <- fiber5.join
} yield ()
program
}
}
}
This project is licensed under the MIT License - see the LICENSE file for details.