E08252: nested enter() on a different lock identity — potential deadlock from lock-order cycle
What went wrong
An enter() call on one MutexLock was reached while another MutexLock was still held, and the two locks are different identities — AND the program elsewhere acquires the same pair of locks in the opposite order. The two contradicting acquisition orders form a cycle in the workspace-wide lock-precedence graph: at runtime two threads taking these paths concurrently can deadlock — each holds one lock and waits forever for the other.
Rationale
EK9's concurrency contract guarantees no execution can deadlock on a lock-acquisition cycle. The compiler enforces this by building a program-wide graph of "outer-held → inner-acquired" precedence edges and running Tarjan's SCC algorithm: any strongly-connected component of size ≥ 2 IS a lock-order cycle, and every edge in it gets flagged.
Single-direction nesting of two PROVABLY-ORDERABLE locks (different types, or different field declarations) is safe and not flagged: E08252 only fires when at least two distinct call sites take the same pair in opposite orders — the canonical textbook deadlock. This matches the lockdep (Linux kernel) model brought forward to compile time thanks to EK9's closed-world type system.
NOTE: this "single direction is safe" rule applies only when a global order can be PROVEN. Two locks of the SAME type on caller- or runtime-determined objects (different same-type parameters, or the same field on two different objects) are NOT orderable, so even a single static direction is rejected — that is E08255 (UNPROVABLE_LOCK_ORDER), not E08252.
How to fix it
- If both resources genuinely need atomic protection together, consolidate them under a SINGLE MutexLock of a record type. The MutexKey body mutates every field atomically; no second lock exists, so no cycle can form. This is the canonical fix. The [EXAMPLE] section below shows two functions taking the same two locks in opposite orders, then one MutexLock of an 'AccountPair' record whose MutexKey moves an amount between both fields.
- If the locks protect orthogonal concerns and don't need joint atomicity, acquire them sequentially — release the first before taking the second. No overlap means no cycle.
- If the same method needs different locks, consider whether the class structure is wrong: each method should own exactly one concern. Split into multiple methods if necessary.
Common causes
- Money-transfer style operations holding two account locks in different orders across methods.
- Iterating one locked collection while updating another, with the outer/inner roles reversed in two different functions.
- Cache + database pattern where one method locks cache-then-db, the other locks db-then-cache.
- The opposing pair reached through getters that resolve to distinct lock fields (e.g. box.getA() / box.getB(), or a delegating getter inner.getA()). The getter is resolved to the underlying field, so the cycle is detected exactly as if the fields were named directly.
Worked examples
Compilable examples demonstrating this error and the correct fixes:
- ek9 -q 1298 (I got E08252 NESTED_ENTER_DIFFERENT_LOCK — what does 'loc...)
- ek9 -q 1300 (How do I use multiple MutexLocks correctly — coarse vs fine-grained?)
- ek9 -q 158 (How do I protect shared data from concurrent access in EK9?)
See also
- E08255: unprovable lock order — the same-TYPE sibling of this error. E08252 is the PROVABLE cycle (orderable locks, opposite-order sites); E08255 is same-type locks on caller/runtime objects whose order cannot be proven, rejected even single-direction.
- E08253: same lock acquired across a thread boundary (related — same identity but cross-thread, which doesn't reenter).
- E08251: shared state mutation outside lock.
Examples
Every example is compiled by the EK9 compiler: the first fails exactly as captioned, the others fix it.
Fails with E08252: nested enter() on a different lock identity — potential deadlock from lock-order cycle
#!ek9 defines module error.e08252.bad defines function transferAtoB() -> lockA as MutexLock of Integer lockB as MutexLock of String innerKeyB <- () is MutexKey of String as function stdout <- Stdout() stdout.println(lockedItem) outerKeyA <- (lockB, innerKeyB) is MutexKey of Integer as function require lockB.enter(innerKeyB) require lockA.enter(outerKeyA) transferBtoA() -> lockA as MutexLock of Integer lockB as MutexLock of String innerKeyA <- () is MutexKey of Integer as function stdout <- Stdout() stdout.println(lockedItem) outerKeyB <- (lockA, innerKeyA) is MutexKey of String as function require lockA.enter(innerKeyA) require lockB.enter(outerKeyB)
Fixed: Protect the state that changes together under ONE lock
#!ek9 defines module error.e08252.good defines record AccountPair source as Integer: 0 target as Integer: 0 default operator ? defines class Ledger accountLock as MutexLock of AccountPair: MutexLock(AccountPair()) transfer() -> amount as Integer key <- (amount) is MutexKey of AccountPair as function lockedItem.source: lockedItem.source - amount lockedItem.target: lockedItem.target + amount require accountLock.enter(key) default operator ?
Questions and answers
These Q&A entries show E08252 and its fix: