Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set Command with IFEQ Support #1324

Open
wants to merge 9 commits into
base: unstable
Choose a base branch
from

Conversation

sarthakaggarwal97
Copy link

@sarthakaggarwal97 sarthakaggarwal97 commented Nov 19, 2024

This PR allows the Valkey users to perform conditional updates where the SET command is completed if the given comparison-value matches the key’s current value.

Behavior with this PR

SET <key> <value> IFEQ <comparison-value>

If the values match, the SET completes as expected. If they do not match, the command returns a (nil).

Behavior with Additional Flags

  1. SET <key> <value> IFEQ <comparison-value> GET returns the existing value if present, (nil) if not and gives the error if there is a Type Mismatch. Conditional set operation is performed if the given comparison value matches the existing value.
  2. SET <key> <value> IFEQ <comparison-value> XX returns nil if the key doesn't exist, otherwise conditionally updates the key if the given comparison value matches the existing value.
  3. SET <key> <value> IFEQ <comparison-value> NX returns nil whether or not the key exists. Does not conditionally update the key if NX flag is present

Addresses: #1215

Copy link
Contributor

@hpatro hpatro left a comment

Choose a reason for hiding this comment

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

Would be nice to think through the behavior with other flags/parameters of SET command https://valkey.io/commands/set/

NX -- Only set the key if it does not already exist - should we error in this case if CAS is provided?
XX -- Only set the key if it already exists - This becomes redundant to use I believe with CAS.
GET -- Return the old string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value stored at key is not a string. - I think we need to support this parameter in some form. The scenario which comes to my mind is when CAS fails and a user doesn't need to send a GET command again to find out the value stored in the engine. However, we need to think about how to differentiate between success scenario vs failure scenario.

Also, we need to document them.

tests/unit/type/string.tcl Show resolved Hide resolved
src/t_string.c Outdated Show resolved Hide resolved
@hpatro hpatro added the needs-doc-pr This change needs to update a documentation page. Remove label once doc PR is open. label Nov 19, 2024
Copy link

codecov bot commented Nov 19, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 70.76%. Comparing base (4986310) to head (3dc3407).

Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #1324      +/-   ##
============================================
+ Coverage     70.68%   70.76%   +0.07%     
============================================
  Files           115      115              
  Lines         63177    63198      +21     
============================================
+ Hits          44657    44722      +65     
+ Misses        18520    18476      -44     
Files with missing lines Coverage Δ
src/commands.def 100.00% <ø> (ø)
src/t_string.c 96.88% <100.00%> (+0.13%) ⬆️

... and 18 files with indirect coverage changes

---- 🚨 Try these New Features:

Signed-off-by: Sarthak Aggarwal <sarthagg@amazon.com>
Signed-off-by: Sarthak Aggarwal <sarthagg@amazon.com>
@hpatro
Copy link
Contributor

hpatro commented Nov 20, 2024

@sarthakaggarwal97 Please avoid force pushing. force push removes the reviewer's history in Github and one needs to look at the entire change again.

@sarthakaggarwal97
Copy link
Author

noted @hpatro, will avoid it.

Copy link
Contributor

@hpatro hpatro left a comment

Choose a reason for hiding this comment

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

Thanks @sarthakaggarwal97 for the PR! Looks pretty good. Could you also document the behavior in the top comment. Will be easier for others to review and we can finalize it.

assert_equal {OK} [r set foo "new_value" ifeq "initial_value"]
assert_equal "new_value" [r get foo]

assert_equal {} [r set foo "should_not_set" ifeq "wrong_value"]
Copy link
Contributor

Choose a reason for hiding this comment

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

@valkey-io/core-team On failure of compare/set instead of nil value we should return an error with old value in it. Otherwise, a client would need to perform another GET operation.

Copy link
Member

@enjoy-binbin enjoy-binbin Nov 21, 2024

Choose a reason for hiding this comment

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

return an error with old value seems odd to me. i think in normal cases, a client is unlikely get an error in set IFEQ, they hold the old value in somewhere, and if the old value is unvalid, this mean client should abort the set, this usually means the client should abort the entire business logic. In this case, client should GET the new value as needed and usually they don't really need the new value, they juse want the result of whether the SET succeeded or not.

Copy link
Contributor

Choose a reason for hiding this comment

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

@enjoy-binbin I understand if the value is updated from a DB to Valkey as a cache, the client won't benefit much from the value present.

However, if I think Valkey being used as a datastore and value is updated by multiple clients based on the value stored (let say increment old value by 1). for this case, they would need to know the previous value.

Let's hear other devs opinion on this and resolve this.

Copy link
Member

Choose a reason for hiding this comment

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

I like the idea of returning value if the IFEQ fails, not as an error, just as a string. That way you can also check to see if someone else did the same work you did (and silently succeed) or retry you work again.

src/t_string.c Outdated Show resolved Hide resolved
Signed-off-by: Sarthak Aggarwal <sarthagg@amazon.com>
Signed-off-by: Sarthak Aggarwal <sarthagg@amazon.com>
Signed-off-by: Sarthak Aggarwal <sarthagg@amazon.com>
Signed-off-by: Sarthak Aggarwal <sarthagg@amazon.com>
Signed-off-by: Sarthak Aggarwal <sarthagg@amazon.com>
src/t_string.c Outdated Show resolved Hide resolved
src/t_string.c Outdated Show resolved Hide resolved
src/t_string.c Outdated Show resolved Hide resolved
tests/unit/type/string.tcl Outdated Show resolved Hide resolved
Signed-off-by: Sarthak Aggarwal <sarthagg@amazon.com>
@sarthakaggarwal97 sarthakaggarwal97 self-assigned this Nov 20, 2024
Copy link
Contributor

