REPEATABLE_READ extends to the scope of the TX. Inside TX1, once you have read /a/b, you are guaranteed that the value remains the same until you commit or roll back TX1. (We use R/W locks do implement this). So when TX1 commits in line 5, TX2 will be able to acquire the WL (upgrade from a RL) in line 6, on which it was blocked before TX1 committed because TX1 had the WL (and WLs are exclusive).
So the assert() on line 8 *must* show the value that TX2 modified if TX2's put() came before the assert().
Thanks for the reply Bela.
My confusion is related to the case where the node doesn't exist. I have since concluded that since there is no node, there is nothing to lock, and so the best approach is to always make sure the node exists before trying to examine it.
In my testing, if I were to change line 6 to be a get() instead of a put(), tx2 would see the value committed by tx1 (for tx2, the gets on line 3 and 6 produce different results) - doesn't that violate the REPEATABLE_READ semantics? Would it be safe to say that the locking semantics within a transaction only apply if the node exist at the start of the transaction?
No, if you change line 6 to a get(), then we do see the change made by TX, but that's similar semantically to phantom reads, which are not prevented by REPEATABLE_READ. If you want to prevent phantom reads, use SERIALIZABLE