Re-re-sending this because it appears twice not to have initially made it through:
I have recently taken up stewardship of the Ruby binding for LMDB. It did not take me long to find problems in its design pertaining to concurrent transactions in a multithreaded environment. I would like to fix these problems but I am afraid I still have a few questions after carefully reading the LMDB documentation.
First, I would like to confirm my understanding that there may be only one active read-write transaction per environment, irrespective of processes and/or threads attached, although this transaction may be nested.
This raises some subsidiary questions:
1) The documentation states specifically that *read-write* transactions may be nested, but what about read-only?
2) Must (read-only) transactions always be in a single hierarchy per thread or can there be many “roots” at once?
3) Given that the relevant LMDB structs appear not to discriminate between transaction types, are there consequences for opening, e.g., a read-write transaction subordinate to a read-only one?
The reason why I ask is that the current (inherited) design of the binding keeps a hash table of transactions keyed by thread, and does not distinguish between read-write and read-only, and affords only a single “root” transaction per thread (whether read-write or read-only). I don’t need the memory leaks, double-frees, deadlocks and other bad behaviour to infer that this structure is probably wrong.
Based on what I can glean from the LMDB documentation, is that I probably want to separate the read-write and read-only transactions, make the former a singleton (since there can only be one read-write transaction per environment), artificially flatten the latter (since it probably isn’t meaningful to nest a read-only transaction anyway) and then wrap the transaction code so it does the right thing. What I suppose I’m looking for here is confirmation that my assumptions are correct.
Thanks in advance,
-- Dorian Taylor Make things. Make sense. https://doriantaylor.com
Dorian Taylor (Lists) wrote:
Re-re-sending this because it appears twice not to have initially made it through:
I have recently taken up stewardship of the Ruby binding for LMDB. It did not take me long to find problems in its design pertaining to concurrent transactions in a multithreaded environment. I would like to fix these problems but I am afraid I still have a few questions after carefully reading the LMDB documentation.
First, I would like to confirm my understanding that there may be only one active read-write transaction per environment, irrespective of processes and/or threads attached, although this transaction may be nested.
Correct.
This raises some subsidiary questions:
- The documentation states specifically that *read-write* transactions may be nested, but what about read-only?
There is no point in nesting a read-only txn. Nesting is used to allow work to be subdivided so that one set of writes can be rolled back independently of the rest of the txn. In a readonly txn there is never anything to rollback.
- Must (read-only) transactions always be in a single hierarchy per thread or can there be many “roots” at once?
I don't understand the question. There must be no more than one read transaction per thread.
- Given that the relevant LMDB structs appear not to discriminate between transaction types, are there consequences for opening, e.g., a read-write transaction subordinate to a read-only one?
That will fail.
The reason why I ask is that the current (inherited) design of the binding keeps a hash table of transactions keyed by thread, and does not distinguish between read-write and read-only, and affords only a single “root” transaction per thread (whether read-write or read-only). I don’t need the memory leaks, double-frees, deadlocks and other bad behaviour to infer that this structure is probably wrong.
Based on what I can glean from the LMDB documentation, is that I probably want to separate the read-write and read-only transactions, make the former a singleton (since there can only be one read-write transaction per environment), artificially flatten the latter (since it probably isn’t meaningful to nest a read-only transaction anyway) and then wrap the transaction code so it does the right thing. What I suppose I’m looking for here is confirmation that my assumptions are correct.
Tracking the existence of transactions in a language binding is almost certainly a wrong thing to do, completely unnecessary.
Thanks in advance,
-- Dorian Taylor Make things. Make sense. https://doriantaylor.com
On Apr 9, 2020, at 10:33 AM, Howard Chu hyc@symas.com wrote:
There is no point in nesting a read-only txn. Nesting is used to allow work to be subdivided so that one set of writes can be rolled back independently of the rest of the txn. In a readonly txn there is never anything to rollback.
This is what I figured, albeit from what I can glean, the system doesn’t complain if you do. But then my only experience with LMDB is this one Ruby binding I inherited, and I am trying to reconcile how the previous author designed the transaction code to work with the behaviour I observe.
- Must (read-only) transactions always be in a single hierarchy per thread or can there be many “roots” at once?
I don't understand the question. There must be no more than one read transaction per thread.
The transaction code in this binding module is organized in terms of a hash table keyed by thread ID, which doesn’t discriminate between read-only and read-write threads. When the Ruby code calls the C code, it replaces value in the hash for that thread with the new transaction (via a proxy struct) and then sets the ‘parent’ member to the outer transaction. This sounds like it is probably wrong for a number of reasons.
This is good information.
- Given that the relevant LMDB structs appear not to discriminate between transaction types, are there consequences for opening, e.g., a read-write transaction subordinate to a read-only one?
That will fail.
It reads then like there can only be one “root” transaction per thread, and if that transaction is read-write, then it is the only one for the environment. Conversely, if the transaction is read-only, it makes no sense to nest, so any nesting behaviour invoked from Ruby should be a no-op. I suppose it would further make no sense to nest a read-only transaction under a read-write, even though it appears that nothing prevents this.
Furthermore an attempt to open a read-write transaction under a read-only one should raise an exception in Ruby.
Tracking the existence of transactions in a language binding is almost certainly a wrong thing to do, completely unnecessary.
This is my feeling too.
This brings me to another issue: to behave properly, the binding will definitely need to know whether whatever comes out of mdb_active_txn() is read-only or not. Problem is, txn->mt_flags is not exposed. I filed a patch on March 20 for a function that returns them: https://bugs.openldap.org/show_bug.cgi?id=9188 .
Ironically it seems somebody else did the same in April 2019, but I didn’t catch it: https://bugs.openldap.org/show_bug.cgi?id=9011
Thanks for your response,
-- Dorian Taylor Make things. Make sense. https://doriantaylor.com tel:+1-604-723-5755 callto:doriantaylor
Dorian Taylor (Lists) wrote:
On Apr 9, 2020, at 10:33 AM, Howard Chu hyc@symas.com wrote:
There is no point in nesting a read-only txn. Nesting is used to allow work to be subdivided so that one set of writes can be rolled back independently of the rest of the txn. In a readonly txn there is never anything to rollback.
This is what I figured, albeit from what I can glean, the system doesn’t complain if you do. But then my only experience with LMDB is this one Ruby binding I inherited, and I am trying to reconcile how the previous author designed the transaction code to work with the behaviour I observe.
A language binding should be a transparent pipe. It should relay whatever parameters the caller supplied down to the underlying library, and return the results. It shouldn't try to guess at or impose any behaviors.
In this particular case, transaction nesting is already documented to only be supported for write txns, and an error will be returned for any attempt to use nesting with a read txn. The binding should allow the caller to try anything, and return the underlying error code when it fails.
On Apr 9, 2020, at 9:29 PM, Howard Chu hyc@symas.com wrote:
A language binding should be a transparent pipe. It should relay whatever parameters the caller supplied down to the underlying library, and return the results. It shouldn't try to guess at or impose any behaviors.
So, this is the current interface (that, again, I inherited):
https://www.rubydoc.info/gems/lmdb/LMDB/Environment#transaction-instance_met...
I won’t bore you with the details, in part because I already have, but there is a considerable amount of (once again, inherited) bookkeeping under the hood to make that syntax work. The problem is it only works under extremely favourable conditions and it is my intention to rip much of that bookkeeping out. The purpose of this thread is to try to narrow down somewhat on what to replace it with.
Again, if I could have gleaned this information successfully from the documentation, I never would have bothered you. If you’re amenable to a patch to said documentation, I am more than willing to provide one that would have answered my questions before asking them. The information you have given me so far in this thread is valuable and could readily be integrated into the documentation.
In this particular case, transaction nesting is already documented to only be supported for write txns, and an error will be returned for any attempt to use nesting with a read txn. The binding should allow the caller to try anything, and return the underlying error code when it fails.
Here’s a question then: is the error returned from a) attempting to nest a read-write transaction under a read-only transaction, or b) attempting to nest a read-only transaction at all, distinct enough from other errors to act properly on?
(Where “act properly” means “trap the error and either raise an exception in the higher-level language or ignore it and do a no-op, or whatever”.)
Thanks,
-- Dorian Taylor Make things. Make sense. https://doriantaylor.com
On 2020-04-10, at 06:29:14, Howard Chu hyc@symas.com wrote:
...
A language binding should be a transparent pipe. It should relay whatever parameters the caller supplied down to the underlying library, and return the results. It shouldn't try to guess at or impose any behaviors.
does this prescription apply even to a facility which would integrate a language’s control-flow primitives to limit transactions to dynamic extent? --- james anderson | james@dydra.com | http://dydra.com
openldap-technical@openldap.org