@hpatro hpatro left a comment

Choose a reason for hiding this comment

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

LGTM overall. The behavior currently is:

  1. if value matches, set the new value.
  2. If existing value is of different type, return error WRONGTYPE Operation against a key holding the wrong kind of value
  3. if existing value is a mismatch, return nil. (want us to finalize on this).

@hpatro hpatro requested a review from madolson November 22, 2024 18:40
src/commands/set.json Show resolved Hide resolved

/* Handle the IFEQ conditional check */
if (flags & OBJ_SET_IFEQ && found) {
if (!(flags & OBJ_SET_GET) && checkType(c, existing_value, OBJ_STRING)) {
Copy link
Member

Choose a reason for hiding this comment

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

can this happen? i see in the above, we already do this getGenericCommand thing, this will handle all these error path i guess?

    if (flags & OBJ_SET_GET) {
        if (getGenericCommand(c) == C_ERR) return;
    }

supporting GET and IFEQ look odd to me. GET will get the old value, isn't the value passed in by IFEQ is the same old value?

Copy link
Author

Choose a reason for hiding this comment

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

thanks @enjoy-binbin for the review.

can this happen?

We currently just want to support Check and Set if the existing value is string. The check for OBJ_SET_GET flag prevents us to add additional line of response, when get is supposed to already add response before.

supporting GET and IFEQ look odd to me

I feel it's okay to support GET with IFEQ, in case the user gives an incorrect existing value, and GET can help retrieve it. It would also solve the discussion around if IFEQ should return the value incase the input doesn't matches the existing value (adding GET flag can always do that now).

@madolson @hpatro what do you guys think on this?

src/t_string.c Outdated Show resolved Hide resolved
Signed-off-by: Sarthak Aggarwal <sarthagg@amazon.com>
@zuiderkwast
Copy link
Contributor

zuiderkwast commented Nov 27, 2024

Let's make IFEQ mutually exclusive with NX and XX. It doesn't make sense to combine them so let's make it a syntax error now. (If we don't, we can't fix it in the future without a breaking change.)

SET key value [ NX | XX | IFEQ comparison-value ] [ other args... ]

In the JSON file, putting them in the same "oneOf" block, this is also used for the website and man pages where the syntax is rendered.

Regarding the combining IFEQ with the GET parameter will be difficult to use, but not impossible with the behaviour you described. If SET replied with the 'comparison-value', it means the SET has succeeded. I think it's logical, even if it will probably be very rarely used.

Copy link
Contributor

@zuiderkwast zuiderkwast left a comment

Choose a reason for hiding this comment

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

This looks pretty good. Just don't want to allow IFEQ combined with NX and XX.

@@ -23,6 +23,10 @@
[
"7.0.0",
"Allowed the `NX` and `GET` options to be used together."
],
[
"8.2.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

Our next version is 8.1.0.

Suggested change
"8.2.0",
"8.1.0",

"name": "comparison-value",
"type": "string",
"token": "IFEQ",
"since": "8.2.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"since": "8.2.0",
"since": "8.1.0",

and move this block to within the "oneOf" for NX | XX below. The "since": "2.6.12" can be moved from the oneOf container to the NX and XX args individually.

Copy link
Contributor

Choose a reason for hiding this comment

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

I also like this approach. Doesn't make a lot of sense to combine IFEQ with NX | XX.

Comment on lines 339 to +340
/* SET key value [NX] [XX] [KEEPTTL] [GET] [EX <seconds>] [PX <milliseconds>]
* [EXAT <seconds-timestamp>][PXAT <milliseconds-timestamp>] */
* [EXAT <seconds-timestamp>][PXAT <milliseconds-timestamp>] [IFEQ <comparison-value>] */
Copy link
Contributor

Choose a reason for hiding this comment

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

SET key value [NX | XX | IFEQ comparison-value] [GET]
    [EX seconds | PX milliseconds | EXAT seconds-timestamp |
     PXAT milliseconds-timestamp | KEEPTTL]

Something like that is more in line with the actual syntax. On website and man pages we use lowercase for syntax variables. No < > brackets.

int unit = UNIT_SECONDS;
int flags = OBJ_NO_FLAGS;

if (parseExtendedStringArgumentsOrReply(c, &flags, &unit, &expire, COMMAND_GET) != C_OK) {
if (parseExtendedStringArgumentsOrReply(c, &flags, &unit, &expire, &comparison, COMMAND_GET) != C_OK) {
Copy link
Contributor

Choose a reason for hiding this comment

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

IFEQ is not allowed with COMMAND_GET, so I think we can just pass NULL for the comparison argument. It will not be used. Makes sense?

Suggested change
if (parseExtendedStringArgumentsOrReply(c, &flags, &unit, &expire, &comparison, COMMAND_GET) != C_OK) {
if (parseExtendedStringArgumentsOrReply(c, &flags, &unit, &expire, NULL, COMMAND_GET) != C_OK) {

"SET key value KEEPTTL " "[NX|XX] [GET]"
"SET key value XX " "[GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value " "[IFEQ comparison-value] [NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EX" "[IFEQ comparison-value] [NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
Copy link
Contributor

Choose a reason for hiding this comment

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

All of these...

Suggested change
"SET key value EX" "[IFEQ comparison-value] [NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EX" "[NX|XX|IFEQ comparison-value]] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-doc-pr This change needs to update a documentation page. Remove label once doc PR is open.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants