Clarify transaction concurrency #392
Comments
From what I saw of the discussions about this, there is no locking between read and write transactions, as long as the database file doesn't need to grow. Taking a quick glance at the code, this appears to be the case as the write transactions don't pick up any exclusive locks that the read transactions do. There is a Mutex (not a RWMutex!) called rwlock, but that is only taken in the write path. |
There are only a couple places where locks are used:
Most of the time Bolt operates with very little contention because the meta lock is obtained very briefly just to update the list of current transactions and to copy the meta page. Once a read transaction is started it doesn't obtain any additional locks until it closes. However, the reason why you can't safely run a read-only and read-write transaction in the same goroutine is because of mmap remapping. Once the data file fills up, it has to close and reopen the mmap with a new size. This causes the memory addresses of all read transactions to change so we have to wait until all read transactions finish which is why we obtain a write lock on Let me know if that answers your question. |
I'm clearly missing something. To demonstrate what I'm talking about, here's a test case that tries to start a write transaction on one goroutine while a read transaction is alive on another goroutine. It deadlocks on the RWMutex at https://github.com/boltdb/bolt/blob/master/db.go#L210. How should I be doing this? |
Actuallly, your last paragraph
sounds like what I see, but I don't get the part about "in the same goroutine" - it sounds and seems like no write transactions can start while any read transaction is ongoing? |
I guess my test case is a case of a read transaction depending on a write transaction, as stated in the README. However this is the case in less trivial situations that I don't know how to avoid. For example, in my actual use case, two devices exchange index information when they connect over the network. Sending this index information is done by iterating over a bucket inside a The receiving side gets that data, batches it up and processes it and in the end wants to store it with a |
You're correct that it's better to say, "read transaction depending on a write transaction". There have been several issues posted where people had the read and write transactions in the same goroutine so it'd been easier to phrase it in that context. As for your use case, wouldn't the devices be on separate machines and therefore separate Bolt instances so it wouldn't block? Also, can the |
But that's just the thing - the inability of one goroutine to start an So my read transaction finishing depends on a write transaction for that data happening on another machine, and vice versa. The amount of data shuffled can be significant, so just grabbing it all into RAM and then sending isn't feasible. But disregarding my scenario, there seems to be some confusion here as the first two answers above are "there is no locking between read and write transactions" and "There are only a couple places where locks are used" but it seems we've determined there's no scenario where |
The The problem is that we have to specify a size for the mmap when we open it. LMDB requires the caller to specify the size explicitly whereas Bolt chooses to remap incrementally. Bolt could add a fixed size option (similar to LMDB) but it hasn't been an issue in general. |
I'm not sure I understand what it takes to re-mmap the file then. For example // Create a database and create a bucket and a value...
// Start a long running read
var wg sync.WaitGroup
wg.Add(1)
go func() {
db.View(func(tx *bolt.Tx) error {
wg.Done()
select {} // This read takes a long time... We're sending stuff over the network or something.
})
}()
// Make sure the read has started
wg.Wait()
// Perform a nil update
db.Update(func(tx *bolt.Tx) error {
return nil
})
panic("this is never reached") never reaches the panic. I can't seem reproduce a case where even an empty update can succeed without deadlocking when there's a read in progress? But anyway, this is academic at this point. If anything I would wish this would be somewhat clearer up front as it means the concurrency is at best undeterministic. |
The readme says:
However it seems transactions are protected by an RWMutex. That means it's impossible to open a write transaction (from any goroutine) while there is a read transaction still open and vice versa; also opening new read transactions is blocked while a write transaction is waiting to begin. Am I misunderstanding how this works?
The text was updated successfully, but these errors were encountered: