diff --git a/course-definition.yml b/course-definition.yml index 6154cb9a..9e0ed78d 100644 --- a/course-definition.yml +++ b/course-definition.yml @@ -128,6 +128,16 @@ extensions: [subscribe-command]: https://redis.io/docs/latest/commands/subscribe/ [publish-command]: https://redis.io/docs/latest/commands/publish/ + - slug: "optimistic-locking" + name: "Optimistic Locking" + description_markdown: | + In this challenge extension you'll add support for [Optimistic Locking][redis-optimistic-locking] to your Redis implementation. + + Along the way you'll learn about the [WATCH][watch-command] command and how it works in synergy with the `MULTI`, `EXEC`, and `DISCARD` commands to achieve optimistic locking. + + [redis-optimistic-locking]: https://redis.io/docs/latest/develop/using-commands/transactions/#cas + [watch-command]: https://redis.io/docs/latest/commands/watch/ + stages: - slug: "jm1" concept_slugs: @@ -676,4 +686,59 @@ stages: primary_extension_slug: "pub-sub" name: "Unsubscribe" difficulty: medium - marketing_md: In this stage, you'll add support for the `UNSUBSCRIBE command`, which is used to unsubscribe from a channel. \ No newline at end of file + marketing_md: In this stage, you'll add support for the `UNSUBSCRIBE command`, which is used to unsubscribe from a channel. + + # Optimistic Locking + - slug: "xj2" + primary_extension_sug: "optimistic-locking" + name: "Watch a key" + difficuly: easy + marketing_md: In this stage, you'll add support for the `WATCH` command. + + - slug: "zs3" + primary_extension_sug: "optimistic-locking" + name: "Disallow WATCH in a transaction" + difficuly: easy + marketing_md: In this stage, you'll add support for disallowing the `WATCH` command in a transaction. + + - slug: "qc8" + primary_extension_slug: "optimistic-locking" + name: "Fail EXEC after WATCH" + difficulty: hard + marketing_md: In this stage, you'll add support for failing transactions after a `WATCH` command is issued. + + - slug: "bu1" + primary_extension_slug: "optimistic-locking" + name: "Optimistic Locking" + difficulty: medium + marketing_md: In this stage, you'll add support for optimistic locking using the `WATCH` command. + + - slug: "al4" + primary_extension_slug: "optimistic-locking" + name: "Watch uninitialized keys" + difficulty: easy + marketing_md: In this stage, you'll add support for watching a non-existent key. + + - slug: "ew6" + primary_extension_slug: "optimistic-locking" + name: "Watch multiple keys" + difficulty: easy + marketing_md: In this stage, you'll add support for watching multiple keys. + + - slug: "mn2" + primary_extension_slug: "optimistic-locking" + name: "UNWATCH a key" + difficulty: easy + marketing_md: In this stage, you'll add support for the `UNWATCH` command. + + - slug: "au8" + primary_extension_slug: "optimistic-locking" + name: "Unwatch on EXEC" + difficulty: easy + marketing_md: In this stage, you'll add support clearing watched keys on `EXEC`. + + - slug: "vh1" + primary_extension_slug: "optimistic-locking" + name: "Unwatch on DISCARD" + difficulty: easy + marketing_md: In this stage, you'll add support clearing watched keys on `DISCARD`. \ No newline at end of file diff --git a/stage_descriptions/optimistic-locking-01-xj2.md b/stage_descriptions/optimistic-locking-01-xj2.md new file mode 100644 index 00000000..328af391 --- /dev/null +++ b/stage_descriptions/optimistic-locking-01-xj2.md @@ -0,0 +1,32 @@ +In this stage, you'll add support for the `WATCH` command. + +### The `WATCH` command + +[The `WATCH` command](https://redis.io/docs/latest/commands/watch/) marks a key to be monitored for changes. +If the watched key is modified by another client before the transaction is executed, the transaction will be aborted. This enables simple optimistic locking behavior in Redis. + +Example Usage: +``` +$ redis-cli WATCH key +OK +``` + +The response is always the simple string `"+OK\r\n"`. + + +### Tests +The tester will execute your program like this: + +```bash +$ ./your_program.sh +``` + +The tester will then send a `WATCH` command with a key. + +```bash +$ redis-cli WATCH key (expecting "+OK\r\n" as the response) +``` + +### Notes +- In this stage, you'll only need to handle replying to the `WATCH` command outside of a transaction. +- We will get to disallowing the `WATCH` command inside a transaction in the next stage. \ No newline at end of file diff --git a/stage_descriptions/optimistic-locking-02-zs3.md b/stage_descriptions/optimistic-locking-02-zs3.md new file mode 100644 index 00000000..c618a7ef --- /dev/null +++ b/stage_descriptions/optimistic-locking-02-zs3.md @@ -0,0 +1,44 @@ +In this stage, you'll add support for disallowing the `WATCH` command in a transaction. + +### `WATCH` in transaction + +[The `WATCH` command](https://redis.io/docs/latest/commands/watch/) is not allowed inside a transaction. If a `WATCH` command is issued from a client when it has begun a transaction using the `MULTI` command, the response to the `WATCH` is an error. + +Example Usage: +``` +$ redis-cli +> MULTI +OK +(TX)> WATCH foo +(error) ERR WATCH inside MULTI is not allowed +``` + +### Tests +The tester will execute your program like this: + +```bash +$ ./your_program.sh +``` + +The tester will then begin a transaction using the `MULTI` command, and send a `WATCH` command with a random key. + +```bash +$ redis-cli +> MULTI (expecting "+OK" as the response) + +> WATCH key +# Expect: (error) ERR WATCH inside MULTI is not allowed +``` +The response is a RESP simple string, which is encoded as: +``` +-ERR WATCH inside MULTI is not allowed\r\n +``` + +The tester will then abort the transaction using the `DISCARD` command, and again send a `WATCH` command. It will expect the response to be `"+OK\r\n` in this case. + +```bash +> DISCARD (expecting "+OK\r\n" as the response) + +> WATCH key2 (expecting "+OK\r\n" as the response) +``` + diff --git a/stage_descriptions/optimistic-locking-03-qc8.md b/stage_descriptions/optimistic-locking-03-qc8.md new file mode 100644 index 00000000..9a95e93f --- /dev/null +++ b/stage_descriptions/optimistic-locking-03-qc8.md @@ -0,0 +1,100 @@ +In this stage, you'll add support for failing transactions after a `WATCH` command is issued. + +### Optimistic locking using `WATCH` +The `WATCH` command enables optimistic locking by monitoring the watched key for changes. +If the watched key is modified after `WATCH` is called and before `EXEC` is run, the transaction is aborted. + +This facilitates the optimistic locking in Redis. Example Usage: + +```bash +# Client A +> SET foo 100 +OK + +> WATCH foo +OK + +# Client B +> SET foo 200 +OK + +# Client A +> MULTI +OK + +> SET foo bar +QUEUED + +> EXEC +(nil) # Transaction aborts due to watched key being modified +``` + +### Tests + +The tester will execute your program like this: + +``` +$ ./your_program.sh +``` + +The tester will spawn two clients. + +Using the first client, it will set the value of two keys and, issue a `WATCH` command specifying one of the keys + +```bash +# Client 1 +> SET foo 100 (Expecting "+OK\r\n") +> SET bar 200 (Expecting "+OK\r\n") +> WATCH foo +``` + +Using the second client, the tester will update the value of the watched variable. +```bash +# Client 2 +> SET foo 200 (Expecing "+OK\r\n") +``` + +Using the first client, the tester will attempt to execute a transaction. +```bash +# Client 1 +> MULTI (Expecting "+OK\r\n") +> SET bar 300 (Expecting "+QUEUED\r\n") +> EXEC (Expecting "*-1\r\n") +``` + +The response to `EXEC` should be a RESP null array. + +Using the first client, the tester will retrieve the value of unwatched key to check if the transaction was aborted. + +```bash +# Client 1 +> GET bar (Expecting bulk string "200") +``` + +Using the second client, the tester will attempt to execute a transaction. +```bash +# Client 2 +> MULTI (Expecting "+OK\r\n") +> SET bar 300 (Expecting "+QUEUED\r\n") +> PING (Expecting "+QUEUED\r\n") +> EXEC (Expecting an array of responses for the queued commands) +``` +The response to the `EXEC` command should be its usual response because `WATCH` command was not issued from this client previously. + + +Using the first client, the tester will retrieve the value of unwatched key to check if the transaction succeeded. + +```bash +# Client 1 +> GET bar (Expecting bulk string "300") +``` + + +### Notes + +- In this stage, you will implement aborting a transaction if a `WATCH` command was issued by the client previously. + - Assume that the watched variable is guaranteed to be modified. + +- If a `WATCH` command was not issued previously by a client, the transaction execution should continue with its usual operation. + +- We will get to implementing failing transactions based on modification of watched keys in the next stage. \ No newline at end of file diff --git a/stage_descriptions/optimistic-locking-04-bu1.md b/stage_descriptions/optimistic-locking-04-bu1.md new file mode 100644 index 00000000..5b10b563 --- /dev/null +++ b/stage_descriptions/optimistic-locking-04-bu1.md @@ -0,0 +1,83 @@ +In this stage, you'll add support for failing transactions based on the modification of watched keys. + +### Tests + +The tester will execute your program like this: + +``` +$ ./your_program.sh +``` + +The tester will spawn four clients. + +Using the first client, it will set the value of two keys and, issue a `WATCH` command specifying one of the keys + +```bash +# Client 1 +> SET foo 100 (Expecting "+OK\r\n") +> SET bar 200 (Expecting "+OK\r\n") +> WATCH foo +``` + +Using the second client, the tester will update the value of the watched variable. +```bash +# Client 2 +> SET foo 200 (Expecing "+OK\r\n") +> SET foo 100 (Expecing "+OK\r\n") +``` + +Using the first client, the tester will attempt to execute a transaction. +```bash +# Client 1 +> MULTI (Expecting "+OK\r\n") +> SET bar 300 (Expecting "+QUEUED\r\n") +> EXEC (Expecting "*-1\r\n") +``` + +The response to `EXEC` should be a RESP null array. Please note that even if the key's value was restored to its original value before the `WATCH` command was issued, the transaction will still fail, because the key was **modified**. + +Using the first client, the tester will retrieve the value of unwatched key to check if the transaction was aborted. + +```bash +# Client 1 +> GET bar (Expecting bulk string "200") +``` +--- + +Using the third client, the tester will set the value of two keys and, issue a `WATCH` command specifying one of the keys + +```bash +# Client 1 +> SET baz 100 (Expecting "+OK\r\n") +> SET caz 200 (Expecting "+OK\r\n") +> WATCH baz +``` + +Using the fourth client, the tester will modify the value of the **un-watched** variable. +```bash +# Client 2 +> SET caz 200 (Expecing "+OK\r\n") +``` + +Using the third client, the tester will attempt to execute a transaction. +```bash +# Client 1 +> MULTI (Expecting "+OK\r\n") +> SET caz 400 (Expecting "+QUEUED\r\n") +> EXEC (Expecting an array of responses for the queued commands) +``` + +The response to `EXEC` should be its usual response because the watched variable was un-modified. + +Using the fourth client, the tester will retrieve the value of the unwatched variable to check if the transaction succeeded. + +```bash +# Client 1 +> GET caz (Expecting bulk string "400") +``` + + +### Notes + +- The transaction should fail if the watched key was modified before the execution of `EXEC`. +- It is not enough to check the equality of values of watched keys at the time of `WATCH` and `EXEC`. If the watched key was modified, it should be marked as modified, even if its value was restored to its original value before the `WATCH` command. \ No newline at end of file diff --git a/stage_descriptions/optimistic-locking-05-al4.md b/stage_descriptions/optimistic-locking-05-al4.md new file mode 100644 index 00000000..4f501fef --- /dev/null +++ b/stage_descriptions/optimistic-locking-05-al4.md @@ -0,0 +1,63 @@ +In this stage, you'll add support for watching a non-existent key. + +### Optimistic locking using `WATCH` +The WATCH command enables optimistic locking by monitoring non-existent exist keys at the time of the WATCH call. Example Usage: + +``` +# Client A +> WATCH foo ++OK + +# Client B +> SET foo external ++OK + +# Client A +> MULTI ++OK +> SET bar value ++QUEUED +> EXEC +*-1 # Transaction aborts because "foo" was created after WATCH +``` + +### Tests + +The tester will execute your program like this: + +``` +$ ./your_program.sh +``` + +The tester will spawn two clients. + +Using the first client, it will set the value of two keys and, issue a `WATCH` command specifying one of the keys + +```bash +# Client 1 +> WATCH foo +``` + +Using the second client, the tester will modify the watched key. + +```bash +# Client 2 +> SET foo 200 (Expecting "+OK\r\n") +``` + +Using the first client, the tester will attempt to execute an empty transaction. +```bash +# Client 1 +> MULTI (Expecting "+OK\r\n") +> SET foo 300 (Expecting "+QUEUED\r\n") +> EXEC (Expecting "*-1\r\n") +``` + +The response to `EXEC` should be a RESP null array. + +Using the second client, the tester will retrieve the value of the watched key to check if the transaction was aborted. + +```bash +# Client 2 +> GET foo (Expecting bulk string "200") +``` \ No newline at end of file diff --git a/stage_descriptions/optimistic-locking-06-ew6.md b/stage_descriptions/optimistic-locking-06-ew6.md new file mode 100644 index 00000000..59696c3d --- /dev/null +++ b/stage_descriptions/optimistic-locking-06-ew6.md @@ -0,0 +1,68 @@ +In this stage, you'll add support for watching multiple keys. + +### The `WATCH` command (Multiple Keys) +The `WATCH` command can be used to watch multiple keys. After the watch command is issued, a transaction will fail if any one of the watched keys was modified before the executing of the `EXEC` command. + +Example Usage: +``` +# Client A +> WATCH foo bar ++OK + +# Client B +> SET bar external ++OK + +# Client A +> MULTI ++OK +> SET foo bar ++QUEUED +> EXEC +*-1 # Transaction aborts due to watched key being modified +``` + + +### Tests + +The tester will execute your program like this: + +``` +$ ./your_program.sh +``` + +The tester will spawn two clients. + +Using the first client, it will set the value of two keys and, issue a `WATCH` command specifying both the keys + +```bash +# Client 1 +> SET foo 100 (Expecting "+OK\r\n") +> SET bar 200 (Expecting "+OK\r\n") +> WATCH foo +> WATCH bar +``` + +Using the second client, the tester will modify one of the watched key. + +```bash +# Client 2 +> SET foo 200 (Expecting "+OK\r\n") +``` + +Using the first client, the tester will attempt to execute an empty transaction. +```bash +# Client 1 +> MULTI (Expecting "+OK\r\n") +> SET bar 300 (Expecting "+QUEUED\r\n") +> EXEC (Expecting "*-1\r\n") +``` + +The response to `EXEC` should be a RESP null array. + +Using the second client, the tester will retrieve the value of unwatched key to check if the transaction was aborted. + +```bash +# Client 2 +> GET bar (Expecting bulk string "200") +``` \ No newline at end of file diff --git a/stage_descriptions/optimistic-locking-07-mn2.md b/stage_descriptions/optimistic-locking-07-mn2.md new file mode 100644 index 00000000..e6e479f2 --- /dev/null +++ b/stage_descriptions/optimistic-locking-07-mn2.md @@ -0,0 +1,66 @@ +In this stage, you'll add support for the `UNWATCH` command. + +### The `UNWATCH` command + +The `UNWATCH` command is used to flush all the previously watched keys. + +Example Usage: + +``` +$ redis-cli +> WATCH foo +OK +> UNWATCH +OK +``` + +After issuing the `UNWATCH` command, any transaction that is executed after it should not fail if the watched key was modified. + +### Tests +The tester will execute your program like this: + +``` +$ ./your_program.sh +``` + +The tester will spawn two clients. + +Using the first client, it will set the value of two keys and, issue a `WATCH` command specifying both the keys. + +```bash +# Client 1 +> SET foo 100 (Expecting "+OK\r\n") +> SET bar 200 (Expecting "+OK\r\n") +> WATCH foo bar +``` + +Using the second client, the tester will modify the watched key. + +```bash +# Client 2 +> SET foo 200 (Expecting "+OK\r\n") +``` + +Using the first client, the tester will issue a `UNWATCH` command, so that the first client stops watching the keys that were being previously watched. +```bash +# Client 1 +> UNWATCH (Expecting "+OK\r\n") +``` + +Using the first client, the tester will then try to execute a transaction + +```bash +# Client 1 +> MULTI (Expecting "+OK\r\n") +> SET bar 300 (Expecting "+QUEUED\r\n") +> SET foo 400 (Expecting "+QUEUED\r\n") +> EXEC (Expecting an array of responses for the queued commands) +``` + +Using the second client, the tester will retrieve the values of the variables to check if the transaction was executed successfully. + +```bash +# Client 2 +> GET foo (Expecting "400") +> GET bar (Expecting "300") +``` \ No newline at end of file diff --git a/stage_descriptions/optimistic-locking-08-au8.md b/stage_descriptions/optimistic-locking-08-au8.md new file mode 100644 index 00000000..13cf1bdb --- /dev/null +++ b/stage_descriptions/optimistic-locking-08-au8.md @@ -0,0 +1,96 @@ +In this stage, you'll add support clearing watched keys on `EXEC`. + +### Clearing Watched Keys Automatically +After a client issues a WATCH command, the watched keys must be automatically cleared when either EXEC or DISCARD is called. +This means that any future transaction from the same client should not be affected by key modifications that occurred prior to the new transaction. +This behavior ensures that WATCH only affects a single transaction and does not persist across multiple transaction cycles. + +Example Usage: + +``` +# Client A +> WATCH foo ++OK + +# Client B +> SET foo 100 ++OK + +# Client A +> MULTI ++OK +> SET bar 200 ++QUEUED +> EXEC +*-1 # Transaction aborts because "foo" was created after WATCH + +> MULTI +OK +> SET bar 200 +OK +> SET foo 1000 +OK +> EXEC + +1) OK +2) OK # Transaction succeeds because the previous EXEC clears watched keys +``` + +### Tests + +``` +$ ./your_program.sh +``` + +The tester will spawn two clients. + +Using the first client, it will set the value of two keys and, issue a `WATCH` command specifying both keys. + +```bash +# Client 1 +> SET foo 100 (Expecting "+OK\r\n") +> SET bar 200 (Expecting "+OK\r\n") +> WATCH foo bar +``` + +Using the second client, the tester will modify one of the watched keys. + +```bash +# Client 2 +> SET foo 200 (Expecting "+OK\r\n") +``` + +Using the first client, the tester will then try to execute a transaction + +```bash +# Client 1 +> MULTI (Expecting "+OK\r\n") +> SET bar 300 (Expecting "+QUEUED\r\n") +> EXEC (Expecting "*-1\r\n) +``` + +The transaction should abort with a RESP null array response to the `EXEC` command. + +Now, using the first client, the tester will then try to execute a transaction + +```bash +# Client 1 +> MULTI (Expecting "+OK\r\n") +> SET bar 1000 (Expecting "+QUEUED\r\n") +> SET foo 2000 (Expecting "+QUEUED\r\n") +> EXEC (Expecting an array of responses for the queued commands) +``` + +The transaction should succeed. + +Using the second client, the tester will check for the values of variables that were modified in the transaction. + +```bash +# Client 2 +> GET foo (Expecting "1000") +> GET bar (Expecting "2000") +``` + +### Notes + +- In this stage, you'll only implement clearing the watched keys on `EXEC`. We'll get to clearing the watched keys on `DISCARD` in the next stage. \ No newline at end of file diff --git a/stage_descriptions/optimistic-locking-09-vh1.md b/stage_descriptions/optimistic-locking-09-vh1.md new file mode 100644 index 00000000..d80a3520 --- /dev/null +++ b/stage_descriptions/optimistic-locking-09-vh1.md @@ -0,0 +1,56 @@ +In this stage, you'll add support clearing watched keys on `DISCARD`. + +### Tests + +``` +$ ./your_program.sh +``` + +The tester will spawn two clients. + +Using the first client, it will set the value of two keys and, issue a `WATCH` command specifying both keys. + +```bash +# Client 1 +> SET foo 100 (Expecting "+OK\r\n") +> SET bar 200 (Expecting "+OK\r\n") +> WATCH foo bar +``` + +Using the second client, the tester will modify one of the watched keys. + +```bash +# Client 2 +> SET foo 200 (Expecting "+OK\r\n") +``` + +Using the first client, the tester will then try to execute a transaction + +```bash +# Client 1 +> MULTI (Expecting "+OK\r\n") +> SET bar 300 (Expecting "+QUEUED\r\n") +> DISCARD (Expecting "+OK\r\n) +``` + +The transaction should abort with an OK response to the `DISCARD` command. + +Now, using the first client, the tester will then try to execute a transaction + +```bash +# Client 1 +> MULTI (Expecting "+OK\r\n") +> SET bar 1000 (Expecting "+QUEUED\r\n") +> SET foo 2000 (Expecting "+QUEUED\r\n") +> EXEC (Expecting an array of responses for the queued commands) +``` + +The transaction should succeed. + +Using the second client, the tester will check for the values of variables that were modified in the transaction. + +```bash +# Client 2 +> GET foo (Expecting "1000") +> GET bar (Expecting "2000") +``` \ No newline at end of file