• Meir Shpilraien (Spielrein)'s avatar
    Fix replication inconsistency on modules that uses key space notifications (#10969) · 508a1388
    Meir Shpilraien (Spielrein) authored
    Fix replication inconsistency on modules that uses key space notifications.
    
    ### The Problem
    
    In general, key space notifications are invoked after the command logic was
    executed (this is not always the case, we will discuss later about specific
    command that do not follow this rules). For example, the `set x 1` will trigger
    a `set` notification that will be invoked after the `set` logic was performed, so
    if the notification logic will try to fetch `x`, it will see the new data that was written.
    Consider the scenario on which the notification logic performs some write
    commands. for example, the notification logic increase some counter,
    `incr x{counter}`, indicating how many times `x` was changed.
    The logical order by which the logic was executed is has follow:
    
    ```
    set x 1
    incr x{counter}
    ```
    
    The issue is that the `set x 1` command is added to the replication buffer
    at the end of the command invocation (specifically after the key space
    notification logic was invoked and performed the `incr` command).
    The replication/aof sees the commands in the wrong order:
    
    ```
    incr x{counter}
    set x 1
    ```
    
    In this specific example the order is less important.
    But if, for example, the notification would have deleted `x` then we would
    end up with primary-replica inconsistency.
    
    ### The Solution
    
    Put the command that cause the notification in its rightful place. In the
    above example, the `set x 1` command logic was executed before the
    notification logic, so it should be added to the replication buffer before
    the commands that is invoked by the notification logic. To achieve this,
    without a major code refactoring, we save a placeholder in the replication
    buffer, when finishing invoking the command logic we check if the command
    need to be replicated, and if it does, we use the placeholder to add it to the
    replication buffer instead of appending it to the end.
    
    To be efficient and not allocating memory on each command to save the
    placeholder, the replication buffer array was modified to reuse memory
    (instead of allocating it each time we want to replicate commands).
    Also, to avoid saving a placeholder when not needed, we do it only for
    WRITE or MAY_REPLICATE commands.
    
    #### Additional Fixes
    
    * Expire and Eviction notifications:
      * Expire/Eviction logical order was to first perform the Expire/Eviction
        and then the notification logic. The replication buffer got this in the
        other way around (first notification effect and then the `del` command).
        The PR fixes this issue.
      * The notification effect and the `del` command was not wrap with
        `multi-exec` (if needed). The PR also fix this issue.
    * SPOP command:
      * On spop, the `spop` notification was fired before the command logic
        was executed. The change in this PR would have cause the replication
        order to be change (first `spop` command and then notification `logic`)
        although the logical order is first the notification logic and then the
        `spop` logic. The right fix would have been to move the notification to
        be fired after the command was executed (like all the other commands),
        but this can be considered a breaking change. To overcome this, the PR
        keeps the current behavior and changes the `spop` code to keep the right
        logical order when pushing commands to the replication buffer. Another PR
        will follow to fix the SPOP properly and match it to the other command (we
        split it to 2 separate PR's so it will be easy to cherry-pick this PR to 7.0 if
        we chose to).
    
    #### Unhanded Known Limitations
    
    * key miss event:
      * On key miss event, if a module performed some write command on the
        event (using `RM_Call`), the `dirty` counter would increase and the read
        command that cause the key miss event would be replicated to the replication
        and aof. This problem can also happened on a write command that open
        some keys but eventually decides not to perform any action. We decided
        not to handle this problem on this PR because the solution is complex
        and will cause additional risks in case we will want to cherry-pick this PR.
        We should decide if we want to handle it in future PR's. For now, modules
        writers is advice not to perform any write commands on key miss event.
    
    #### Testing
    
    * We already have tests to cover cases where a notification is invoking write
      commands that are also added to the replication buffer, the tests was modified
      to verify that the replica gets the command in the correct logical order.
    * Test was added to verify that `spop` behavior was kept unchanged.
    * Test was added to verify key miss event behave as expected.
    * Test was added to verify the changes do not break lazy expiration.
    
    #### Additional Changes
    
    * `propagateNow` function can accept a special dbid, -1, indicating not
      to replicate `select`. We use this to replicate `multi/exec` on `propagatePendingCommands`
      function. The side effect of this change is that now the `select` command
      will appear inside the `multi/exec` block on the replication stream (instead of
      outside of the `multi/exec` block). Tests was modified to match this new behavior.
    508a1388
t_set.c 42.9 KB