feat: add onPut/onDelete hooks with correct tombstone hook semantics#345
feat: add onPut/onDelete hooks with correct tombstone hook semantics#345guillaumemichel wants to merge 12 commits into
Conversation
I think it helps simplify things to have only one representation of things, and in that regard this seems like a good idea, so as long as there is no other purpose for a nil value. Was there ever an intended purpose of a nil value, since they are allowed? |
Good point, empty values are actually allowed IIUC so we have to keep the delete hooks. |
I don't know where I stand on this. There might be some cases when it is important to trigger the hook on "overwrites", even with the same value, as a way to signal that the key has been written again.
This sounds like a bug worth fixing at some point. |
hsanjuan
left a comment
There was a problem hiding this comment.
So, while functionality wise seems ok, plainly speaking I think it is ugly to have putHook and onPut, with similar semantics, but different, and mutually exclusive. I don't have a good answer though.
Sorry, I read now that it only affects tombstones, normal puts retain behavior. -- So in principle, it seems to me that the old hooks could adopt the semantic/behavior of the new hooks, with the main difference being the inclusion of the old value, which requires an extra read from the db? I wonder if we can consolidate into a single put/delete hook definition, and make the "old value reading" an option. |
|
Thanks for the review @hsanjuan! I consolidated the new hooks interface to the existing hooks. It breaks the interface but it is cleaner than adding new hooks. Let me know your thoughts. |
|
I updated the PR description to reflect the latest state. Concerning consolidating put and delete hooks into a single hook, it is possible to use the current |
|
@guillaumemichel Thank you for this set of changes. Going to be really useful. As far as I can see, there seems to be an inconsistency between |
|
Good point @sergey-cheperis! Updated so that |
|
@guillaumemichel Looks great, thank you! |
| continue | ||
| } | ||
|
|
||
| if v := e.GetValue(); bytes.Compare(v, st.bestVal) > 0 { |
There was a problem hiding this comment.
I think this is correct. Double check the you want to set st.bestVal when v > st.bestVal
Summary
This PR reworks the CRDT set hook system:
PutHookandDeleteHookare given richer event-struct signatures, a newHookLoadPreviousValueoption exposes the pre-write state, and several correctness issues in how hooks are triggered during tombstone processing are fixed.Breaking change
The
PutHookandDeleteHooksignatures are changed. This is an intentional breaking change: extending the existing hooks is cleaner than introducing a parallel set.Before:
After:
Where
PutEventcarriesKey,NewValue,NewPriority, optionallyOldValue/OldPriority(whenHookLoadPreviousValueis set), and the triggeringDelta.DeleteEventcarriesKey, optionallyLastValue/LastPriority, and the triggeringDelta.Callers upgrading need to update their hook signatures accordingly. See
examples/globaldb/globaldb.gofor an updated example.New
HookLoadPreviousValueoptionA new boolean option,
HookLoadPreviousValue(defaultfalse), controls whether the previous value and priority for the key are read from the store before a hook-relevant write, so they can be forwarded to the hook:false:OldValue/OldPriority(put) andLastValue/LastPriority(delete) are left zero-valued. No extra read is performed.true: one extra datastore read is done per triggered hook to populate those fields, and the suppression rules below kick in.PutHookmay fire during tombstone processing (new)A tombstone does not always mean a delete. When multiple elements share a key and only some are tombstoned, the surviving element with the highest priority becomes the new value. In that case
PutHookfires (notDeleteHook) because the key remains in the set with a (potentially different) value. Callers should be aware that receiving a put hook during a tombstone delta is valid and expected.To tell the two cases apart, compare
PutEvent.NewPrioritywithPutEvent.Delta.GetPriority(): equal means the value was written by this delta; lower means an older element resurfaced after a partial tombstone.Hook suppression rules (only when
HookLoadPreviousValueis true)Two suppression rules prevent spurious hook calls:
PutHookdoes not fire. A same-value / different-priority swap still fires the hook.DeleteHookdoes not fire. Hooks only signal actual state transitions. Note that the delete hook is triggered when a legitimately-stored empty value is removed.These rules are only active when
HookLoadPreviousValueis set, since that's when the pre-state is known.New tests
Added tests covering various
putTombs()scenarios using the hooks inset_test.go.