From d2583b86fddbce4af043e9b6169fa1b5d22afc1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 00:51:25 +0200 Subject: [PATCH 01/19] Fix comments in cluster-tutorial and cluster-spec, #140 and #141 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove junk line numbers in code examples and incorrect output in the valkey-cli prompt in examples in in cluster-tutorial. * Remove reference to Redis OSS 3 and revert an occurrence of "adapterd to Redis coding style", since that's what this old code was. Signed-off-by: Viktor Söderqvist --- topics/cluster-spec.md | 4 +- topics/cluster-tutorial.md | 84 +++++++++++++++++++------------------- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/topics/cluster-spec.md b/topics/cluster-spec.md index c6b5bf55..bbd46149 100644 --- a/topics/cluster-spec.md +++ b/topics/cluster-spec.md @@ -20,8 +20,6 @@ Valkey Cluster is a distributed implementation of Valkey with the following goal * Acceptable degree of write safety: the system tries (in a best-effort way) to retain all the writes originating from clients connected with the majority of the master nodes. Usually there are small windows where acknowledged writes can be lost. Windows to lose acknowledged writes are larger when clients are in a minority partition. * Availability: Valkey Cluster is able to survive partitions where the majority of the master nodes are reachable and there is at least one reachable replica for every master node that is no longer reachable. Moreover using *replicas migration*, masters no longer replicated by any replica will receive one from a master which is covered by multiple replicas. -What is described in this document is implemented in Redis OSS 3.0 or greater. - ### Implemented subset Valkey Cluster implements all the single key commands available in the @@ -1208,7 +1206,7 @@ The cluster makes sure the published shard messages are forwarded to all nodes i /* * Copyright 2001-2010 Georges Menie (www.menie.org) - * Copyright 2010 Salvatore Sanfilippo (adapted to Valkey coding style) + * Copyright 2010 Salvatore Sanfilippo (adapted to Redis coding style) * All rights reserved. * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/topics/cluster-tutorial.md b/topics/cluster-tutorial.md index 0709059d..016adb7f 100644 --- a/topics/cluster-tutorial.md +++ b/topics/cluster-tutorial.md @@ -325,16 +325,16 @@ You can also test your Valkey Cluster using the `valkey-cli` command line utilit ``` $ valkey-cli -c -p 7000 -valkey 127.0.0.1:7000> set foo bar +127.0.0.1:7000> set foo bar -> Redirected to slot [12182] located at 127.0.0.1:7002 OK -valkey 127.0.0.1:7002> set hello world +127.0.0.1:7002> set hello world -> Redirected to slot [866] located at 127.0.0.1:7000 OK -valkey 127.0.0.1:7000> get foo +127.0.0.1:7000> get foo -> Redirected to slot [12182] located at 127.0.0.1:7002 "bar" -valkey 127.0.0.1:7002> get hello +127.0.0.1:7002> get hello -> Redirected to slot [866] located at 127.0.0.1:7000 "world" ``` @@ -370,44 +370,44 @@ The first is the following, and is the [`example.rb`](https://github.com/antirez/redis-rb-cluster/blob/master/example.rb) file inside the redis-rb-cluster distribution: -``` - 1 require './cluster' - 2 - 3 if ARGV.length != 2 - 4 startup_nodes = [ - 5 {:host => "127.0.0.1", :port => 7000}, - 6 {:host => "127.0.0.1", :port => 7001} - 7 ] - 8 else - 9 startup_nodes = [ - 10 {:host => ARGV[0], :port => ARGV[1].to_i} - 11 ] - 12 end - 13 - 14 rc = RedisCluster.new(startup_nodes,32,:timeout => 0.1) - 15 - 16 last = false - 17 - 18 while not last - 19 begin - 20 last = rc.get("__last__") - 21 last = 0 if !last - 22 rescue => e - 23 puts "error #{e.to_s}" - 24 sleep 1 - 25 end - 26 end - 27 - 28 ((last.to_i+1)..1000000000).each{|x| - 29 begin - 30 rc.set("foo#{x}",x) - 31 puts rc.get("foo#{x}") - 32 rc.set("__last__",x) - 33 rescue => e - 34 puts "error #{e.to_s}" - 35 end - 36 sleep 0.1 - 37 } +```ruby +require './cluster' + +if ARGV.length != 2 + startup_nodes = [ + {:host => "127.0.0.1", :port => 7000}, + {:host => "127.0.0.1", :port => 7001} + ] +else + startup_nodes = [ + {:host => ARGV[0], :port => ARGV[1].to_i} + ] +end + +rc = RedisCluster.new(startup_nodes,32,:timeout => 0.1) + +last = false + +while not last + begin + last = rc.get("__last__") + last = 0 if !last + rescue => e + puts "error #{e.to_s}" + sleep 1 + end +end + +((last.to_i+1)..1000000000).each{|x| + begin + rc.set("foo#{x}",x) + puts rc.get("foo#{x}") + rc.set("__last__",x) + rescue => e + puts "error #{e.to_s}" + end + sleep 0.1 +} ``` The application does a very simple thing, it sets keys in the form `foo` to `number`, one after the other. So if you run the program the result is the From acf007fc856cd3b6a8672baa24993e35d4be59d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 01:09:40 +0200 Subject: [PATCH 02/19] Delete references to old CPUs in benchmark.md, fixes #139 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/benchmark.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/topics/benchmark.md b/topics/benchmark.md index 52f060c5..aa20d5cf 100644 --- a/topics/benchmark.md +++ b/topics/benchmark.md @@ -325,12 +325,7 @@ world scenarios, Valkey throughput is limited by the network well before being limited by the CPU. To consolidate several high-throughput Valkey instances on a single server, it worth considering putting a 10 Gbit/s NIC or multiple 1 Gbit/s NICs with TCP/IP bonding. -+ CPU is another very important factor. Being single-threaded, Valkey favors -fast CPUs with large caches and not many cores. At this game, Intel CPUs are -currently the winners. It is not uncommon to get only half the performance on -an AMD Opteron CPU compared to similar Nehalem EP/Westmere EP/Sandy Bridge -Intel CPUs with Valkey. When client and server run on the same box, the CPU is -the limiting factor with valkey-benchmark. ++ CPU is another important factor. + Speed of RAM and memory bandwidth seem less critical for global performance especially for small objects. For large objects (>10 KB), it may become noticeable though. Usually, it is not really cost-effective to buy expensive From 6c5abd270ce8e2a0f3a246bc3c871b5a463dff85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 01:15:22 +0200 Subject: [PATCH 03/19] Address comments for twitter-clone.md, fixes #138 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/twitter-clone.md | 210 +++++++++++++++++++++------------------- 1 file changed, 110 insertions(+), 100 deletions(-) diff --git a/topics/twitter-clone.md b/topics/twitter-clone.md index 12ce23b4..d5dee147 100644 --- a/topics/twitter-clone.md +++ b/topics/twitter-clone.md @@ -8,8 +8,8 @@ This article describes the design and implementation of a [very simple Twitter c Note: the original version of this article was written in 2009 when Redis OSS was released. It was not exactly clear at that time that the data model was -suitable to write entire applications. After 5 years there were many cases of -applications using Redis OSS as their main store, so the goal of the article today +suitable to write entire applications. There were many cases of +applications using Valkeyt as their main store, so the goal of the article today is to be a tutorial for newcomers. You'll learn how to design a simple data layout using Valkey, and how to apply different data structures. @@ -249,27 +249,29 @@ In order to authenticate a user we'll do these simple steps (see the `login.php` This is the actual code: - include("retwis.php"); - - # Form sanity checks - if (!gt("username") || !gt("password")) - goback("You need to enter both username and password to login."); - - # The form is ok, check if the username is available - $username = gt("username"); - $password = gt("password"); - $r = redisLink(); - $userid = $r->hget("users",$username); - if (!$userid) - goback("Wrong username or password"); - $realpassword = $r->hget("user:$userid","password"); - if ($realpassword != $password) - goback("Wrong username or password"); - - # Username / password OK, set the cookie and redirect to index.php - $authsecret = $r->hget("user:$userid","auth"); - setcookie("auth",$authsecret,time()+3600*24*365); - header("Location: index.php"); +```php +include("retwis.php"); + +# Form sanity checks +if (!gt("username") || !gt("password")) + goback("You need to enter both username and password to login."); + +# The form is ok, check if the username is available +$username = gt("username"); +$password = gt("password"); +$r = redisLink(); +$userid = $r->hget("users",$username); +if (!$userid) + goback("Wrong username or password"); +$realpassword = $r->hget("user:$userid","password"); +if ($realpassword != $password) + goback("Wrong username or password"); + +# Username / password OK, set the cookie and redirect to index.php +$authsecret = $r->hget("user:$userid","auth"); +setcookie("auth",$authsecret,time()+3600*24*365); +header("Location: index.php"); +``` This happens every time a user logs in, but we also need a function `isLoggedIn` in order to check if a given user is already authenticated or not. These are the logical steps preformed by the `isLoggedIn` function: @@ -280,53 +282,57 @@ This happens every time a user logs in, but we also need a function `isLoggedIn` The code is simpler than the description, possibly: - function isLoggedIn() { - global $User, $_COOKIE; +```php +function isLoggedIn() { + global $User, $_COOKIE; - if (isset($User)) return true; + if (isset($User)) return true; - if (isset($_COOKIE['auth'])) { - $r = redisLink(); - $authcookie = $_COOKIE['auth']; - if ($userid = $r->hget("auths",$authcookie)) { - if ($r->hget("user:$userid","auth") != $authcookie) return false; - loadUserInfo($userid); - return true; - } + if (isset($_COOKIE['auth'])) { + $r = redisLink(); + $authcookie = $_COOKIE['auth']; + if ($userid = $r->hget("auths",$authcookie)) { + if ($r->hget("user:$userid","auth") != $authcookie) return false; + loadUserInfo($userid); + return true; } - return false; } + return false; +} - function loadUserInfo($userid) { - global $User; +function loadUserInfo($userid) { + global $User; - $r = redisLink(); - $User['id'] = $userid; - $User['username'] = $r->hget("user:$userid","username"); - return true; - } + $r = redisLink(); + $User['id'] = $userid; + $User['username'] = $r->hget("user:$userid","username"); + return true; +} +``` Having `loadUserInfo` as a separate function is overkill for our application, but it's a good approach in a complex application. The only thing that's missing from all the authentication is the logout. What do we do on logout? That's simple, we'll just change the random string in user:1000 `auth` field, remove the old authentication secret from the `auths` Hash, and add the new one. *Important:* the logout procedure explains why we don't just authenticate the user after looking up the authentication secret in the `auths` Hash, but double check it against user:1000 `auth` field. The true authentication string is the latter, while the `auths` Hash is just an authentication field that may even be volatile, or, if there are bugs in the program or a script gets interrupted, we may even end with multiple entries in the `auths` key pointing to the same user ID. The logout code is the following (`logout.php`): - include("retwis.php"); +```php +include("retwis.php"); - if (!isLoggedIn()) { - header("Location: index.php"); - exit; - } +if (!isLoggedIn()) { + header("Location: index.php"); + exit; +} - $r = redisLink(); - $newauthsecret = getrand(); - $userid = $User['id']; - $oldauthsecret = $r->hget("user:$userid","auth"); +$r = redisLink(); +$newauthsecret = getrand(); +$userid = $User['id']; +$oldauthsecret = $r->hget("user:$userid","auth"); - $r->hset("user:$userid","auth",$newauthsecret); - $r->hset("auths",$newauthsecret,$userid); - $r->hdel("auths",$oldauthsecret); +$r->hset("user:$userid","auth",$newauthsecret); +$r->hset("auths",$newauthsecret,$userid); +$r->hdel("auths",$oldauthsecret); - header("Location: index.php"); +header("Location: index.php"); +``` That is just what we described and should be simple to understand. @@ -342,29 +348,31 @@ As you can see each post is just represented by a Hash with three fields. The ID After we create a post and we obtain the post ID, we need to LPUSH the ID in the timeline of every user that is following the author of the post, and of course in the list of posts of the author itself (everybody is virtually following herself/himself). This is the file `post.php` that shows how this is performed: - include("retwis.php"); +```php +include("retwis.php"); - if (!isLoggedIn() || !gt("status")) { - header("Location:index.php"); - exit; - } +if (!isLoggedIn() || !gt("status")) { + header("Location:index.php"); + exit; +} - $r = redisLink(); - $postid = $r->incr("next_post_id"); - $status = str_replace("\n"," ",gt("status")); - $r->hmset("post:$postid","user_id",$User['id'],"time",time(),"body",$status); - $followers = $r->zrange("followers:".$User['id'],0,-1); - $followers[] = $User['id']; /* Add the post to our own posts too */ - - foreach($followers as $fid) { - $r->lpush("posts:$fid",$postid); - } - # Push the post on the timeline, and trim the timeline to the - # newest 1000 elements. - $r->lpush("timeline",$postid); - $r->ltrim("timeline",0,1000); +$r = redisLink(); +$postid = $r->incr("next_post_id"); +$status = str_replace("\n"," ",gt("status")); +$r->hmset("post:$postid","user_id",$User['id'],"time",time(),"body",$status); +$followers = $r->zrange("followers:".$User['id'],0,-1); +$followers[] = $User['id']; /* Add the post to our own posts too */ - header("Location: index.php"); +foreach($followers as $fid) { + $r->lpush("posts:$fid",$postid); +} +# Push the post on the timeline, and trim the timeline to the +# newest 1000 elements. +$r->lpush("timeline",$postid); +$r->ltrim("timeline",0,1000); + +header("Location: index.php"); +``` The core of the function is the `foreach` loop. We use `ZRANGE` to get all the followers of the current user, then the loop will `LPUSH` the push the post in every follower timeline List. @@ -383,36 +391,38 @@ Paginating updates Now it should be pretty clear how we can use `LRANGE` in order to get ranges of posts, and render these posts on the screen. The code is simple: - function showPost($id) { - $r = redisLink(); - $post = $r->hgetall("post:$id"); - if (empty($post)) return false; +```php +function showPost($id) { + $r = redisLink(); + $post = $r->hgetall("post:$id"); + if (empty($post)) return false; - $userid = $post['user_id']; - $username = $r->hget("user:$userid","username"); - $elapsed = strElapsed($post['time']); - $userlink = "".utf8entities($username).""; + $userid = $post['user_id']; + $username = $r->hget("user:$userid","username"); + $elapsed = strElapsed($post['time']); + $userlink = "".utf8entities($username).""; - echo('
'.$userlink.' '.utf8entities($post['body'])."
"); - echo('posted '.$elapsed.' ago via web
'); - return true; - } + echo('
'.$userlink.' '.utf8entities($post['body'])."
"); + echo('posted '.$elapsed.' ago via web
'); + return true; +} - function showUserPosts($userid,$start,$count) { - $r = redisLink(); - $key = ($userid == -1) ? "timeline" : "posts:$userid"; - $posts = $r->lrange($key,$start,$start+$count); - $c = 0; - foreach($posts as $p) { - if (showPost($p)) $c++; - if ($c == $count) break; - } - return count($posts) == $count+1; +function showUserPosts($userid,$start,$count) { + $r = redisLink(); + $key = ($userid == -1) ? "timeline" : "posts:$userid"; + $posts = $r->lrange($key,$start,$start+$count); + $c = 0; + foreach($posts as $p) { + if (showPost($p)) $c++; + if ($c == $count) break; } + return count($posts) == $count+1; +} +``` `showPost` will simply convert and print a Post in HTML while `showUserPosts` gets a range of posts and then passes them to `showPosts`. -*Note: `LRANGE` is not very efficient if the list of posts start to be very +**Note:** `LRANGE` is not very efficient if the list of posts start to be very big, and we want to access elements which are in the middle of the list, since Lists are backed by linked lists. If a system is designed for deep pagination of million of items, it is better to resort to Sorted Sets instead.* @@ -422,8 +432,8 @@ Following users It is not hard, but we did not yet check how we create following / follower relationships. If user ID 1000 (antirez) wants to follow user ID 5000 (pippo), we need to create both a following and a follower relationship. We just need to `ZADD` calls: - ZADD following:1000 5000 - ZADD followers:5000 1000 + ZADD following:1000 5000 + ZADD followers:5000 1000 Note the same pattern again and again. In theory with a relational database, the list of following and followers would be contained in a single table with fields like `following_id` and `follower_id`. You can extract the followers or following of every user using an SQL query. With a key-value DB things are a bit different since we need to set both the `1000 is following 5000` and `5000 is followed by 1000` relations. This is the price to pay, but on the other hand accessing the data is simpler and extremely fast. Having these things as separate sets allows us to do interesting stuff. For example, using `ZINTERSTORE` we can have the intersection of `following` of two different users, so we may add a feature to our Twitter clone so that it is able to tell you very quickly when you visit somebody else's profile, "you and Alice have 34 followers in common", and things like that. From f219bbed32ef59fdb1dc23cb2578c909f5773665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 01:29:41 +0200 Subject: [PATCH 04/19] transactions.md, #137 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/transactions.md | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/topics/transactions.md b/topics/transactions.md index 85171eac..1eef95c5 100644 --- a/topics/transactions.md +++ b/topics/transactions.md @@ -208,10 +208,8 @@ like expiration or eviction. If keys were modified between when they were `WATCH`ed and when the `EXEC` was received, the entire transaction will be aborted instead. -**NOTE** -* In Redis OSS versions before 6.0.9, an expired key would not cause a transaction -to be aborted. [More on this](https://github.com/redis/redis/pull/7920) -* Commands within a transaction won't trigger the `WATCH` condition since they +**NOTE:** +Commands within a transaction won't trigger the `WATCH` condition since they are only queued until the `EXEC` is sent. `WATCH` can be called multiple times. Simply all the `WATCH` calls will @@ -231,14 +229,12 @@ of the keys we don't want to proceed. When this happens we just call `UNWATCH` so that the connection can already be used freely for new transactions. -### Using WATCH to implement ZPOP +### Using WATCH to implement ZPOPMIN -A good example to illustrate how `WATCH` can be used to create new -atomic operations otherwise not supported by Valkey is to implement ZPOP -(`ZPOPMIN`, `ZPOPMAX` and their blocking variants have only been added -in version 5.0), that is a command that pops the element with the lower -score from a sorted set in an atomic way. This is the simplest -implementation: +An example to illustrate how `WATCH` can be used to create +atomic operations is to implement `ZPOPMIN`, +that is a command that pops the element with the lower +score from a sorted set in an atomic way. This is a possible implementation: ``` WATCH zset @@ -252,7 +248,6 @@ If `EXEC` fails (i.e. returns a [Null reply](protocol.md#nil-reply)) we just rep ## Valkey scripting and transactions -Something else to consider for transaction like operations in redis are -[redis scripts](../commands/eval.md) which are transactional. Everything -you can do with a Valkey Transaction, you can also do with a script, and -usually the script will be both simpler and faster. +Something else to consider for transaction-like operations are +[scripts](../commands/eval.md) which are transactional. Everything +you can do with a Valkey Transaction, you can also do with a script. From 3b0f6635d7a9d186b8acc7cf84a707379bed3bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 01:31:49 +0200 Subject: [PATCH 05/19] streams-intro.md, delete link to self, #136 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/streams-intro.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/topics/streams-intro.md b/topics/streams-intro.md index f3359a7a..be2b903f 100644 --- a/topics/streams-intro.md +++ b/topics/streams-intro.md @@ -918,10 +918,3 @@ A few remarks: * Here we processed up to 10k messages per iteration, this means that the `COUNT` parameter of `XREADGROUP` was set to 10000. This adds a lot of latency but is needed in order to allow the slow consumers to be able to keep with the message flow. So you can expect a real world latency that is a lot smaller. * The system used for this benchmark is very slow compared to today's standards. - - - - -## Learn more - -* The [Streams Tutorial](streams-intro.md) explains Streams with many examples. From ff0ce4c55a0ae70941d7d728eaf3c0e1f0e01bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 01:37:55 +0200 Subject: [PATCH 06/19] sorted-sets.md, remove ref to Redis OSS 2.8, #135 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/sorted-sets.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/topics/sorted-sets.md b/topics/sorted-sets.md index 9a79e0a9..1f4fee50 100644 --- a/topics/sorted-sets.md +++ b/topics/sorted-sets.md @@ -142,11 +142,11 @@ the elements sorted in a descending way. ### Lexicographical scores -In version Redis OSS 2.8, a new feature was introduced that allows +A family of commands allow getting ranges lexicographically, assuming elements in a sorted set are all -inserted with the same identical score (elements are compared with the C +inserted with the same identical score. Elements are compared with the C `memcmp` function, so it is guaranteed that there is no collation, and every -Valkey instance will reply with the same output). +Valkey instance will reply with the same output. The main commands to operate with lexicographical ranges are `ZRANGEBYLEX`, `ZREVRANGEBYLEX`, `ZREMRANGEBYLEX` and `ZLEXCOUNT`. From 1b338115f3aa1766699145d84b053aed4e040bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 01:45:17 +0200 Subject: [PATCH 07/19] signals.md, #134 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/signals.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/topics/signals.md b/topics/signals.md index 1a23c441..98131600 100644 --- a/topics/signals.md +++ b/topics/signals.md @@ -22,7 +22,7 @@ This shutdown process includes the following actions: * If there are any replicas lagging behind in replication: * Pause clients attempting to write with `CLIENT PAUSE` and the `WRITE` option. - * Wait up to the configured `shutdown-timeout` (default 10 seconds) for replicas to catch up with the master's replication offset. + * Wait up to the configured `shutdown-timeout` (default 10 seconds) for replicas to catch up with the primary's replication offset. * If a background child is saving the RDB file or performing an AOF rewrite, the child process is killed. * If the AOF is active, Valkey calls the `fsync` system call on the AOF file descriptor to flush the buffers on disk. * If Valkey is configured to persist on disk using RDB files, a synchronous (blocking) save is performed. Since the save is synchronous, it doesn't use any additional memory. @@ -36,8 +36,8 @@ No further attempt to shut down will be made unless a new `SIGTERM` is received Since Redis OSS 7.0, the server waits for lagging replicas up to a configurable `shutdown-timeout`, 10 seconds by default, before shutting down. This provides a best effort to minimize the risk of data loss in a situation where no save points are configured and AOF is deactivated. -Before version 7.0, shutting down a heavily loaded master node in a diskless setup was more likely to result in data loss. -To minimize the risk of data loss in such setups, trigger a manual `FAILOVER` (or `CLUSTER FAILOVER`) to demote the master to a replica and promote one of the replicas to a new master before shutting down a master node. +Before version 7.0, shutting down a heavily loaded primary node in a diskless setup was more likely to result in data loss. +To minimize the risk of data loss in such setups, trigger a manual `FAILOVER` (or `CLUSTER FAILOVER`) to demote the primary to a replica and promote one of the replicas to a new primary before shutting down a primary node. ## SIGSEGV, SIGBUS, SIGFPE and SIGILL @@ -51,7 +51,7 @@ The following signals are handled as a Valkey crash: Once one of these signals is trapped, Valkey stops any current operation and performs the following actions: * Adds a bug report to the log file. This includes a stack trace, dump of registers, and information about the state of clients. -* Since Redis OSS 2.8, a fast memory test is performed as a first check of the reliability of the crashing system. +* A fast memory test is performed as a first check of the reliability of the crashing system. * If the server was daemonized, the PID file is removed. * Finally the server unregisters its own signal handler for the received signal and resends the same signal to itself to make sure that the default action is performed, such as dumping the core on the file system. From f7b63ee786d206e7ee7456b944fb993a9b25711e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 02:00:22 +0200 Subject: [PATCH 08/19] sentinel.md, fix version references and a link issue, #133 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/sentinel.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/topics/sentinel.md b/topics/sentinel.md index d34a4a39..4bcab42d 100644 --- a/topics/sentinel.md +++ b/topics/sentinel.md @@ -460,7 +460,7 @@ Once you start the three Sentinels, you'll see a few messages they log, like: +monitor master mymaster 127.0.0.1 6379 quorum 2 This is a Sentinel event, and you can receive this kind of events via Pub/Sub -if you `SUBSCRIBE` to the event name as specified later in [_Pub/Sub Messages_ section](#pubsub-messages). +if you `SUBSCRIBE` to the event name as specified later in [_Pubsub Messages_ section](#pubsub-messages). Sentinel generates and logs different events during failure detection and failover. @@ -599,7 +599,7 @@ The `SENTINEL` command is the main API for Sentinel. The following is the list o * **SENTINEL FLUSHCONFIG** Force Sentinel to rewrite its configuration on disk, including the current Sentinel state. Normally Sentinel rewrites the configuration every time something changes in its state (in the context of the subset of the state which is persisted on disk across restart). However sometimes it is possible that the configuration file is lost because of operation errors, disk failures, package upgrade scripts or configuration managers. In those cases a way to force Sentinel to rewrite the configuration file is handy. This command works even if the previous configuration file is completely missing. * **SENTINEL FAILOVER ``** Force a failover as if the master was not reachable, and without asking for agreement to other Sentinels (however a new version of the configuration will be published so that the other Sentinels will update their configurations). * **SENTINEL GET-MASTER-ADDR-BY-NAME ``** Return the ip and port number of the master with that name. If a failover is in progress or terminated successfully for this master it returns the address and port of the promoted replica. -* **SENTINEL INFO-CACHE** (`>= 3.2`) Return cached `INFO` output from masters and replicas. +* **SENTINEL INFO-CACHE** Return cached `INFO` output from masters and replicas. * **SENTINEL IS-MASTER-DOWN-BY-ADDR ** Check if the master specified by ip:port is down from current Sentinel's point of view. This command is mostly for internal use. * **SENTINEL MASTER ``** Show the state and info of the specified master. * **SENTINEL MASTERS** Show a list of monitored masters and their state. @@ -607,16 +607,16 @@ The `SENTINEL` command is the main API for Sentinel. The following is the list o * **SENTINEL MYID** (`>= 6.2`) Return the ID of the Sentinel instance. * **SENTINEL PENDING-SCRIPTS** This command returns information about pending scripts. * **SENTINEL REMOVE** Stop Sentinel's monitoring. Refer to the [_Reconfiguring Sentinel at Runtime_ section](#reconfiguring-sentinel-at-runtime) for more information. -* **SENTINEL REPLICAS ``** (`>= 5.0`) Show a list of replicas for this master, and their state. +* **SENTINEL REPLICAS ``** Show a list of replicas for this master, and their state. * **SENTINEL SENTINELS ``** Show a list of sentinel instances for this master, and their state. * **SENTINEL SET** Set Sentinel's monitoring configuration. Refer to the [_Reconfiguring Sentinel at Runtime_ section](#reconfiguring-sentinel-at-runtime) for more information. -* **SENTINEL SIMULATE-FAILURE (crash-after-election|crash-after-promotion|help)** (`>= 3.2`) This command simulates different Sentinel crash scenarios. +* **SENTINEL SIMULATE-FAILURE (crash-after-election|crash-after-promotion|help)** This command simulates different Sentinel crash scenarios. * **SENTINEL RESET ``** This command will reset all the masters with matching name. The pattern argument is a glob-style pattern. The reset process clears any previous state in a master (including a failover in progress), and removes every replica and sentinel already discovered and associated with the master. For connection management and administration purposes, Sentinel supports the following subset of Valkey's commands: * **ACL** (`>= 6.2`) This command manages the Sentinel Access Control List. For more information refer to the [ACL](acl.md) documentation page and the [_Sentinel Access Control List authentication_](#sentinel-access-control-list-authentication). -* **AUTH** (`>= 5.0.1`) Authenticate a client connection. For more information refer to the `AUTH` command and the [_Configuring Sentinel instances with authentication_ section](#configuring-sentinel-instances-with-authentication). +* **AUTH** Authenticate a client connection. For more information refer to the `AUTH` command and the [_Configuring Sentinel instances with authentication_ section](#configuring-sentinel-instances-with-authentication). * **CLIENT** This command manages client connections. For more information refer to its subcommands' pages. * **COMMAND** (`>= 6.2`) This command returns information about commands. For more information refer to the `COMMAND` command and its various subcommands. * **HELLO** (`>= 6.0`) Switch the connection's protocol. For more information refer to the `HELLO` command. @@ -647,7 +647,7 @@ As already stated, `SENTINEL SET` can be used to set all the configuration param Note that there is no equivalent GET command since `SENTINEL MASTER` provides all the configuration parameters in a simple to parse format (as a field/value pairs array). -Starting with Valkey OSS 6.2, Sentinel also allows getting and setting global configuration parameters which were only supported in the configuration file prior to that. +Sentinel also allows getting and setting global configuration parameters which were only supported in the configuration file prior to that. * **SENTINEL CONFIG GET ``** Get the current value of a global Sentinel configuration parameter. The specified name may be a wildcard, similar to the Valkey `CONFIG GET` command. * **SENTINEL CONFIG SET `` ``** Set the value of a global Sentinel configuration parameter. @@ -707,7 +707,7 @@ to all the Sentinels: they'll refresh the list of replicas within the next 10 seconds, only adding the ones listed as correctly replicating from the current master `INFO` output. -### Pub/Sub messages +### Pubsub messages A client can use a Sentinel as a Valkey-compatible Pub/Sub server (but you can't use `PUBLISH`) in order to `SUBSCRIBE` or `PSUBSCRIBE` to @@ -793,7 +793,7 @@ used for the asynchronous replication protocol. ## Valkey Access Control List authentication -Starting with Valkey OSS 6, user authentication and permission is managed with the [Access Control List (ACL)](acl.md). +User authentication and permission is managed with the [Access Control List (ACL)](acl.md). In order for Sentinels to connect to Valkey server instances when they are configured with ACL, the Sentinel configuration must include the @@ -808,7 +808,7 @@ Where `` and `` are the username and password for accessing ### Valkey password-only authentication -Until Valkey OSS 6, authentication is achieved using the following configuration directives: +Before ACL was introduced, authentication could be achieved using the following configuration directives: * `requirepass` in the master, in order to set the authentication password, and to make sure the instance will not process requests for non authenticated clients. * `masterauth` in the replicas in order for the replicas to authenticate with the master in order to correctly replicate data from it. @@ -837,7 +837,7 @@ configured with `requirepass`, the Sentinel configuration must include the Configuring Sentinel instances with authentication --- -Sentinel instances themselves can be secured by requiring clients to authenticate via the `AUTH` command. Starting with Valkey OSS 6.2, the [Access Control List (ACL)](acl.md) is available, whereas previous versions (starting with Valkey OSS 5.0.1) support password-only authentication. +Sentinel instances themselves can be secured by requiring clients to authenticate via the `AUTH` command. Starting with Redis OSS 6.2, the [Access Control List (ACL)](acl.md) is available, whereas older versions support password-only authentication. Note that Sentinel's authentication configuration should be **applied to each of the instances** in your deployment, and **all instances should use the same configuration**. Furthermore, ACL and password-only authentication should not be used together. @@ -1165,7 +1165,7 @@ replication and the discarding nature of the "virtual" merge function of the sys 1. Use synchronous replication (and a proper consensus algorithm to run a replicated state machine). 2. Use an eventually consistent system where different versions of the same object can be merged. -Valkey (like it's predecessor Valkey OSS) is currently not able to use any of the above systems, and using them is currently outside the development goals. However, there are proxies implementing solution "2" on top of Valkey OSS stores such as SoundCloud [Roshi](https://github.com/soundcloud/roshi), or Netflix [Dynomite](https://github.com/Netflix/dynomite). +Valkey (like it's predecessor Redis OSS) is currently not able to use any of the above systems, and using them is currently outside the development goals. However, there are proxies implementing solution "2" on top of Redis OSS stores such as SoundCloud [Roshi](https://github.com/soundcloud/roshi), or Netflix [Dynomite](https://github.com/Netflix/dynomite). Sentinel persistent state --- @@ -1227,4 +1227,4 @@ API that many kernels offer. However it is not still clear if this is a good solution since the current system avoids issues in case the process is just suspended or not executed by the scheduler for a long time. -**A note about the word slave used in this man page**: Starting with Valkey OSS 5, if not for backward compatibility, the Valkey project no longer uses the word slave. Unfortunately in this command the word slave is part of the protocol, so we'll be able to remove such occurrences only when this API will be naturally deprecated. +**A note about the word slave used in this man page**: If not for backward compatibility, the Valkey project no longer uses the word slave. Unfortunately in this command the word slave is part of the protocol, so we'll be able to remove such occurrences only when this API will be naturally deprecated. From a2516b055488dc16b0119b0c56b562bf849d53bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 02:04:20 +0200 Subject: [PATCH 09/19] sentinel-clients.md delete refs to old versions and redis google group, #132 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/sentinel-clients.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/topics/sentinel-clients.md b/topics/sentinel-clients.md index 386dce01..be54dee7 100644 --- a/topics/sentinel-clients.md +++ b/topics/sentinel-clients.md @@ -67,8 +67,6 @@ Once the client discovered the address of the master instance, it should attempt a connection with the master, and call the `ROLE` command in order to verify the role of the instance is actually a master. -If the `ROLE` commands is not available (it was introduced in Redis OSS 2.8.12), a client may resort to the `INFO replication` command parsing the `role:` field of the output. - If the instance is not a master as expected, the client should wait a short amount of time (a few hundreds of milliseconds) and should try again starting from Step 1. Handling reconnections @@ -84,7 +82,7 @@ In the above cases and any other case where the client lost the connection with Sentinel failover disconnection === -Starting with Redis OSS 2.8.12, when Valkey Sentinel changes the configuration of +When Valkey Sentinel changes the configuration of an instance, for example promoting a replica to a master, demoting a master to replicate to the new master after a failover, or simply changing the master address of a stale replica instance, it sends a `CLIENT KILL type normal` @@ -151,8 +149,3 @@ document in order to resolve the new Valkey master (or replica) address. However update messages received via Pub/Sub should not substitute the above procedure, since there is no guarantee that a client is able to receive all the update messages. - -Additional information -=== - -For additional information or to discuss specific aspects of this guidelines, please drop a message to the [Valkey Google Group](https://groups.google.com/group/redis-db). From 74937179bd7603b8fc2048450980dff3e9f5c361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 02:14:12 +0200 Subject: [PATCH 10/19] security.md delete outdated information, #131 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/security.md | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/topics/security.md b/topics/security.md index 48b7f49f..ced4db3b 100644 --- a/topics/security.md +++ b/topics/security.md @@ -70,7 +70,7 @@ disable protected mode or manually bind all the interfaces. ## Authentication Valkey provides two ways to authenticate clients. -The recommended authentication method, introduced in Valkey OSS 6.0, is via Access Control Lists, allowing named users to be created and assigned fine-grained permissions. +The recommended authentication method is via Access Control Lists, allowing named users to be created and assigned fine-grained permissions. Read more about Access Control Lists [here](acl.md). The legacy authentication method is enabled by editing the **valkey.conf** file, and providing a database password using the `requirepass` setting. @@ -101,26 +101,6 @@ perform eavesdropping. Valkey has optional support for TLS on all communication channels, including client connections, replication links, and the Valkey Cluster bus protocol. -## Disallowing specific commands - -It is possible to disallow commands in Valkey or to rename them as an unguessable -name, so that normal clients are limited to a specified set of commands. - -For instance, a virtualized server provider may offer a managed Valkey instance -service. In this context, normal users should probably not be able to -call the **CONFIG** command to alter the configuration of the instance, -but the systems that provide and remove instances should be able to do so. - -In this case, it is possible to either rename or completely shadow commands from -the command table. This feature is available as a statement that can be used -inside the valkey.conf configuration file. For example: - - rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52 - -In the above example, the **CONFIG** command was renamed into an unguessable name. It is also possible to completely disallow it (or any other command) by renaming it to the empty string, like in the following example: - - rename-command CONFIG "" - ## Attacks triggered by malicious inputs from external clients There is a class of attacks that an attacker can trigger from the outside even @@ -152,16 +132,8 @@ While it would be a strange use case, the application should avoid composing the ## Code security -In a classical setup, clients are allowed full access to the command set, -but accessing the instance should never result in the ability to control the -system where Valkey is running. - Internally, Valkey uses all the well-known practices for writing secure code to prevent buffer overflows, format bugs, and other memory corruption issues. -However, the ability to control the server configuration using the **CONFIG** -command allows the client to change the working directory of the program and -the name of the dump file. This allows clients to write RDB Valkey files -to random paths. This is [a security issue](http://antirez.com/news/96) that may lead to the ability to compromise the system and/or run untrusted code as the same user as Valkey is running. Valkey does not require root privileges to run. It is recommended to run it as an unprivileged *valkey* user that is only used for this purpose. From b037085c5eab41baeaa17fb9e99939e51c00edb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 02:23:57 +0200 Subject: [PATCH 11/19] replication.md, delete refs to Redis OSS 4, #130 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/replication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/topics/replication.md b/topics/replication.md index 63587def..7fa01b16 100644 --- a/topics/replication.md +++ b/topics/replication.md @@ -35,9 +35,9 @@ about high availability and failover. The rest of this document mainly describes * Valkey uses asynchronous replication, with asynchronous replica-to-master acknowledges of the amount of data processed. * A master can have multiple replicas. -* Replicas are able to accept connections from other replicas. Aside from connecting a number of replicas to the same master, replicas can also be connected to other replicas in a cascading-like structure. Since Redis OSS 4.0, all the sub-replicas will receive exactly the same replication stream from the master. +* Replicas are able to accept connections from other replicas. Aside from connecting a number of replicas to the same master, replicas can also be connected to other replicas in a cascading-like structure. All the sub-replicas will receive exactly the same replication stream from the master. * Valkey replication is non-blocking on the master side. This means that the master will continue to handle queries when one or more replicas perform the initial synchronization or a partial resynchronization. -* Replication is also largely non-blocking on the replica side. While the replica is performing the initial synchronization, it can handle queries using the old version of the dataset, assuming you configured Valkey to do so in valkey.conf. Otherwise, you can configure Valkey replicas to return an error to clients if the replication stream is down. However, after the initial sync, the old dataset must be deleted and the new one must be loaded. The replica will block incoming connections during this brief window (that can be as long as many seconds for very large datasets). Since Redis OSS 4.0 you can configure Valkey so that the deletion of the old data set happens in a different thread, however loading the new initial dataset will still happen in the main thread and block the replica. +* Replication is also largely non-blocking on the replica side. While the replica is performing the initial synchronization, it can handle queries using the old version of the dataset, assuming you configured Valkey to do so in valkey.conf. Otherwise, you can configure Valkey replicas to return an error to clients if the replication stream is down. However, after the initial sync, the old dataset must be deleted and the new one must be loaded. The replica will block incoming connections during this brief window (that can be as long as many seconds for very large datasets). You can configure Valkey so that the deletion of the old data set happens in a different thread, however loading the new initial dataset will still happen in the main thread and block the replica. * Replication can be used both for scalability, to have multiple replicas for read-only queries (for example, slow O(N) operations can be offloaded to replicas), or simply for improving data safety and high availability. * You can use replication to avoid the cost of having the master writing the full dataset to disk: a typical technique involves configuring your master `valkey.conf` to avoid persisting to disk at all, then connect a replica configured to save from time to time, or with AOF enabled. However, this setup must be handled with care, since a restarting master will start with an empty dataset: if the replica tries to sync with it, the replica will be emptied as well. From 5fc2cd0ca93854a8cc3108467ad5d508899a32de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 02:25:11 +0200 Subject: [PATCH 12/19] pubsub.md, delete link to most likely broken example, #128 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/pubsub.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/topics/pubsub.md b/topics/pubsub.md index e101507e..5a0b6367 100644 --- a/topics/pubsub.md +++ b/topics/pubsub.md @@ -185,10 +185,6 @@ It restricts the propagation of messages to be within the shard of a cluster. Hence, the amount of data passing through the cluster bus is limited in comparison to global Pub/Sub where each message propagates to each node in the cluster. This allows users to horizontally scale the Pub/Sub usage by adding more shards. -## Programming example - -Pieter Noordhuis provided a great example using EventMachine and Valkey to create [a multi user high performance web chat](https://gist.github.com/pietern/348262). - ## Client library implementation hints Because all the messages received contain the original subscription causing the message delivery (the channel in the case of message type, and the original pattern in the case of pmessage type) client libraries may bind the original subscription to callbacks (that can be anonymous functions, blocks, function pointers), using a hash table. From b6d78647619c31e91e69931d578da257973439b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 03:39:04 +0200 Subject: [PATCH 13/19] Fix links to sections in protocol.md and delete manually created anchors, #127 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor the sections about various kinds of nulls. Signed-off-by: Viktor Söderqvist --- commands/command-count.md | 2 +- topics/eval-intro.md | 2 +- topics/lua-api.md | 62 ++++++++++----------- topics/protocol.md | 110 +++++++++++++++----------------------- topics/pubsub.md | 2 +- topics/transactions.md | 2 +- 6 files changed, 79 insertions(+), 101 deletions(-) diff --git a/commands/command-count.md b/commands/command-count.md index 3e81efe2..bf90aa26 100644 --- a/commands/command-count.md +++ b/commands/command-count.md @@ -1,4 +1,4 @@ -Returns @integer-reply of number of total commands in this Valkey server. +Returns the total number of commands in this Valkey server. ## Examples diff --git a/topics/eval-intro.md b/topics/eval-intro.md index e18c33aa..b191580f 100644 --- a/topics/eval-intro.md +++ b/topics/eval-intro.md @@ -104,7 +104,7 @@ The following attempts to demonstrate the distribution of input arguments betwee ``` **Note:** -as can been seen above, Lua's table arrays are returned as [RESP2 array replies](protocol.md#resp-arrays), so it is likely that your client's library will convert it to the native array data type in your programming language. +as can been seen above, Lua's table arrays are returned as [RESP2 array replies](protocol.md#arrays), so it is likely that your client's library will convert it to the native array data type in your programming language. Please refer to the rules that govern [data type conversion](lua-api.md#data-type-conversion) for more pertinent information. ## Interacting with Valkey from a script diff --git a/topics/lua-api.md b/topics/lua-api.md index 948745b2..d75af62b 100644 --- a/topics/lua-api.md +++ b/topics/lua-api.md @@ -178,7 +178,7 @@ Evaluating this script with more than one argument will return: * Available in scripts: yes * Available in functions: yes -This is a helper function that returns an [error reply](protocol.md#resp-errors). +This is a helper function that returns an [error reply](protocol.md#simply-errors). The helper accepts a single string argument and returns a Lua table with the _err_ field set to that string. The outcome of the following code is that _error1_ and _error2_ are identical for all intents and purposes: @@ -211,7 +211,7 @@ Scripts are advised to follow this convention, as shown in the example above, bu * Available in scripts: yes * Available in functions: yes -This is a helper function that returns a [simple string reply](protocol.md#resp-simple-strings). +This is a helper function that returns a [simple string reply](protocol.md#simple-strings). "OK" is an example of a standard Valkey status reply. The Lua API represents status replies as tables with a single field, _ok_, set with a simple status string. @@ -547,27 +547,27 @@ The following sections describe the type conversion rules between Lua and Valkey The following type conversion rules apply to the execution's context by default as well as after calling `server.setresp(2)`: -* [RESP2 integer reply](protocol.md#resp-integers) -> Lua number -* [RESP2 bulk string reply](protocol.md#resp-bulk-strings) -> Lua string -* [RESP2 array reply](protocol.md#resp-arrays) -> Lua table (may have other Valkey data types nested) -* [RESP2 status reply](protocol.md#resp-simple-strings) -> Lua table with a single _ok_ field containing the status string -* [RESP2 error reply](protocol.md#resp-errors) -> Lua table with a single _err_ field containing the error string -* [RESP2 null bulk reply](protocol.md#null-elements-in-arrays) and [null multi bulk reply](protocol.md#resp-arrays) -> Lua false boolean type +* [RESP2 integer reply](protocol.md#integers) -> Lua number +* [RESP2 bulk string reply](protocol.md#bulk-strings) -> Lua string +* [RESP2 array reply](protocol.md#arrays) -> Lua table (may have other Valkey data types nested) +* [RESP2 status reply](protocol.md#simple-strings) -> Lua table with a single _ok_ field containing the status string +* [RESP2 error reply](protocol.md#simple-errors) -> Lua table with a single _err_ field containing the error string +* [RESP2 null bulk reply and null multi bulk reply](protocol.md#nulls) -> Lua false boolean type ## Lua to RESP2 type conversion The following type conversion rules apply by default as well as after the user had called `HELLO 2`: -* Lua number -> [RESP2 integer reply](protocol.md#resp-integers) (the number is converted into an integer) -* Lua string -> [RESP bulk string reply](protocol.md#resp-bulk-strings) -* Lua table (indexed, non-associative array) -> [RESP2 array reply](protocol.md#resp-arrays) (truncated at the first Lua `nil` value encountered in the table, if any) -* Lua table with a single _ok_ field -> [RESP2 status reply](protocol.md#resp-simple-strings) -* Lua table with a single _err_ field -> [RESP2 error reply](protocol.md#resp-errors) -* Lua boolean false -> [RESP2 null bulk reply](protocol.md#null-elements-in-arrays) +* Lua number -> [RESP2 integer reply](protocol.md#integers) (the number is converted into an integer) +* Lua string -> [RESP bulk string reply](protocol.md#bulk-strings) +* Lua table (indexed, non-associative array) -> [RESP2 array reply](protocol.md#arrays) (truncated at the first Lua `nil` value encountered in the table, if any) +* Lua table with a single _ok_ field -> [RESP2 status reply](protocol.md#simple-strings) +* Lua table with a single _err_ field -> [RESP2 error reply](protocol.md#simple-errors) +* Lua boolean false -> [RESP2 null bulk reply](protocol.md#nulls) There is an additional Lua-to-Valkey conversion rule that has no corresponding Valkey-to-Lua conversion rule: -* Lua Boolean `true` -> [RESP2 integer reply](protocol.md#resp-integers) with value of 1. +* Lua Boolean `true` -> [RESP2 integer reply](protocol.md#integers) with value of 1. There are three additional rules to note about converting Lua to Valkey data types: @@ -613,24 +613,24 @@ As you can see, the float value of _3.333_ gets converted to an integer _3_, the ### RESP3 to Lua type conversion -[RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md) is a newer version of the [Valkey Serialization Protocol](protocol.md). -It is available as an opt-in choice as of Redis OSS v6.0. +RESP3 is a newer version of Valkey's [Serialization Protocol](protocol.md). +It is available as an opt-in choice. An executing script may call the [`server.setresp`](#server.setresp) function during its execution and switch the protocol version that's used for returning replies from Valkey' commands (that can be invoked via [`server.call()`](#server.call) or [`server.pcall()`](#server.pcall)). Once Valkey' replies are in RESP3 protocol, all of the [RESP2 to Lua conversion](#resp2-to-lua-type-conversion) rules apply, with the following additions: -* [RESP3 map reply](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#map-type) -> Lua table with a single _map_ field containing a Lua table representing the fields and values of the map. -* [RESP set reply](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#set-reply) -> Lua table with a single _set_ field containing a Lua table representing the elements of the set as fields, each with the Lua Boolean value of `true`. -* [RESP3 null](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#null-reply) -> Lua `nil`. -* [RESP3 true reply](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#boolean-reply) -> Lua true boolean value. -* [RESP3 false reply](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#boolean-reply) -> Lua false boolean value. -* [RESP3 double reply](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#double-type) -> Lua table with a single _double_ field containing a Lua number representing the double value. -* [RESP3 big number reply](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#big-number-type) -> Lua table with a single _big_number_ field containing a Lua string representing the big number value. -* [Valkey verbatim string reply](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#verbatim-string-type) -> Lua table with a single _verbatim_string_ field containing a Lua table with two fields, _string_ and _format_, representing the verbatim string and its format, respectively. +* [Map reply](protocol.md#maps) -> Lua table with a single _map_ field containing a Lua table representing the fields and values of the map. +* [Set reply](protocol.md#sets) -> Lua table with a single _set_ field containing a Lua table representing the elements of the set as fields, each with the Lua Boolean value of `true`. +* [Null](protocol.md#nulls) -> Lua `nil`. +* [True reply](protocol.md#booleans) -> Lua true boolean value. +* [False reply](protocol.md#booleans) -> Lua false boolean value. +* [Double reply](protocol.md#doubles) -> Lua table with a single _double_ field containing a Lua number representing the double value. +* [Big number reply](protocol.md#big-numbers) -> Lua table with a single _big_number_ field containing a Lua string representing the big number value. +* [Verbatim string reply](protocol.md#verbatim-strings) -> Lua table with a single _verbatim_string_ field containing a Lua table with two fields, _string_ and _format_, representing the verbatim string and its format, respectively. **Note:** -the RESP3 [big number](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#big-number-type) and [verbatim strings](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#verbatim-string-type) replies are only supported as of Redis OSS v7.0 and greater. +the RESP3 [big number](protocol.md#big-numbers) and [verbatim strings](protocol.md#verbatim-strings) replies are supported since Redis OSS 7.0. Also, presently, RESP3's [attributes](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#attribute-type), [streamed strings](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#streamed-strings) and [streamed aggregate data types](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#streamed-aggregate-data-types) are not supported by the Valkey Lua API. ### Lua to RESP3 type conversion @@ -638,11 +638,11 @@ Also, presently, RESP3's [attributes](https://github.com/redis/redis-specificati Regardless of the script's choice of protocol version set for replies with the [`server.setresp()` function] when it calls `server.call()` or `server.pcall()`, the user may opt-in to using RESP3 (with the `HELLO 3` command) for the connection. Although the default protocol for incoming client connections is RESP2, the script should honor the user's preference and return adequately-typed RESP3 replies, so the following rules apply on top of those specified in the [Lua to RESP2 type conversion](#lua-to-resp2-type-conversion) section when that is the case. -* Lua Boolean -> [RESP3 Boolean reply](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#boolean-reply) (note that this is a change compared to the RESP2, in which returning a Boolean Lua `true` returned the number 1 to the Valkey client, and returning a `false` used to return a `null`. -* Lua table with a single _map_ field set to an associative Lua table -> [RESP3 map reply](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#map-type). -* Lua table with a single _set_ field set to an associative Lua table -> [RESP3 set reply](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#set-type). Values can be set to anything and are discarded anyway. -* Lua table with a single _double_ field to an associative Lua table -> [RESP3 double reply](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#double-type). -* Lua nil -> [RESP3 null](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#null-reply). +* Lua Boolean -> [RESP3 Boolean reply](protocol.md#booleans) (note that this is a change compared to the RESP2, in which returning a Boolean Lua `true` returned the number 1 to the Valkey client, and returning a `false` used to return a `null`. +* Lua table with a single _map_ field set to an associative Lua table -> [RESP3 map reply](protocol.md#maps). +* Lua table with a single _set_ field set to an associative Lua table -> [RESP3 set reply](protocol.md#sets). Values can be set to anything and are discarded anyway. +* Lua table with a single _double_ field to an associative Lua table -> [RESP3 double reply](protocol.md#doubles). +* Lua nil -> [RESP3 null](protocol.md#nulls). However, if the connection is set use the RESP2 protocol, and even if the script replies with RESP3-typed responses, Valkey will automatically perform a RESP3 to RESP2 conversion of the reply as is the case for regular commands. That means, for example, that returning the RESP3 map type to a RESP2 connection will result in the reply being converted to a flat RESP2 array that consists of alternating field names and their values, rather than a RESP3 map. diff --git a/topics/protocol.md b/topics/protocol.md index 17c37642..c92611d3 100644 --- a/topics/protocol.md +++ b/topics/protocol.md @@ -1,11 +1,11 @@ --- -title: "Valkey serialization protocol specification" +title: "Serialization protocol specification" linkTitle: "Protocol spec" -description: Valkey serialization protocol (RESP) is the wire protocol that clients implement ---- +description: Valkey's serialization protocol (RESP) is the wire protocol that clients implement +---- To communicate with the Valkey server, Valkey clients use a protocol called REdis Serialization Protocol (RESP). -While the protocol was designed specifically for Valkey, you can use it for other client-server software projects. +While the protocol was designed for Redis, it's used by many other client-server software projects. RESP is a compromise among the following considerations: @@ -28,10 +28,10 @@ The protocol outlined here is used only for client-server communication. [Valkey Cluster](cluster-spec.md) uses a different binary protocol for exchanging messages between nodes. ## RESP versions -Support for the first version of the RESP protocol was introduced in Redis OSS 1.2. -Using RESP with Redis OSS 1.2 was optional and had mainly served the purpose of working the kinks out of the protocol. -In Redis OSS 2.0, the protocol's next version, a.k.a RESP2, became the standard communication method for clients with the Valkey server. +The first version of the RESP protocol was experimental and was never widely used. + +The next version, RESP2, early became the standard communication method for clients with Redis OSS. [RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md) is a superset of RESP2 that mainly aims to make a client author's life a little bit easier. Redis OSS 6.0 introduced experimental opt-in support of RESP3's features (excluding streaming strings and streaming aggregates). @@ -64,10 +64,14 @@ This is the simplest model possible; however, there are some exceptions: * [Protected mode](security.md#protected-mode). Connections opened from a non-loopback address to a Valkey while in protected mode are denied and terminated by the server. Before terminating the connection, Valkey unconditionally sends a `-DENIED` reply, regardless of whether the client writes to the socket. -* The [RESP3 Push type](#resp3-pushes). +* The [RESP3 Push type](#pushes). As the name suggests, a push type allows the server to send out-of-band data to the connection. The server may push data at any time, and the data isn't necessarily related to specific commands executed by the client. - +* When RESP3 is used, the commands `SUBSCRIBE`, `UNSUBSCRIBE` and their pattern and sharded variants, + return either an error reply *or one or more Push replies, without any regular in-band reply*. + This is considered a design mistake of these commands but the behaviour is kept for backward compatibility. + Clients need to compensate for this behaviour. + Excluding these exceptions, the Valkey protocol is a simple request-response protocol. ## RESP protocol description @@ -121,8 +125,6 @@ The following table summarizes the RESP data types that Valkey supports: | [Sets](#sets) | RESP3 | Aggregate | `~` | | [Pushes](#pushes) | RESP3 | Aggregate | `>` | - - ### Simple strings Simple strings are encoded as a plus (`+`) character, followed by a string. The string mustn't contain a CR (`\r`) or LF (`\n`) character and is terminated by CRLF (i.e., `\r\n`). @@ -137,8 +139,6 @@ When Valkey replies with a simple string, a client library should return to the To send binary strings, use [bulk strings](#bulk-strings) instead. - - ### Simple errors RESP has specific data types for errors. Simple errors, or simply just errors, are similar to [simple strings](#simple-strings), but their first character is the minus (`-`) character. @@ -168,8 +168,6 @@ A client implementation can return different types of exceptions for various err However, such a feature should not be considered vital as it is rarely useful. Also, simpler client implementations can return a generic error value, such as `false`. - - ### Integers This type is a CRLF-terminated string that represents a signed, base-10, 64-bit integer. @@ -194,8 +192,6 @@ For instance, `SISMEMBER` returns 1 for true and 0 for false. Other commands, including `SADD`, `SREM`, and `SETNX`, return 1 when the data changes and 0 otherwise. - - ### Bulk strings A bulk string represents a single binary string. The string can be of any size, but by default, Valkey limits it to 512 MB (see the `proto-max-bulk-len` configuration directive). @@ -220,22 +216,6 @@ The empty string's encoding is: -#### Null bulk strings -Whereas RESP3 has a dedicated data type for [null values](#nulls), RESP2 has no such type. -Instead, due to historical reasons, the representation of null values in RESP2 is via predetermined forms of the [bulk strings](#bulk-strings) and [arrays](#arrays) types. - -The null bulk string represents a non-existing value. -The `GET` command returns the Null Bulk String when the target key doesn't exist. - -It is encoded as a bulk string with the length of negative one (-1), like so: - - $-1\r\n - -A Valkey client should return a nil object when the server replies with a null bulk string rather than the empty string. -For example, a Ruby library should return `nil` while a C library should return `NULL` (or set a special flag in the reply object). - - - ### Arrays Clients send commands to the Valkey server as RESP arrays. Similarly, some Valkey commands that return collections of elements use arrays as their replies. @@ -302,7 +282,33 @@ The second element is another array containing a simple string and an error. In some places, the RESP Array type may be referred to as _multi bulk_. The two are the same. - +### Nulls +The null data type represents non-existent values. + +In RESP3, null is encoded using the underscore (`_`) character, followed by the CRLF terminator (`\r\n`). +Here's null's raw RESP encoding: + + _\r\n + +RESP2 features two specially crafted values for representing null values, +known as "null bulk strings" and "null arrays". +This duality has always been a redundancy that added zero semantical value to the protocol itself. +The null type, introduced in RESP3, aims to fix this wrong. +Clients should handle all these representations of null in the same way. +For example, a Ruby library should return `nil` while a C library should return `NULL` (or set a special flag in the reply object). + +#### Null bulk strings +Whereas RESP3 has a dedicated data type for [null values](#nulls), RESP2 has no such type. +Instead, due to historical reasons, the representation of null values in RESP2 is via predetermined forms of the [bulk strings](#bulk-strings) and [arrays](#arrays) types. + +The null bulk string represents a non-existing value. +The `GET` command returns the Null Bulk String when the target key doesn't exist. + +It is encoded as a bulk string with the length of negative one (-1), like so: + + $-1\r\n + +A Valkey client should return a nil object when the server replies with a null bulk string rather than the empty string. #### Null arrays Whereas RESP3 has a dedicated data type for [null values](#nulls), RESP2 has no such type. Instead, due to historical reasons, the representation of null values in RESP2 is via predetermined forms of the [Bulk Strings](#bulk-strings) and [arrays](#arrays) types. @@ -310,19 +316,18 @@ Whereas RESP3 has a dedicated data type for [null values](#nulls), RESP2 has no Null arrays exist as an alternative way of representing a null value. For instance, when the `BLPOP` command times out, it returns a null array. -The encoding of a null array is that of an array with the length of -1, i.e.: +The encoding of a null array is that of an array with the length of -1, i.e. *-1\r\n When Valkey replies with a null array, the client should return a null object rather than an empty array. -This is necessary to distinguish between an empty list and a different condition (for instance, the timeout condition of the `BLPOP` command). #### Null elements in arrays -Single elements of an array may be [null bulk string](#null-bulk-strings). +Single elements of an array may be [null](#nulls). This is used in Valkey replies to signal that these elements are missing and not empty strings. This can happen, for example, with the `SORT` command when used with the `GET pattern` option if the specified key is missing. -Here's an example of an array reply containing a null element: +Here's an example of an array reply containing a null element, represented as a RESP2 null bulk string: *3\r\n $5\r\n @@ -336,23 +341,6 @@ The client library should return to its caller something like this: ["hello",nil,"world"] - - -### Nulls -The null data type represents non-existent values. - -Nulls' encoding is the underscore (`_`) character, followed by the CRLF terminator (`\r\n`). -Here's Null's raw RESP encoding: - - _\r\n - -**Note:** -Due to historical reasons, RESP2 features two specially crafted values for representing null values of bulk strings and arrays. -This duality has always been a redundancy that added zero semantical value to the protocol itself. -The null type, introduced in RESP3, aims to fix this wrong. - - - ### Booleans RESP booleans are encoded as follows: @@ -362,8 +350,6 @@ RESP booleans are encoded as follows: * A `t` character for true values, or an `f` character for false ones. * The CRLF terminator. - - ### Doubles The Double RESP type encodes a double-precision floating point value. Doubles are encoded as follows: @@ -416,8 +402,6 @@ Big numbers can be positive or negative but can't include fractionals. Client libraries written in languages with a big number type should return a big number. When big numbers aren't supported, the client should return a string and, when possible, signal to the caller that the reply is a big integer (depending on the API used by the client library). - - ### Bulk errors This type combines the purpose of [simple errors](#simple-errors) with the expressive power of [bulk strings](#bulk-strings). @@ -440,8 +424,6 @@ For instance, the error "SYNTAX invalid syntax" is represented by the following (The raw RESP encoding is split into multiple lines for readability). - - ### Verbatim strings This type is similar to the [bulk string](#bulk-strings), with the addition of providing a hint about the data's encoding. @@ -471,8 +453,6 @@ For example, the Valkey command `INFO` outputs a report that includes newlines. When using RESP3, `valkey-cli` displays it correctly because it is sent as a Verbatim String reply (with its three bytes being "txt"). When using RESP2, however, the `valkey-cli` is hard-coded to look for the `INFO` command to ensure its correct display to the user. - - ### Maps The RESP map encodes a collection of key-value tuples, i.e., a dictionary or a hash. @@ -513,8 +493,6 @@ A map in RESP2 is represented by a flat array containing the keys and the values The first element is a key, followed by the corresponding value, then the next key and so on, like this: `key1, value1, key2, value2, ...`. - - ### Sets Sets are somewhat like [Arrays](#arrays) but are unordered and should only contain unique elements. @@ -569,7 +547,7 @@ If we specify a connection version that is too big and unsupported by the server Client: HELLO 4 Server: -NOPROTO sorry, this protocol version is not supported. - + At that point, the client may retry with a lower protocol version. Similarly, the client can easily detect a server that is only able to speak RESP2: diff --git a/topics/pubsub.md b/topics/pubsub.md index 5a0b6367..79c17567 100644 --- a/topics/pubsub.md +++ b/topics/pubsub.md @@ -50,7 +50,7 @@ Messages in streams are persisted, and support both _at-most-once_ as well as _a ## Format of pushed messages -A message is an [array-reply](protocol.md#array-reply) with three elements. +A message is an [array-reply](protocol.md#arrays) with three elements. The first element is the kind of message: diff --git a/topics/transactions.md b/topics/transactions.md index 1eef95c5..b369f48f 100644 --- a/topics/transactions.md +++ b/topics/transactions.md @@ -104,7 +104,7 @@ EXEC -WRONGTYPE Operation against a key holding the wrong kind of value ``` -`EXEC` returned two-element [bulk string reply](protocol.md#bulk-string-reply) where one is an `OK` code and +`EXEC` returned two-element [bulk string reply](protocol.md#bulk-strings) where one is an `OK` code and the other an error reply. It's up to the client library to find a sensible way to provide the error to the user. From a699d5a03e57b76c16dcaf47894846d7bf71e878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 03:49:06 +0200 Subject: [PATCH 14/19] programmability.md, #126 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/programmability.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/topics/programmability.md b/topics/programmability.md index 984cdafb..cfb3524e 100644 --- a/topics/programmability.md +++ b/topics/programmability.md @@ -5,11 +5,13 @@ description: > Extending Valkey with Lua and Valkey Functions --- -Valkey provides a programming interface that lets you execute custom scripts on the server itself. In Redis OSS 7 and beyond, you can use [Valkey Functions](functions-intro.md) to manage and run your scripts. In Redis OSS 6.2 and below, you use [Lua scripting with the EVAL command](eval-intro.md) to program the server. +Valkey provides a programming interface that lets you execute custom scripts on the server itself. +You can use [Functions](functions-intro.md) to create, manage and run scripts. +You can also use [Lua scripting with the EVAL command](eval-intro.md) to program the server. ## Background -Valkey is, by [definition](https://github.com/valkey-io/valkey/blob/unstable/MANIFESTO#L7), a _"domain-specific language for abstract data types"_. +Valkey is a _"domain-specific language for abstract data types"_. The language that Valkey speaks consists of its [commands](../commands/). Most the commands specialize at manipulating core [data types](data-types.md) in different ways. In many cases, these commands provide all the functionality that a developer requires for managing application data in Valkey. @@ -30,7 +32,7 @@ Please refer to the [Valkey Lua API Reference](lua-api.md) page for complete doc Valkey provides two means for running scripts. -Firstly, and ever since Redis OSS 2.6.0, the `EVAL` command enables running server-side scripts. +Firstly, the `EVAL` command enables running server-side scripts. Eval scripts provide a quick and straightforward way to have Valkey run your scripts ad-hoc. However, using them means that the scripted logic is a part of your application (not an extension of the Valkey server). Every applicative instance that runs a script must have the script's source code readily available for loading at any time. @@ -64,7 +66,7 @@ They have the following properties: * They can always be executed on replicas. * They can always be killed by the `SCRIPT KILL` command. -* They never fail with OOM error when redis is over the memory limit. +* They never fail with OOM error when Valkey is over the memory limit. * They are not blocked during write pauses, such as those that occur during coordinated failovers. * They cannot execute any command that may modify the data set. * Currently `PUBLISH`, `SPUBLISH` and `PFCOUNT` are also considered write commands in scripts, because they could attempt to propagate commands to replicas and AOF file. From 5847fdb815ec144d87aa91ab4af002d9c9e6becd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 18:32:57 +0200 Subject: [PATCH 15/19] pipelining.md, #125 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/pipelining.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/topics/pipelining.md b/topics/pipelining.md index 50efbd51..04d01f99 100644 --- a/topics/pipelining.md +++ b/topics/pipelining.md @@ -94,7 +94,7 @@ reaches 10 times the baseline obtained without pipelining, as shown in this figu ## A real world code example -In the following benchmark we'll use the Valkey Ruby client, supporting pipelining, to test the speed improvement due to pipelining: +In the following benchmark we'll use a Ruby client, supporting pipelining, to test the speed improvement due to pipelining: ```ruby require 'rubygems' @@ -130,7 +130,7 @@ bench('with pipelining') do end ``` -Running the above simple script yields the following figures on my Mac OS X system, running over the loopback interface, where pipelining will provide the smallest improvement as the RTT is already pretty low: +Running the above simple script yields the following figures on my MacOS system, running over the loopback interface, where pipelining will provide the smallest improvement as the RTT is already pretty low: ``` without pipelining 1.185238 seconds From 4257377ca948d5ee076f527a26cc78b61d5a7f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 18:35:42 +0200 Subject: [PATCH 16/19] performance-on-cpu.md remove refs to redis, #124 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/performance-on-cpu.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/topics/performance-on-cpu.md b/topics/performance-on-cpu.md index b3915f14..1e2f19c4 100644 --- a/topics/performance-on-cpu.md +++ b/topics/performance-on-cpu.md @@ -53,7 +53,7 @@ It's important that you ensure that: You can do it as follows within redis main repo: - $ make REDIS_CFLAGS="-g -fno-omit-frame-pointer" + $ make SERVER_CFLAGS="-g -fno-omit-frame-pointer" ## A set of instruments to identify performance regressions and/or potential **on-CPU performance** improvements @@ -119,9 +119,9 @@ Specifically, for perf we need to convert the generated perf.data into the captured stacks, and fold each of them into single lines. You can then render the on-CPU flame graph with: - $ perf script > redis.perf.stacks - $ stackcollapse-perf.pl redis.perf.stacks > redis.folded.stacks - $ flamegraph.pl redis.folded.stacks > redis.svg + $ perf script > valkey.perf.stacks + $ stackcollapse-perf.pl valkey.perf.stacks > valkey.folded.stacks + $ flamegraph.pl valkey.folded.stacks > valkey.svg By default, perf script will generate a perf.data file in the current working directory. See the [perf script](https://linux.die.net/man/1/perf-script) @@ -158,12 +158,12 @@ removed the perf.data and intermediate steps if stack traces analysis is our main goal. You can use bcc's profile tool to output folded format directly, for flame graph generation: - $ /usr/share/bcc/tools/profile -F 999 -f --pid $(pgrep valkey-server) --duration 60 > redis.folded.stacks + $ /usr/share/bcc/tools/profile -F 999 -f --pid $(pgrep valkey-server) --duration 60 > valkey.folded.stacks In that manner, we've remove any preprocessing and can render the on-CPU flame graph with a single command: - $ flamegraph.pl redis.folded.stacks > redis.svg + $ flamegraph.pl valkey.folded.stacks > valkey.svg ### Visualizing the recorded profile information using Flame Graphs @@ -203,7 +203,7 @@ your CPU supported counters and features by using `perf list`. To calculate the number of instructions per cycle, the number of micro ops executed, the number of cycles during which no micro ops were dispatched, the number stalled cycles on memory, including a per memory type stalls, for the -duration of 60s, specifically for redis process: +duration of 60s, specifically for the valkey-server process: $ perf stat -e "cpu-clock,cpu-cycles,instructions,uops_executed.core,uops_executed.stall_cycles,cache-references,cache-misses,cycle_activity.stalls_total,cycle_activity.stalls_mem_any,cycle_activity.stalls_l3_miss,cycle_activity.stalls_l2_miss,cycle_activity.stalls_l1d_miss" --pid $(pgrep valkey-server) -- sleep 60 From c9be7e12498615bb87062ae26b7acf014377cf9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 18:40:59 +0200 Subject: [PATCH 17/19] modules-native-types.md syntax-highlight code, remove WIP, #122 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/modules-native-types.md | 215 ++++++++++++++++++--------------- 1 file changed, 120 insertions(+), 95 deletions(-) diff --git a/topics/modules-native-types.md b/topics/modules-native-types.md index cb184067..b925113e 100644 --- a/topics/modules-native-types.md +++ b/topics/modules-native-types.md @@ -47,22 +47,24 @@ to declare a global variable that will hold a reference to the data type. The API to register the data type will return a data type reference that will be stored in the global variable. - static ValkeyModuleType *MyType; - #define MYTYPE_ENCODING_VERSION 0 - - int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx) { - ValkeyModuleTypeMethods tm = { - .version = VALKEYMODULE_TYPE_METHOD_VERSION, - .rdb_load = MyTypeRDBLoad, - .rdb_save = MyTypeRDBSave, - .aof_rewrite = MyTypeAOFRewrite, - .free = MyTypeFree - }; - - MyType = ValkeyModule_CreateDataType(ctx, "MyType-AZ", - MYTYPE_ENCODING_VERSION, &tm); - if (MyType == NULL) return VALKEYMODULE_ERR; - } +```C +static ValkeyModuleType *MyType; +#define MYTYPE_ENCODING_VERSION 0 + +int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx) { +ValkeyModuleTypeMethods tm = { + .version = VALKEYMODULE_TYPE_METHOD_VERSION, + .rdb_load = MyTypeRDBLoad, + .rdb_save = MyTypeRDBSave, + .aof_rewrite = MyTypeAOFRewrite, + .free = MyTypeFree +}; + + MyType = ValkeyModule_CreateDataType(ctx, "MyType-AZ", + MYTYPE_ENCODING_VERSION, &tm); + if (MyType == NULL) return VALKEYMODULE_ERR; +} +``` As you can see from the example above, a single API call is needed in order to register the new type. However a number of function pointers are passed as @@ -108,12 +110,14 @@ The last argument is a structure used in order to pass the type methods to the registration function: `rdb_load`, `rdb_save`, `aof_rewrite`, `digest` and `free` and `mem_usage` are all callbacks with the following prototypes and uses: - typedef void *(*ValkeyModuleTypeLoadFunc)(ValkeyModuleIO *rdb, int encver); - typedef void (*ValkeyModuleTypeSaveFunc)(ValkeyModuleIO *rdb, void *value); - typedef void (*ValkeyModuleTypeRewriteFunc)(ValkeyModuleIO *aof, ValkeyModuleString *key, void *value); - typedef size_t (*ValkeyModuleTypeMemUsageFunc)(void *value); - typedef void (*ValkeyModuleTypeDigestFunc)(ValkeyModuleDigest *digest, void *value); - typedef void (*ValkeyModuleTypeFreeFunc)(void *value); +```C +typedef void *(*ValkeyModuleTypeLoadFunc)(ValkeyModuleIO *rdb, int encver); +typedef void (*ValkeyModuleTypeSaveFunc)(ValkeyModuleIO *rdb, void *value); +typedef void (*ValkeyModuleTypeRewriteFunc)(ValkeyModuleIO *aof, ValkeyModuleString *key, void *value); +typedef size_t (*ValkeyModuleTypeMemUsageFunc)(void *value); +typedef void (*ValkeyModuleTypeDigestFunc)(ValkeyModuleDigest *digest, void *value); +typedef void (*ValkeyModuleTypeFreeFunc)(void *value); +``` * `rdb_load` is called when loading data from the RDB file. It loads data in the same format as `rdb_save` produces. * `rdb_save` is called when saving data to the RDB file. @@ -189,9 +193,11 @@ The API uses the normal modules `ValkeyModule_OpenKey()` low level key access interface in order to deal with this. This is an example of setting a native type private data structure to a Valkey key: - ValkeyModuleKey *key = ValkeyModule_OpenKey(ctx,keyname,VALKEYMODULE_WRITE); - struct some_private_struct *data = createMyDataStructure(); - ValkeyModule_ModuleTypeSetValue(key,MyType,data); +```C +ValkeyModuleKey *key = ValkeyModule_OpenKey(ctx,keyname,VALKEYMODULE_WRITE); +struct some_private_struct *data = createMyDataStructure(); +ValkeyModule_ModuleTypeSetValue(key,MyType,data); +``` The function `ValkeyModule_ModuleTypeSetValue()` is used with a key handle open for writing, and gets three arguments: the key handle, the reference to the @@ -204,43 +210,51 @@ to perform operations on the type. Similarly we can retrieve the private data from a key using this function: - struct some_private_struct *data; - data = ValkeyModule_ModuleTypeGetValue(key); +``` +struct some_private_struct *data; +data = ValkeyModule_ModuleTypeGetValue(key); +``` We can also test for a key to have our native type as value: - if (ValkeyModule_ModuleTypeGetType(key) == MyType) { - /* ... do something ... */ - } +```C +if (ValkeyModule_ModuleTypeGetType(key) == MyType) { + /* ... do something ... */ +} +``` However for the calls to do the right thing, we need to check if the key is empty, if it contains a value of the right kind, and so forth. So the idiomatic code to implement a command writing to our native type is along these lines: - ValkeyModuleKey *key = ValkeyModule_OpenKey(ctx,argv[1], - VALKEYMODULE_READ|VALKEYMODULE_WRITE); - int type = ValkeyModule_KeyType(key); - if (type != VALKEYMODULE_KEYTYPE_EMPTY && - ValkeyModule_ModuleTypeGetType(key) != MyType) - { - return ValkeyModule_ReplyWithError(ctx,VALKEYMODULE_ERRORMSG_WRONGTYPE); - } +```C +ValkeyModuleKey *key = ValkeyModule_OpenKey(ctx,argv[1], + VALKEYMODULE_READ|VALKEYMODULE_WRITE); +int type = ValkeyModule_KeyType(key); +if (type != VALKEYMODULE_KEYTYPE_EMPTY && + ValkeyModule_ModuleTypeGetType(key) != MyType) +{ + return ValkeyModule_ReplyWithError(ctx,VALKEYMODULE_ERRORMSG_WRONGTYPE); +} +``` Then if we successfully verified the key is not of the wrong type, and we are going to write to it, we usually want to create a new data structure if the key is empty, or retrieve the reference to the value associated to the key if there is already one: - /* Create an empty value object if the key is currently empty. */ - struct some_private_struct *data; - if (type == VALKEYMODULE_KEYTYPE_EMPTY) { - data = createMyDataStructure(); - ValkeyModule_ModuleTypeSetValue(key,MyTyke,data); - } else { - data = ValkeyModule_ModuleTypeGetValue(key); - } - /* Do something with 'data'... */ +```C +/* Create an empty value object if the key is currently empty. */ +struct some_private_struct *data; +if (type == VALKEYMODULE_KEYTYPE_EMPTY) { + data = createMyDataStructure(); + ValkeyModule_ModuleTypeSetValue(key,MyTyke,data); +} else { + data = ValkeyModule_ModuleTypeGetValue(key); +} +/* Do something with 'data'... */ +``` Free method --- @@ -249,14 +263,18 @@ As already mentioned, when Valkey needs to free a key holding a native type value, it needs help from the module in order to release the memory. This is the reason why we pass a `free` callback during the type registration: - typedef void (*ValkeyModuleTypeFreeFunc)(void *value); +```C +typedef void (*ValkeyModuleTypeFreeFunc)(void *value); +``` A trivial implementation of the free method can be something like this, assuming our data structure is composed of a single allocation: - void MyTypeFreeCallback(void *value) { - ValkeyModule_Free(value); - } +```C +void MyTypeFreeCallback(void *value) { + ValkeyModule_Free(value); +} +``` However a more real world one will call some function that performs a more complex memory reclaiming, by casting the void pointer to some structure @@ -282,16 +300,18 @@ have to care those details yourself. This is the list of functions performing RDB saving and loading: - void ValkeyModule_SaveUnsigned(ValkeyModuleIO *io, uint64_t value); - uint64_t ValkeyModule_LoadUnsigned(ValkeyModuleIO *io); - void ValkeyModule_SaveSigned(ValkeyModuleIO *io, int64_t value); - int64_t ValkeyModule_LoadSigned(ValkeyModuleIO *io); - void ValkeyModule_SaveString(ValkeyModuleIO *io, ValkeyModuleString *s); - void ValkeyModule_SaveStringBuffer(ValkeyModuleIO *io, const char *str, size_t len); - ValkeyModuleString *ValkeyModule_LoadString(ValkeyModuleIO *io); - char *ValkeyModule_LoadStringBuffer(ValkeyModuleIO *io, size_t *lenptr); - void ValkeyModule_SaveDouble(ValkeyModuleIO *io, double value); - double ValkeyModule_LoadDouble(ValkeyModuleIO *io); +```C +void ValkeyModule_SaveUnsigned(ValkeyModuleIO *io, uint64_t value); +uint64_t ValkeyModule_LoadUnsigned(ValkeyModuleIO *io); +void ValkeyModule_SaveSigned(ValkeyModuleIO *io, int64_t value); +int64_t ValkeyModule_LoadSigned(ValkeyModuleIO *io); +void ValkeyModule_SaveString(ValkeyModuleIO *io, ValkeyModuleString *s); +void ValkeyModule_SaveStringBuffer(ValkeyModuleIO *io, const char *str, size_t len); +ValkeyModuleString *ValkeyModule_LoadString(ValkeyModuleIO *io); +char *ValkeyModule_LoadStringBuffer(ValkeyModuleIO *io, size_t *lenptr); +void ValkeyModule_SaveDouble(ValkeyModuleIO *io, double value); +double ValkeyModule_LoadDouble(ValkeyModuleIO *io); +``` The functions don't require any error checking from the module, that can always assume calls succeed. @@ -299,40 +319,46 @@ always assume calls succeed. As an example, imagine I've a native type that implements an array of double values, with the following structure: - struct double_array { - size_t count; - double *values; - }; +```C +struct double_array { + size_t count; + double *values; +}; +``` My `rdb_save` method may look like the following: - void DoubleArrayRDBSave(ValkeyModuleIO *io, void *ptr) { - struct dobule_array *da = ptr; - ValkeyModule_SaveUnsigned(io,da->count); - for (size_t j = 0; j < da->count; j++) - ValkeyModule_SaveDouble(io,da->values[j]); - } +```C +void DoubleArrayRDBSave(ValkeyModuleIO *io, void *ptr) { + struct dobule_array *da = ptr; + ValkeyModule_SaveUnsigned(io,da->count); + for (size_t j = 0; j < da->count; j++) + ValkeyModule_SaveDouble(io,da->values[j]); +} +``` What we did was to store the number of elements followed by each double value. So when later we'll have to load the structure in the `rdb_load` method we'll do something like this: - void *DoubleArrayRDBLoad(ValkeyModuleIO *io, int encver) { - if (encver != DOUBLE_ARRAY_ENC_VER) { - /* We should actually log an error here, or try to implement - the ability to load older versions of our data structure. */ - return NULL; - } - - struct double_array *da; - da = ValkeyModule_Alloc(sizeof(*da)); - da->count = ValkeyModule_LoadUnsigned(io); - da->values = ValkeyModule_Alloc(da->count * sizeof(double)); - for (size_t j = 0; j < da->count; j++) - da->values[j] = ValkeyModule_LoadDouble(io); - return da; +```C +void *DoubleArrayRDBLoad(ValkeyModuleIO *io, int encver) { + if (encver != DOUBLE_ARRAY_ENC_VER) { + /* We should actually log an error here, or try to implement + the ability to load older versions of our data structure. */ + return NULL; } + struct double_array *da; + da = ValkeyModule_Alloc(sizeof(*da)); + da->count = ValkeyModule_LoadUnsigned(io); + da->values = ValkeyModule_Alloc(da->count * sizeof(double)); + for (size_t j = 0; j < da->count; j++) + da->values[j] = ValkeyModule_LoadDouble(io); + return da; +} +``` + The load callback just reconstruct back the data structure from the data we stored in the RDB file. @@ -343,12 +369,9 @@ it reads does not look correct. Valkey will just panic in that case. AOF rewriting --- - void ValkeyModule_EmitAOF(ValkeyModuleIO *io, const char *cmdname, const char *fmt, ...); - -Handling multiple encodings ---- - - WORK IN PROGRESS +```C +void ValkeyModule_EmitAOF(ValkeyModuleIO *io, const char *cmdname, const char *fmt, ...); +``` Allocating memory --- @@ -371,10 +394,12 @@ to avoid replacing manually all the calls with the Valkey Modules API calls, an approach could be to use simple macros in order to replace the libc calls with the Valkey API calls. Something like this could work: - #define malloc ValkeyModule_Alloc - #define realloc ValkeyModule_Realloc - #define free ValkeyModule_Free - #define strdup ValkeyModule_Strdup +```C +#define malloc ValkeyModule_Alloc +#define realloc ValkeyModule_Realloc +#define free ValkeyModule_Free +#define strdup ValkeyModule_Strdup +``` However take in mind that mixing libc calls with Valkey API calls will result into troubles and crashes, so if you replace calls using macros, you need to From 64fef043c59aee00c5c49cc82904886a221eac61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 19:08:56 +0200 Subject: [PATCH 18/19] modules-intro.md refer to modules API ref, delete WIP, fix syntax-highlighing, #121 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/modules-intro.md | 375 ++++++++++++++++++++++++---------------- 1 file changed, 228 insertions(+), 147 deletions(-) diff --git a/topics/modules-intro.md b/topics/modules-intro.md index 7baad71b..b717ed4f 100644 --- a/topics/modules-intro.md +++ b/topics/modules-intro.md @@ -28,9 +28,6 @@ run with a specific version of Valkey. For this reason, the module will register to the Valkey core using a specific API version. The current API version is "1". -This document is about an alpha version of Valkey modules. API, functionalities -and other details may change in the future. - ## Loading modules In order to test the module you are developing, you can load the module @@ -62,25 +59,27 @@ uses to register itself into the Valkey core. In order to show the different parts of a module, here we'll show a very simple module that implements a command that outputs a random number. - #include "valkeymodule.h" - #include +```C +#include "valkeymodule.h" +#include - int HelloworldRand_ValkeyCommand(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) { - ValkeyModule_ReplyWithLongLong(ctx,rand()); - return VALKEYMODULE_OK; - } +int HelloworldRand_ValkeyCommand(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) { + ValkeyModule_ReplyWithLongLong(ctx,rand()); + return VALKEYMODULE_OK; +} - int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) { - if (ValkeyModule_Init(ctx,"helloworld",1,VALKEYMODULE_APIVER_1) - == VALKEYMODULE_ERR) return VALKEYMODULE_ERR; +int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) { + if (ValkeyModule_Init(ctx,"helloworld",1,VALKEYMODULE_APIVER_1) + == VALKEYMODULE_ERR) return VALKEYMODULE_ERR; - if (ValkeyModule_CreateCommand(ctx,"helloworld.rand", - HelloworldRand_ValkeyCommand, "fast random", - 0, 0, 0) == VALKEYMODULE_ERR) - return VALKEYMODULE_ERR; + if (ValkeyModule_CreateCommand(ctx,"helloworld.rand", + HelloworldRand_ValkeyCommand, "fast random", + 0, 0, 0) == VALKEYMODULE_ERR) + return VALKEYMODULE_ERR; - return VALKEYMODULE_OK; - } + return VALKEYMODULE_OK; +} +``` The example module has two functions. One implements a command called HELLOWORLD.RAND. This function is specific of that module. However the @@ -105,8 +104,10 @@ The above example shows the usage of the function `ValkeyModule_Init()`. It should be the first function called by the module `OnLoad` function. The following is the function prototype: - int ValkeyModule_Init(ValkeyModuleCtx *ctx, const char *modulename, - int module_version, int api_version); +```C +int ValkeyModule_Init(ValkeyModuleCtx *ctx, const char *modulename, + int module_version, int api_version); +``` The `Init` function announces the Valkey core that the module has a given name, its version (that is reported by `MODULE LIST`), and that is willing @@ -122,9 +123,11 @@ otherwise the module will segfault and the Valkey instance will crash. The second function called, `ValkeyModule_CreateCommand`, is used in order to register commands into the Valkey core. The following is the prototype: - int ValkeyModule_CreateCommand(ValkeyModuleCtx *ctx, const char *name, - ValkeyModuleCmdFunc cmdfunc, const char *strflags, - int firstkey, int lastkey, int keystep); +```C +int ValkeyModule_CreateCommand(ValkeyModuleCtx *ctx, const char *name, + ValkeyModuleCmdFunc cmdfunc, const char *strflags, + int firstkey, int lastkey, int keystep); +``` As you can see, most Valkey modules API calls all take as first argument the `context` of the module, so that they have a reference to the module @@ -136,7 +139,9 @@ and the positions of key names in the command's arguments. The function that implements the command must have the following prototype: - int mycommand(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc); +```C +int mycommand(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc); +``` The command function arguments are just the context, that will be passed to all the other API calls, the command argument vector, and total number @@ -148,7 +153,9 @@ functions to access and use, direct access to its fields is never needed. Zooming into the example command implementation, we can find another call: - int ValkeyModule_ReplyWithLongLong(ValkeyModuleCtx *ctx, long long integer); +```C +int ValkeyModule_ReplyWithLongLong(ValkeyModuleCtx *ctx, long long integer); +``` This function returns an integer to the client that invoked the command, exactly like other Valkey commands do, like for example `INCR` or `SCARD`. @@ -165,7 +172,9 @@ If a module provides this function, it will be invoked during the module unload process. The following is the function prototype: - int ValkeyModule_OnUnload(ValkeyModuleCtx *ctx); +```C +int ValkeyModule_OnUnload(ValkeyModuleCtx *ctx); +``` The `OnUnload` function may prevent module unloading by returning `VALKEYMODULE_ERR`. @@ -186,9 +195,11 @@ A module can be designed to support both newer and older Redis OSS versions wher If an API function is not implemented in the currently running Redis OSS version, the function pointer is set to NULL. This allows the module to check if a function exists before using it: - if (ValkeyModule_SetCommandInfo != NULL) { - ValkeyModule_SetCommandInfo(cmd, &info); - } +```C +if (ValkeyModule_SetCommandInfo != NULL) { + ValkeyModule_SetCommandInfo(cmd, &info); +} +``` In recent versions of `valkeymodule.h`, a convenience macro `RMAPI_FUNC_SUPPORTED(funcname)` is defined. Using the macro or just comparing with NULL is a matter of personal preference. @@ -221,7 +232,9 @@ you may need to directly access the string object. There are a few functions in order to work with string objects: - const char *ValkeyModule_StringPtrLen(ValkeyModuleString *string, size_t *len); +```C +const char *ValkeyModule_StringPtrLen(ValkeyModuleString *string, size_t *len); +``` The above function accesses a string by returning its pointer and setting its length in `len`. @@ -231,12 +244,16 @@ You should never write to a string object pointer, as you can see from the However, if you want, you can create new string objects using the following API: - ValkeyModuleString *ValkeyModule_CreateString(ValkeyModuleCtx *ctx, const char *ptr, size_t len); +```C +ValkeyModuleString *ValkeyModule_CreateString(ValkeyModuleCtx *ctx, const char *ptr, size_t len); +``` The string returned by the above command must be freed using a corresponding call to `ValkeyModule_FreeString()`: - void ValkeyModule_FreeString(ValkeyModuleString *str); +```C +void ValkeyModule_FreeString(ValkeyModuleString *str); +``` However if you want to avoid having to free strings, the automatic memory management, covered later in this document, can be a good alternative, by @@ -252,14 +269,18 @@ be freed. Creating a new string from an integer is a very common operation, so there is a function to do this: - ValkeyModuleString *mystr = ValkeyModule_CreateStringFromLongLong(ctx,10); +```C +ValkeyModuleString *mystr = ValkeyModule_CreateStringFromLongLong(ctx,10); +``` Similarly in order to parse a string as a number: - long long myval; - if (ValkeyModule_StringToLongLong(ctx,argv[1],&myval) == VALKEYMODULE_OK) { - /* Do something with 'myval' */ - } +```C +long long myval; +if (ValkeyModule_StringToLongLong(ctx,argv[1],&myval) == VALKEYMODULE_OK) { + /* Do something with 'myval' */ +} +``` ## Accessing Valkey keys from modules @@ -305,8 +326,10 @@ of ValkeyModuleString object pointers, and a C string representing the number "10" as second argument (the increment), I'll use the following function call: - ValkeyModuleCallReply *reply; - reply = ValkeyModule_Call(ctx,"INCRBY","sc",argv[1],"10"); +```C +ValkeyModuleCallReply *reply; +reply = ValkeyModule_Call(ctx,"INCRBY","sc",argv[1],"10"); +``` The first argument is the context, and the second is always a null terminated C string with the command name. The third argument is the format specifier @@ -344,11 +367,13 @@ In order to obtain the type or reply (corresponding to one of the data types supported by the Valkey protocol), the function `ValkeyModule_CallReplyType()` is used: - reply = ValkeyModule_Call(ctx,"INCRBY","sc",argv[1],"10"); - if (ValkeyModule_CallReplyType(reply) == VALKEYMODULE_REPLY_INTEGER) { - long long myval = ValkeyModule_CallReplyInteger(reply); - /* Do something with myval. */ - } +```C +reply = ValkeyModule_Call(ctx,"INCRBY","sc",argv[1],"10"); +if (ValkeyModule_CallReplyType(reply) == VALKEYMODULE_REPLY_INTEGER) { + long long myval = ValkeyModule_CallReplyInteger(reply); + /* Do something with myval. */ +} +``` Valid reply types are: @@ -363,19 +388,25 @@ the length corresponds to the length of the string. For arrays the length is the number of elements. To obtain the reply length the following function is used: - size_t reply_len = ValkeyModule_CallReplyLength(reply); +```C +size_t reply_len = ValkeyModule_CallReplyLength(reply); +``` In order to obtain the value of an integer reply, the following function is used, as already shown in the example above: - long long reply_integer_val = ValkeyModule_CallReplyInteger(reply); +```C +long long reply_integer_val = ValkeyModule_CallReplyInteger(reply); +``` Called with a reply object of the wrong type, the above function always returns `LLONG_MIN`. Sub elements of array replies are accessed this way: - ValkeyModuleCallReply *subreply; - subreply = ValkeyModule_CallReplyArrayElement(reply,idx); +```C +ValkeyModuleCallReply *subreply; +subreply = ValkeyModule_CallReplyArrayElement(reply,idx); +``` The above function returns NULL if you try to access out of range elements. @@ -384,8 +415,10 @@ be accessed using in the following way, making sure to never write to the resulting pointer (that is returned as a `const` pointer so that misusing must be pretty explicit): - size_t len; - char *ptr = ValkeyModule_CallReplyStringPtr(reply,&len); +```C +size_t len; +char *ptr = ValkeyModule_CallReplyStringPtr(reply,&len); +``` If the reply type is not a string or an error, NULL is returned. @@ -398,7 +431,9 @@ API could be a simpler way to implement your command, or you can use the following function in order to create a new string object from a call reply of type string, error or integer: - ValkeyModuleString *mystr = ValkeyModule_CreateStringFromCallReply(myreply); +```C +ValkeyModuleString *mystr = ValkeyModule_CreateStringFromCallReply(myreply); +``` If the reply is not of the right type, NULL is returned. The returned string object should be released with `ValkeyModule_FreeString()` @@ -433,7 +468,9 @@ All the functions to send a reply to the client are called To return an error, use: - ValkeyModule_ReplyWithError(ValkeyModuleCtx *ctx, const char *err); +```C +ValkeyModule_ReplyWithError(ValkeyModuleCtx *ctx, const char *err); +``` There is a predefined error string for key of wrong type errors: @@ -441,23 +478,31 @@ There is a predefined error string for key of wrong type errors: Example usage: - ValkeyModule_ReplyWithError(ctx,"ERR invalid arguments"); +```C +ValkeyModule_ReplyWithError(ctx,"ERR invalid arguments"); +``` We already saw how to reply with a `long long` in the examples above: - ValkeyModule_ReplyWithLongLong(ctx,12345); +```C +ValkeyModule_ReplyWithLongLong(ctx,12345); +``` To reply with a simple string, that can't contain binary values or newlines, (so it's suitable to send small words, like "OK") we use: - ValkeyModule_ReplyWithSimpleString(ctx,"OK"); +```C +ValkeyModule_ReplyWithSimpleString(ctx,"OK"); +``` It's possible to reply with "bulk strings" that are binary safe, using two different functions: - int ValkeyModule_ReplyWithStringBuffer(ValkeyModuleCtx *ctx, const char *buf, size_t len); +```C +int ValkeyModule_ReplyWithStringBuffer(ValkeyModuleCtx *ctx, const char *buf, size_t len); - int ValkeyModule_ReplyWithString(ValkeyModuleCtx *ctx, ValkeyModuleString *str); +int ValkeyModule_ReplyWithString(ValkeyModuleCtx *ctx, ValkeyModuleString *str); +``` The first function gets a C pointer and length. The second a ValkeyModuleString object. Use one or the other depending on the source type you have at hand. @@ -466,9 +511,11 @@ In order to reply with an array, you just need to use a function to emit the array length, followed by as many calls to the above functions as the number of elements of the array are: - ValkeyModule_ReplyWithArray(ctx,2); - ValkeyModule_ReplyWithStringBuffer(ctx,"age",3); - ValkeyModule_ReplyWithLongLong(ctx,22); +```C +ValkeyModule_ReplyWithArray(ctx,2); +ValkeyModule_ReplyWithStringBuffer(ctx,"age",3); +ValkeyModule_ReplyWithLongLong(ctx,22); +``` To return nested arrays is easy, your nested array element just uses another call to `ValkeyModule_ReplyWithArray()` followed by the calls to emit the @@ -484,24 +531,30 @@ later produce the command reply, a better solution is to start an array reply where the length is not known, and set it later. This is accomplished with a special argument to `ValkeyModule_ReplyWithArray()`: - ValkeyModule_ReplyWithArray(ctx, VALKEYMODULE_POSTPONED_LEN); +```C +ValkeyModule_ReplyWithArray(ctx, VALKEYMODULE_POSTPONED_LEN); +``` The above call starts an array reply so we can use other `ReplyWith` calls in order to produce the array items. Finally in order to set the length, use the following call: - ValkeyModule_ReplySetArrayLength(ctx, number_of_items); +```C +ValkeyModule_ReplySetArrayLength(ctx, number_of_items); +``` In the case of the FACTOR command, this translates to some code similar to this: - ValkeyModule_ReplyWithArray(ctx, VALKEYMODULE_POSTPONED_LEN); - number_of_factors = 0; - while(still_factors) { - ValkeyModule_ReplyWithLongLong(ctx, some_factor); - number_of_factors++; - } - ValkeyModule_ReplySetArrayLength(ctx, number_of_factors); +```C +ValkeyModule_ReplyWithArray(ctx, VALKEYMODULE_POSTPONED_LEN); +number_of_factors = 0; +while(still_factors) { + ValkeyModule_ReplyWithLongLong(ctx, some_factor); + number_of_factors++; +} +ValkeyModule_ReplySetArrayLength(ctx, number_of_factors); +``` Another common use case for this feature is iterating over the arrays of some collection and only returning the ones passing some kind of filtering. @@ -510,12 +563,14 @@ It is possible to have multiple nested arrays with postponed reply. Each call to `SetArray()` will set the length of the latest corresponding call to `ReplyWithArray()`: - ValkeyModule_ReplyWithArray(ctx, VALKEYMODULE_POSTPONED_LEN); - ... generate 100 elements ... - ValkeyModule_ReplyWithArray(ctx, VALKEYMODULE_POSTPONED_LEN); - ... generate 10 elements ... - ValkeyModule_ReplySetArrayLength(ctx, 10); - ValkeyModule_ReplySetArrayLength(ctx, 100); +```C +ValkeyModule_ReplyWithArray(ctx, VALKEYMODULE_POSTPONED_LEN); +// ... generate 100 elements ... +ValkeyModule_ReplyWithArray(ctx, VALKEYMODULE_POSTPONED_LEN); +// ... generate 10 elements ... +ValkeyModule_ReplySetArrayLength(ctx, 10); +ValkeyModule_ReplySetArrayLength(ctx, 100); +``` This creates a 100 items array having as last element a 10 items array. @@ -525,20 +580,24 @@ Often commands need to check that the number of arguments and type of the key is correct. In order to report a wrong arity, there is a specific function called `ValkeyModule_WrongArity()`. The usage is trivial: - if (argc != 2) return ValkeyModule_WrongArity(ctx); +```C +if (argc != 2) return ValkeyModule_WrongArity(ctx); +``` Checking for the wrong type involves opening the key and checking the type: - ValkeyModuleKey *key = ValkeyModule_OpenKey(ctx,argv[1], - VALKEYMODULE_READ|VALKEYMODULE_WRITE); +```C +ValkeyModuleKey *key = ValkeyModule_OpenKey(ctx,argv[1], + VALKEYMODULE_READ|VALKEYMODULE_WRITE); - int keytype = ValkeyModule_KeyType(key); - if (keytype != VALKEYMODULE_KEYTYPE_STRING && - keytype != VALKEYMODULE_KEYTYPE_EMPTY) - { - ValkeyModule_CloseKey(key); - return ValkeyModule_ReplyWithError(ctx,VALKEYMODULE_ERRORMSG_WRONGTYPE); - } +int keytype = ValkeyModule_KeyType(key); +if (keytype != VALKEYMODULE_KEYTYPE_STRING && + keytype != VALKEYMODULE_KEYTYPE_EMPTY) +{ + ValkeyModule_CloseKey(key); + return ValkeyModule_ReplyWithError(ctx,VALKEYMODULE_ERRORMSG_WRONGTYPE); +} +``` Note that you often want to proceed with a command both if the key is of the expected type, or if it's empty. @@ -563,8 +622,10 @@ In order to open a key the `ValkeyModule_OpenKey` function is used. It returns a key pointer, that we'll use with all the next calls to access and modify the value: - ValkeyModuleKey *key; - key = ValkeyModule_OpenKey(ctx,argv[1],VALKEYMODULE_READ); +```C +ValkeyModuleKey *key; +key = ValkeyModule_OpenKey(ctx,argv[1],VALKEYMODULE_READ); +``` The second argument is the key name, that must be a `ValkeyModuleString` object. The third argument is the mode: `VALKEYMODULE_READ` or `VALKEYMODULE_WRITE`. @@ -580,7 +641,9 @@ exist. Once you are done using a key, you can close it with: - ValkeyModule_CloseKey(key); +```C +ValkeyModule_CloseKey(key); +``` Note that if automatic memory management is enabled, you are not forced to close keys. When the module function returns, Valkey will take care to close @@ -590,7 +653,9 @@ all the keys which are still open. In order to obtain the value of a key, use the `ValkeyModule_KeyType()` function: - int keytype = ValkeyModule_KeyType(key); +```C +int keytype = ValkeyModule_KeyType(key); +``` It returns one of the following values: @@ -610,17 +675,21 @@ does not yet exists. To create a new key, open it for writing and then write to it using one of the key writing functions. Example: - ValkeyModuleKey *key; - key = ValkeyModule_OpenKey(ctx,argv[1],VALKEYMODULE_WRITE); - if (ValkeyModule_KeyType(key) == VALKEYMODULE_KEYTYPE_EMPTY) { - ValkeyModule_StringSet(key,argv[2]); - } +```C +ValkeyModuleKey *key; +key = ValkeyModule_OpenKey(ctx,argv[1],VALKEYMODULE_WRITE); +if (ValkeyModule_KeyType(key) == VALKEYMODULE_KEYTYPE_EMPTY) { + ValkeyModule_StringSet(key,argv[2]); +} +``` ## Deleting keys Just use: - ValkeyModule_DeleteKey(key); +```C +ValkeyModule_DeleteKey(key); +``` The function returns `VALKEYMODULE_ERR` if the key is not open for writing. Note that after a key gets deleted, it is setup in order to be targeted @@ -635,7 +704,9 @@ modify, get, and unset the time to live associated with a key. One function is used in order to query the current expire of an open key: - mstime_t ValkeyModule_GetExpire(ValkeyModuleKey *key); +```C +mstime_t ValkeyModule_GetExpire(ValkeyModuleKey *key); +``` The function returns the time to live of the key in milliseconds, or `VALKEYMODULE_NO_EXPIRE` as a special value to signal the key has no associated @@ -644,7 +715,9 @@ if the key type is `VALKEYMODULE_KEYTYPE_EMPTY`). In order to change the expire of a key the following function is used instead: - int ValkeyModule_SetExpire(ValkeyModuleKey *key, mstime_t expire); +```C +int ValkeyModule_SetExpire(ValkeyModuleKey *key, mstime_t expire); +``` When called on a non existing key, `VALKEYMODULE_ERR` is returned, because the function can only associate expires to existing open keys (non existing @@ -667,7 +740,9 @@ associated to an open key. The returned length is value-specific, and is the string length for strings, and the number of elements for the aggregated data types (how many elements there is in a list, set, sorted set, hash). - size_t len = ValkeyModule_ValueLength(key); +```C +size_t len = ValkeyModule_ValueLength(key); +``` If the key does not exist, 0 is returned by the function: @@ -676,7 +751,9 @@ If the key does not exist, 0 is returned by the function: Setting a new string value, like the Valkey `SET` command does, is performed using: - int ValkeyModule_StringSet(ValkeyModuleKey *key, ValkeyModuleString *str); +```C +int ValkeyModule_StringSet(ValkeyModuleKey *key, ValkeyModuleString *str); +``` The function works exactly like the Valkey `SET` command itself, that is, if there is a prior value (of any type) it will be deleted. @@ -685,9 +762,11 @@ Accessing existing string values is performed using DMA (direct memory access) for speed. The API will return a pointer and a length, so that's possible to access and, if needed, modify the string directly. - size_t len, j; - char *myptr = ValkeyModule_StringDMA(key,&len,VALKEYMODULE_WRITE); - for (j = 0; j < len; j++) myptr[j] = 'A'; +```C +size_t len, j; +char *myptr = ValkeyModule_StringDMA(key,&len,VALKEYMODULE_WRITE); +for (j = 0; j < len; j++) myptr[j] = 'A'; +``` In the above example we write directly on the string. Note that if you want to write, you must be sure to ask for `WRITE` mode. @@ -699,7 +778,9 @@ Sometimes when we want to manipulate strings directly, we need to change their size as well. For this scope, the `ValkeyModule_StringTruncate` function is used. Example: - ValkeyModule_StringTruncate(mykey,1024); +```C +ValkeyModule_StringTruncate(mykey,1024); +``` The function truncates, or enlarges the string as needed, padding it with zero bytes if the previous length is smaller than the new length we request. @@ -709,12 +790,18 @@ a string value is created and associated to the key. Note that every time `StringTruncate()` is called, we need to re-obtain the DMA pointer again, since the old may be invalid. +For a complete list of string type functions, see [Key API for String +type](modules-api-ref.md#section-key-api-for-string-type) in the Modules API +reference. + ## List type API It's possible to push and pop values from list values: - int ValkeyModule_ListPush(ValkeyModuleKey *key, int where, ValkeyModuleString *ele); - ValkeyModuleString *ValkeyModule_ListPop(ValkeyModuleKey *key, int where); +```C +int ValkeyModule_ListPush(ValkeyModuleKey *key, int where, ValkeyModuleString *ele); +ValkeyModuleString *ValkeyModule_ListPop(ValkeyModuleKey *key, int where); +``` In both the APIs the `where` argument specifies if to push or pop from tail or head, using the following macros: @@ -726,43 +813,25 @@ Elements returned by `ValkeyModule_ListPop()` are like strings created with `ValkeyModule_CreateString()`, they must be released with `ValkeyModule_FreeString()` or by enabling automatic memory management. +For a complete list of set type functions, see [Key API for List +type](modules-api-ref.md#section-key-api-for-list-type) in the Modules API +reference. + ## Set type API -Work in progress. +A direct API to set type keys is not yet implemented. +Use the `ValkeyModule_Call` API with set commands like SADD to access keys of type set. ## Sorted set type API -Documentation missing, please refer to the top comments inside `module.c` -for the following functions: - -* `ValkeyModule_ZsetAdd` -* `ValkeyModule_ZsetIncrby` -* `ValkeyModule_ZsetScore` -* `ValkeyModule_ZsetRem` - -And for the sorted set iterator: - -* `ValkeyModule_ZsetRangeStop` -* `ValkeyModule_ZsetFirstInScoreRange` -* `ValkeyModule_ZsetLastInScoreRange` -* `ValkeyModule_ZsetFirstInLexRange` -* `ValkeyModule_ZsetLastInLexRange` -* `ValkeyModule_ZsetRangeCurrentElement` -* `ValkeyModule_ZsetRangeNext` -* `ValkeyModule_ZsetRangePrev` -* `ValkeyModule_ZsetRangeEndReached` +See the [Key API for Sorted Set +type](modules-api-ref.md#section-key-api-for-sorted-set-type) section in the +Modules API reference. ## Hash type API -Documentation missing, please refer to the top comments inside `module.c` -for the following functions: - -* `ValkeyModule_HashSet` -* `ValkeyModule_HashGet` - -## Iterating aggregated values - -Work in progress. +See [Key API for Hash type](modules-api-ref.md#section-key-api-for-hash-type) in +the Modules API reference. ## Replicating commands @@ -775,7 +844,9 @@ When using the higher level APIs to invoke commands, replication happens automatically if you use the "!" modifier in the format string of `ValkeyModule_Call()` as in the following example: - reply = ValkeyModule_Call(ctx,"INCRBY","!sc",argv[1],"10"); +```C +reply = ValkeyModule_Call(ctx,"INCRBY","!sc",argv[1],"10"); +``` As you can see the format specifier is `"!sc"`. The bang is not parsed as a format specifier, but it internally flags the command as "must replicate". @@ -787,7 +858,9 @@ it consistently always performs the same work, what is possible to do is to replicate the command verbatim as the user executed it. To do that, you just need to call the following function: - ValkeyModule_ReplicateVerbatim(ctx); +```C +ValkeyModule_ReplicateVerbatim(ctx); +``` When you use the above API, you should not use any other replication function since they are not guaranteed to mix well. @@ -797,7 +870,9 @@ Valkey what commands to replicate as the effect of the command execution, using an API similar to `ValkeyModule_Call()` but that instead of calling the command sends it to the AOF / replicas stream. Example: - ValkeyModule_Replicate(ctx,"INCRBY","cl","foo",my_increment); +```C +ValkeyModule_Replicate(ctx,"INCRBY","cl","foo",my_increment); +``` It's possible to call `ValkeyModule_Replicate` multiple times, and each will emit a command. All the sequence emitted is wrapped between a @@ -834,7 +909,9 @@ you may still want to free strings no longer used. In order to enable automatic memory management, just call the following function at the start of the command implementation: - ValkeyModule_AutoMemory(ctx); +```C +ValkeyModule_AutoMemory(ctx); +``` Automatic memory management is usually the way to go, however experienced C programmers may not use it in order to gain some speed and memory usage @@ -848,11 +925,13 @@ not technically forbidden, it is a lot better to use the Valkey Modules specific functions, that are exact replacements for `malloc`, `free`, `realloc` and `strdup`. These functions are: - void *ValkeyModule_Alloc(size_t bytes); - void* ValkeyModule_Realloc(void *ptr, size_t bytes); - void ValkeyModule_Free(void *ptr); - void ValkeyModule_Calloc(size_t nmemb, size_t size); - char *ValkeyModule_Strdup(const char *str); +```C +void *ValkeyModule_Alloc(size_t bytes); +void* ValkeyModule_Realloc(void *ptr, size_t bytes); +void ValkeyModule_Free(void *ptr); +void ValkeyModule_Calloc(size_t nmemb, size_t size); +char *ValkeyModule_Strdup(const char *str); +``` They work exactly like their `libc` equivalent calls, however they use the same allocator Valkey uses, and the memory allocated using these @@ -876,7 +955,9 @@ execution, but are just functional to execute the command itself. This work can be more easily accomplished using the Valkey pool allocator: - void *ValkeyModule_PoolAlloc(ValkeyModuleCtx *ctx, size_t bytes); +```C +void *ValkeyModule_PoolAlloc(ValkeyModuleCtx *ctx, size_t bytes); +``` It works similarly to `malloc()`, and returns memory aligned to the next power of two of greater or equal to `bytes` (for a maximum alignment @@ -889,7 +970,7 @@ allocator. ## Writing commands compatible with Valkey Cluster -Documentation missing, please check the following functions inside `module.c`: +See the Modules API reference for the following commands: - ValkeyModule_IsKeysPositionRequest(ctx); - ValkeyModule_KeyAtPos(ctx,pos); +* [`ValkeyModule_IsKeysPositionRequest(ctx)`](modules-api-ref.md#ValkeyModule_IsKeysPositionRequest) +* [`ValkeyModule_KeyAtPos(ctx, pos)`](modules-api-ref.md#ValkeyModule_KeyAtPos) From d0d77ef1b9e0d4e6f41c82b842d3a7e2545cd1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Thu, 20 Jun 2024 19:23:59 +0200 Subject: [PATCH 19/19] modules-blocking-ops.md fix issues, #120 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Viktor Söderqvist --- topics/modules-blocking-ops.md | 207 ++++++++++++++++++--------------- 1 file changed, 116 insertions(+), 91 deletions(-) diff --git a/topics/modules-blocking-ops.md b/topics/modules-blocking-ops.md index 25f66c8a..c09d7573 100644 --- a/topics/modules-blocking-ops.md +++ b/topics/modules-blocking-ops.md @@ -24,9 +24,9 @@ that can be used in order to model blocking commands. How blocking and resuming works. --- -_Note: You may want to check the `helloblock.c` example in the Valkey source tree +**Note:** You may want to check the `helloblock.c` example in the Valkey source tree inside the `src/modules` directory, for a simple to understand example -on how the blocking API is applied._ +on how the blocking API is applied. In Valkey modules, commands are implemented by callback functions that are invoked by the Valkey core when the specific command is called @@ -35,7 +35,13 @@ some reply to the client. Using the following function instead, the function implementing the module command may request that the client is put into the blocked state: - ValkeyModuleBlockedClient *ValkeyModule_BlockClient(ValkeyModuleCtx *ctx, ValkeyModuleCmdFunc reply_callback, ValkeyModuleCmdFunc timeout_callback, void (*free_privdata)(void*), long long timeout_ms); +```C +ValkeyModuleBlockedClient *ValkeyModule_BlockClient(ValkeyModuleCtx *ctx, + ValkeyModuleCmdFunc reply_callback, + ValkeyModuleCmdFunc timeout_callback, + void (*free_privdata)(void*), + long long timeout_ms); +``` The function returns a `ValkeyModuleBlockedClient` object, which is later used in order to unblock the client. The arguments have the following @@ -49,7 +55,9 @@ meaning: Once a client is blocked, it can be unblocked with the following API: - int ValkeyModule_UnblockClient(ValkeyModuleBlockedClient *bc, void *privdata); +```C +int ValkeyModule_UnblockClient(ValkeyModuleBlockedClient *bc, void *privdata); +``` The function takes as argument the blocked client object returned by the previous call to `ValkeyModule_BlockClient()`, and unblock the client. @@ -73,41 +81,45 @@ that blocks a client for one second, and then send as reply "Hello!". Note: arity checks and other non important things are not implemented int his command, in order to take the example simple. - int Example_ValkeyCommand(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, - int argc) - { - ValkeyModuleBlockedClient *bc = - ValkeyModule_BlockClient(ctx,reply_func,timeout_func,NULL,0); +```C +int Example_ValkeyCommand(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, + int argc) +{ + ValkeyModuleBlockedClient *bc = + ValkeyModule_BlockClient(ctx,reply_func,timeout_func,NULL,0); - pthread_t tid; - pthread_create(&tid,NULL,threadmain,bc); + pthread_t tid; + pthread_create(&tid,NULL,threadmain,bc); - return VALKEYMODULE_OK; - } + return VALKEYMODULE_OK; +} - void *threadmain(void *arg) { - ValkeyModuleBlockedClient *bc = arg; +void *threadmain(void *arg) { + ValkeyModuleBlockedClient *bc = arg; - sleep(1); /* Wait one second and unblock. */ - ValkeyModule_UnblockClient(bc,NULL); - } + sleep(1); /* Wait one second and unblock. */ + ValkeyModule_UnblockClient(bc,NULL); +} +``` The above command blocks the client ASAP, spawning a thread that will wait a second and will unblock the client. Let's check the reply and timeout callbacks, which are in our case very similar, since they just reply the client with a different reply type. - int reply_func(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, - int argc) - { - return ValkeyModule_ReplyWithSimpleString(ctx,"Hello!"); - } +```C +int reply_func(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, + int argc) +{ + return ValkeyModule_ReplyWithSimpleString(ctx,"Hello!"); +} - int timeout_func(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, - int argc) - { - return ValkeyModule_ReplyWithNull(ctx); - } +int timeout_func(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, + int argc) +{ + return ValkeyModule_ReplyWithNull(ctx); +} +``` The reply callback just sends the "Hello!" string to the client. The important bit here is that the reply callback is called when the @@ -130,41 +142,49 @@ actually expansive operation of some kind. Then this random number can be passed to the reply function so that we return it to the command caller. In order to make this working, we modify the functions as follow: - void *threadmain(void *arg) { - ValkeyModuleBlockedClient *bc = arg; +```C +void *threadmain(void *arg) { + ValkeyModuleBlockedClient *bc = arg; - sleep(1); /* Wait one second and unblock. */ + sleep(1); /* Wait one second and unblock. */ - long *mynumber = ValkeyModule_Alloc(sizeof(long)); - *mynumber = rand(); - ValkeyModule_UnblockClient(bc,mynumber); - } + long *mynumber = ValkeyModule_Alloc(sizeof(long)); + *mynumber = rand(); + ValkeyModule_UnblockClient(bc,mynumber); +} +``` As you can see, now the unblocking call is passing some private data, that is the `mynumber` pointer, to the reply callback. In order to obtain this private data, the reply callback will use the following function: - void *ValkeyModule_GetBlockedClientPrivateData(ValkeyModuleCtx *ctx); +```C +void *ValkeyModule_GetBlockedClientPrivateData(ValkeyModuleCtx *ctx); +``` So our reply callback is modified like that: - int reply_func(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, - int argc) - { - long *mynumber = ValkeyModule_GetBlockedClientPrivateData(ctx); - /* IMPORTANT: don't free mynumber here, but in the - * free privdata callback. */ - return ValkeyModule_ReplyWithLongLong(ctx,mynumber); - } +```C +int reply_func(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, + int argc) +{ + long *mynumber = ValkeyModule_GetBlockedClientPrivateData(ctx); + /* IMPORTANT: don't free mynumber here, but in the + * free privdata callback. */ + return ValkeyModule_ReplyWithLongLong(ctx,mynumber); +} +``` Note that we also need to pass a `free_privdata` function when blocking the client with `ValkeyModule_BlockClient()`, since the allocated long value must be freed. Our callback will look like the following: - void free_privdata(void *privdata) { - ValkeyModule_Free(privdata); - } +```C +void free_privdata(void *privdata) { + ValkeyModule_Free(privdata); +} +``` NOTE: It is important to stress that the private data is best freed in the `free_privdata` callback because the reply function may not be called @@ -185,25 +205,29 @@ because this will trigger the reply callback to be called. In this case the best thing to do is to use the following function: - int ValkeyModule_AbortBlock(ValkeyModuleBlockedClient *bc); +```C +int ValkeyModule_AbortBlock(ValkeyModuleBlockedClient *bc); +``` Practically this is how to use it: - int Example_ValkeyCommand(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, - int argc) - { - ValkeyModuleBlockedClient *bc = - ValkeyModule_BlockClient(ctx,reply_func,timeout_func,NULL,0); - - pthread_t tid; - if (pthread_create(&tid,NULL,threadmain,bc) != 0) { - ValkeyModule_AbortBlock(bc); - ValkeyModule_ReplyWithError(ctx,"Sorry can't create a thread"); - } - - return VALKEYMODULE_OK; +```C +int Example_ValkeyCommand(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, + int argc) +{ + ValkeyModuleBlockedClient *bc = + ValkeyModule_BlockClient(ctx,reply_func,timeout_func,NULL,0); + + pthread_t tid; + if (pthread_create(&tid,NULL,threadmain,bc) != 0) { + ValkeyModule_AbortBlock(bc); + ValkeyModule_ReplyWithError(ctx,"Sorry can't create a thread"); } + return VALKEYMODULE_OK; +} +``` + The client will be unblocked but the reply callback will not be called. Implementing the command, reply and timeout callback using a single function @@ -213,34 +237,38 @@ The following functions can be used in order to implement the reply and callback with the same function that implements the primary command function: - int ValkeyModule_IsBlockedReplyRequest(ValkeyModuleCtx *ctx); - int ValkeyModule_IsBlockedTimeoutRequest(ValkeyModuleCtx *ctx); +```C +int ValkeyModule_IsBlockedReplyRequest(ValkeyModuleCtx *ctx); +int ValkeyModule_IsBlockedTimeoutRequest(ValkeyModuleCtx *ctx); +``` So I could rewrite the example command without using a separated reply and timeout callback: - int Example_ValkeyCommand(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, - int argc) - { - if (ValkeyModule_IsBlockedReplyRequest(ctx)) { - long *mynumber = ValkeyModule_GetBlockedClientPrivateData(ctx); - return ValkeyModule_ReplyWithLongLong(ctx,mynumber); - } else if (ValkeyModule_IsBlockedTimeoutRequest) { - return ValkeyModule_ReplyWithNull(ctx); - } - - ValkeyModuleBlockedClient *bc = - ValkeyModule_BlockClient(ctx,reply_func,timeout_func,NULL,0); - - pthread_t tid; - if (pthread_create(&tid,NULL,threadmain,bc) != 0) { - ValkeyModule_AbortBlock(bc); - ValkeyModule_ReplyWithError(ctx,"Sorry can't create a thread"); - } - - return VALKEYMODULE_OK; +```C +int Example_ValkeyCommand(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, + int argc) +{ + if (ValkeyModule_IsBlockedReplyRequest(ctx)) { + long *mynumber = ValkeyModule_GetBlockedClientPrivateData(ctx); + return ValkeyModule_ReplyWithLongLong(ctx,mynumber); + } else if (ValkeyModule_IsBlockedTimeoutRequest) { + return ValkeyModule_ReplyWithNull(ctx); + } + + ValkeyModuleBlockedClient *bc = + ValkeyModule_BlockClient(ctx,reply_func,timeout_func,NULL,0); + + pthread_t tid; + if (pthread_create(&tid,NULL,threadmain,bc) != 0) { + ValkeyModule_AbortBlock(bc); + ValkeyModule_ReplyWithError(ctx,"Sorry can't create a thread"); } + return VALKEYMODULE_OK; +} +``` + Functionally is the same but there are people that will prefer the less verbose implementation that concentrates most of the command logic in a single function. @@ -255,16 +283,13 @@ the old version. However when the thread terminated its work, the representations are swapped and the new, processed version, is used. An example of this approach is the -[Neural Valkey module](https://github.com/antirez/neural-redis) +[Neural Redis module](https://github.com/antirez/neural-redis) where neural networks are trained in different threads while the user can still execute and inspect their older versions. -Future work +Thread safe contexts --- -An API is work in progress right now in order to allow Valkey modules APIs -to be called in a safe way from threads, so that the threaded command -can access the data space and do incremental operations. - -There is no ETA for this feature but it may appear in the course of the -Redis OSS 4.0 release at some point. +See [Thread Safe Contexts](modules-api-ref.md#section-thread-safe-contexts) in +the Modules API reference for how Valkey modules APIs can be called in a safe +way from threads.