Skip to content

Delete key doesn't dirty client who watched stale key#10256

Merged
oranagra merged 8 commits intoredis:unstablefrom
zuiderkwast:watch-stale-keys
Feb 22, 2022
Merged

Delete key doesn't dirty client who watched stale key#10256
oranagra merged 8 commits intoredis:unstablefrom
zuiderkwast:watch-stale-keys

Conversation

@zuiderkwast
Copy link
Contributor

@zuiderkwast zuiderkwast commented Feb 7, 2022

When WATCH is called on a key that's already logically expired, avoid discarding the transaction when the keys is actually deleted.

This is a revamp of #9234.

When WATCH is called, a flag is stored if the key is already expired
at the time of watch. The expired key is not deleted, only checked.

When a key is "touched", if it is deleted and it was already expired
when a client watched it, the client is not marked as dirty.

This is a revamp of redis#9234.

When WATCH is called, a flag is stored if the key is already expired
at the time of watch. The expired key is not deleted, only checked.

When a key is "touched", if it is deleted and it was already expired
when a client watched it, the client is not marked as dirty.

The LRU field of the key name's robj (the argument of WATCH) is used
for the flag.
@oranagra
Copy link
Member

@zuiderkwast thank you for revamping that stale PR.
i actually no longer remember the status there and why it was stale.
maybe you can quickly remind me, and also list the differences between this one and the old one?

@zuiderkwast
Copy link
Contributor Author

maybe you can quickly remind me, and also list the differences between this one and the old one?

@oranagra The problem: WATCH an already expired key, then active expire or DEL marks the key as dirty, causing EXEC to fail, although there was no actual change in the logical existence of the key since WATCH.

The old PR attempted to delete expired keys on WATCH (expireIfNeeded), making WATCH a command with side effects that need replication.

This PR is based on your idea of storing a bit somewhere, flagging that the keys was already expired when WATCH was called. Then, if the key is deleted or active-expired after WATCH, this doesn't mark the key as dirty if the flag is set.

Problem solved without a need for replicating side-effects of WATCH. We don't care if the key is actually removed from the db dict or not.

@soloestoy
Copy link
Contributor

Oops... I have already implemented it in my laptop but forgot to push it, it's similar with your codes. I just pushed it and you can compare with the original PR.

BTW, the implements both this PR and mine are incomplete, the touchAllWatchedKeysInDb() also needs check if watched keys are expired/stale, since FLUSHALL/FLUSHDB/SWAPDB would call it, and SWAPDB is irritating.

@zuiderkwast
Copy link
Contributor Author

zuiderkwast commented Feb 16, 2022

Oops... I have already implemented it in my laptop but forgot to push it, it's similar with your codes. I just pushed it and you can compare with the original PR.

@soloestoy OK, it seems to have diverged from unstable. Do you want to continue your work or shall we focus on this PR? We can avoid doing parallel work...

BTW, the implements both this PR and mine are incomplete, the touchAllWatchedKeysInDb() also needs check if watched keys are expired/stale, since FLUSHALL/FLUSHDB/SWAPDB would call it, and SWAPDB is irritating.

Good point!

Looking at touchAllWatchedKeysInDb(), I think it may have a bug since it doesn't touch deleted keys. Correct me if I'm wrong.
I'm not sure what's the expected behaviour for the stale keys in touchAllWatchedKeysInDb.

…/pointer

Use db pointer in watchedKey again (reverts the change to db index, because
when swapping db with tempDb the db is not in the server.db array so index
is not enough).

Add test cases for FLUSDB and SWAPDB with watched stale key.
Copy link
Member

@oranagra oranagra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks.
i think the explicit code is easier to reason with.

@oranagra
Copy link
Member

@soloestoy please have a look before i merge this.

@oranagra oranagra added the release-notes indication that this issue needs to be mentioned in the release notes label Feb 21, 2022
@zuiderkwast
Copy link
Contributor Author

zuiderkwast commented Feb 21, 2022

@oranagra Please don't merge yet. I have found some errors for swapdb in a not-yet-committed test case "SWAPDB does not touch non-existing key replaced with stale key". It turns out the database dicts are swapped before touchAllKeysInDb are called. In the flushdb command, the dicts are emptied after touchAllKeysInDb is called.

@zuiderkwast
Copy link
Contributor Author

More test cases for SWAPDB found more errors that are now fixed:

  1. Expired key replaced with non-existing key (clear expired flag)
  2. Non-existing key replaced with expired key (set expired flag)
  3. Expired key replaced with expired key (key remains expired)

@oranagra It's unfortunate that touchAllWatchedKeysInDb is called after swapping the dicts when it's called before flushing dics in FLUSHDB. I'll try to change this so touchAllWatchedKeysInDb is called before swapping the dics and see if it breaks other things.

Copy link
Member

@oranagra oranagra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@oranagra oranagra merged commit e9ae037 into redis:unstable Feb 22, 2022
@enjoy-binbin
Copy link
Contributor

enjoy-binbin commented Feb 23, 2022

this somehow break the test (timing issue in valgrind and FreeBsd):

https://github.com/enjoy-binbin/redis/runs/5289611534?check_suite_focus=true#step:6:4999
*** [err]: EXEC fail on lazy expired WATCHed key in tests/unit/multi.tcl
Expected '1' to be equal to '' (context: type eval line 13 cmd {assert_equal [r exec] {}} proc ::test)

When WATCH is called on a key that's already logically expired, avoid discarding the transaction when the keys is actually deleted.

    test {EXEC fail on lazy expired WATCHed key} {
        r flushall
        r debug set-active-expire 0

        r del key
        r set key 1 px 2
        r watch key  ------> key already logically expired

        after 100

        r multi
        r incr key
        assert_equal [r exec] {}
        r debug set-active-expire 1
    } {OK} {needs:debug}

new edit:
ok, i have an alternative method (#10332)

enjoy-binbin added a commit to enjoy-binbin/redis that referenced this pull request Feb 23, 2022
The test will fail on slow machines (valgrind or FreeBsd).
Because in redis#10256 when WATCH is called on a key that's already
logically expired, we will add an `expired` flag, and we will
skip it in `isWatchedKeyExpired` check.

Apparently we need to increase the expiration time so that
the key can not expire logically then the WATCH is called.
Also added retries to make sure it doesn't fail. I suppose
100ms is enough in valgrind, tested locally, no need to retry.
oranagra pushed a commit that referenced this pull request Feb 23, 2022
The test will fail on slow machines (valgrind or FreeBsd).
Because in #10256 when WATCH is called on a key that's already
logically expired, we will add an `expired` flag, and we will
skip it in `isWatchedKeyExpired` check.

Apparently we need to increase the expiration time so that
the key can not expire logically then the WATCH is called.
Also added retries to make sure it doesn't fail. I suppose
100ms is enough in valgrind, tested locally, no need to retry.
@zuiderkwast zuiderkwast deleted the watch-stale-keys branch February 23, 2022 08:57
@oranagra oranagra mentioned this pull request Feb 28, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-notes indication that this issue needs to be mentioned in the release notes

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

4 participants