This is a tracking issue to collect information on whether it is worth trying to improve the testing/synctest package's support for mutexes.
A core concept in synctest is that of a goroutine "durably blocking". A goroutine is durably blocked when it is blocked and can only be unblocked by another goroutine in the same bubble. A goroutine blocked on time.Sleep is durably blocked, because it will only wake when the bubble's clock advances. A goroutine blocked reading from a network socket is not durably blocked, because it will wake when the kernel unblocks it.
We mark channels created within a bubble as associated with that bubble. Goroutines are not permitted to operate on channels created in a different bubble (the operation panics), and therefore a goroutine blocked on a bubbled channel is durably blocked. A goroutine blocked on an unbubbled channel is not durably blocked. These rules let us use synctest to test hermetic (within-bubble) channel operations without prohibiting operations which access global channels.
Blocking on a mutex (sync.Mutex or sync.RWMutex) is not durably blocking. We made this choice out of practicality: Global mutexes are common, so goroutines in synctest bubbles will often block on a mutex held by a goroutine from outside the bubble. We must treat this state as not durably blocking. Simply treating all mutex blocking as non-durable causes few problems in practice, since most mutexes are held for a short period of time.
There are cases, however, where treating all mutex blocking as non-durable causes problems in testing. For example, the database/sql package currently uses a long-held sync.RWMutex to block close operations while a row scan is in progress. This results in cases which can't be tested by synctest: The goroutine calling close is blocked non-durably on sync.RWMutex.Lock, but no other goroutine is running.
We can work around this limitation of synctest by replacing the mutex with channel-based operations.
Perhaps, however, we could make synctest play better with mutexes.
Mutexes do not have a constructor and therefore cannot be associated with a bubble in the way that channels are. Instead, we could apply the following rules to mutexes:
- A mutex which is locked by a bubbled goroutine becomes associated with that bubble.
- A bubbled mutex must be unlocked by a goroutine from the same bubble. Unlocking it from a different bubble panics.
- Unlocking a bubbled mutex disassociates it from any bubble. Unlocked mutexes are never bubbled.
- Blocking on a mutex from the same bubble is durable.
- Blocking on a mutex from any other bubble is not durable.
In other words, mutex operations that stay within a bubble are durable, operations that escape a bubble are not, and you can't lock a mutex in one bubble and unlock it elsewhere.
All of this was proposed and discussed during the initial synctest proposal. We chose not to do it at the time because the risks of changing the mutex implementation--one of the more performance-critical parts of the standard library--seemed too high for the likely benefit.
I'm opening this issue to track places where the current lack of durable blocking on mutex operations is a problem, so we can better understand whether it might be worth revisiting this.
This is a tracking issue to collect information on whether it is worth trying to improve the testing/synctest package's support for mutexes.
A core concept in synctest is that of a goroutine "durably blocking". A goroutine is durably blocked when it is blocked and can only be unblocked by another goroutine in the same bubble. A goroutine blocked on
time.Sleepis durably blocked, because it will only wake when the bubble's clock advances. A goroutine blocked reading from a network socket is not durably blocked, because it will wake when the kernel unblocks it.We mark channels created within a bubble as associated with that bubble. Goroutines are not permitted to operate on channels created in a different bubble (the operation panics), and therefore a goroutine blocked on a bubbled channel is durably blocked. A goroutine blocked on an unbubbled channel is not durably blocked. These rules let us use synctest to test hermetic (within-bubble) channel operations without prohibiting operations which access global channels.
Blocking on a mutex (
sync.Mutexorsync.RWMutex) is not durably blocking. We made this choice out of practicality: Global mutexes are common, so goroutines in synctest bubbles will often block on a mutex held by a goroutine from outside the bubble. We must treat this state as not durably blocking. Simply treating all mutex blocking as non-durable causes few problems in practice, since most mutexes are held for a short period of time.There are cases, however, where treating all mutex blocking as non-durable causes problems in testing. For example, the
database/sqlpackage currently uses a long-heldsync.RWMutexto block close operations while a row scan is in progress. This results in cases which can't be tested by synctest: The goroutine calling close is blocked non-durably onsync.RWMutex.Lock, but no other goroutine is running.We can work around this limitation of synctest by replacing the mutex with channel-based operations.
Perhaps, however, we could make synctest play better with mutexes.
Mutexes do not have a constructor and therefore cannot be associated with a bubble in the way that channels are. Instead, we could apply the following rules to mutexes:
In other words, mutex operations that stay within a bubble are durable, operations that escape a bubble are not, and you can't lock a mutex in one bubble and unlock it elsewhere.
All of this was proposed and discussed during the initial synctest proposal. We chose not to do it at the time because the risks of changing the mutex implementation--one of the more performance-critical parts of the standard library--seemed too high for the likely benefit.
I'm opening this issue to track places where the current lack of durable blocking on mutex operations is a problem, so we can better understand whether it might be worth revisiting this.