Browse Source

Add support to Redis test suite (#41)

* Fixed config and unix socket issues

* Added option to parse Option<t>.
* Fixed issue with unix socket. If file exists it must be removed.

* Update redis protocol parser lib

Updated library which added support for inline command parsing. Redis'
test suite uses inline command, so the support was a hard dependency.

Added also some logging information expected by the test suite,
specially to know when the server is ready to accept connections.

* Add FLUSHDB and DBSIZE commands

These commands are needed for test suite.

* Add INFO and improve SCAN parsing

Added INFO command and improve SCAN argument parsing to comply with the
main implementation.

* Add DEBUG command

* Added DEBUG command
* Fixed few internal typos in the Db mod
* Add Value digest()

* Add support for object debug

* Fixed bug in RENAMENX found with integrational test

* Fixed SETNX bug found by test suit

Fetching the previous would select event expired (but not yet collected)
data.

* Fix bug in GETEX PERSIST found by test suite

* Fixed bug in MSET / MSETNX

The Database was not checking if the list of key,value was complete or
not. Leading to a potential panic.

The issue was found by Redis test suite.

The fix also return the expected error.

* Fixed SETRANGE error handling

* Handle negative offset with an error (instead of invalid number)
* Handle max size

* Fixed bug with SET argument parsing

If the `GET` argument is passed, it should return the previous value or
Null.

This issue has been found with the redis test suite.

* Fixed GETRANGE math panic

If the value's len is 0 it would panic. This issue has been found by
redis' test suite.

* Fixed SET .. .. GET error

If the previous value is not a stream of bytes the whole operation must
fail and no changes should happen.

* Change error for INCR for non-string types

Instead of returning an NotANumber error it should return a WrongType
error

* Improve INCR* casting

Float numbers will be converted to integers if it is possible without
any loss.

* Rename error to be more standard with Redis

* Added support for negative RANK in LPOS

* Improved LINDEX

* Added better support for negative offsets
* Not found keys should return Null instead of `0`

* Improve `remove_element`

It was receiving usize instead of Option<usize> and using `0` instead of
None and it was very confusing.

* Fixed BLPOP/BRPOP timeout=0

It should wait forever, instead of giving up right away.

* Update `remove_element`

Delete key if the list is empty after removing elements

* Improve error code

* Update BLPOP/BRPOP timeout parsing errors

* Working on `client unblock`

* Improved LRANGE

* Change error while trying to parse int instead of number

* Improved `smismember` when key does not exists

* Fixed bug for SUNION when first key does not exists

* Improve sdiffstore

sdiffstore should delete the key if the sdiff is empty.

* Improved SINTER with empty keys

* Improved sinterstore and sunionstore

* SPOP/SRANDMEMBER must return Null when the key is not found instead of 0

* Add support for srandmember with negative limits

* Improved SMOVE

* Removed deadlock when the source/target are the same key
* Check if the key is the a set *before* doing any other check
* Improve other error cases

* Fixed response of HMGET when key doesn't exists

* Improve HDEL

If hash is empty after HDEL the key must be deleted

* Improve INCR / HINCRBY / HINCRBYFLOAT

* Improved internal storage. Rounding to an integer if possible
* Move HINCRY* to the Database for better edge case handling

* Minor changes

* Added Overflow protection for INCR/HINCR

The overflow is for f64 and integers

* Float must be returned as strings

In INCR/HINCR

* Add support for failed tx

Failed Tx will continue queueing commands as regular but won't execute
and will return a very particular error.

* SPOP should remove key if the set is empty

* Improved WATCH behaviour

WATCH calls are not allowed inside Multi, only before.

* Added support for FLUSHALL

* Fix WATCH for non-existing keys

It should be watch a `0` version instead of current.

* Implement QUIT command

* Improvements

 * Fixed PING to be called inside pubsub
 * Changed error for invalid expiration

* Fixed expiration overflows

* Adding extra params to EXPIRE / EXPIREAT

* Parse EXPIRE/EXPIREAT options

* Fixed time parsing

* Make sure exists only counts non-expired keys

A key may exists, because it was not yet gargabe collected. Before
counting it must be checked to be valid.

* Improved timestamp parsing and its errors

* Improved data mutation

* Improved PING in pubsub mode

* Improved SUBSCRIBE support

* PING support
* Added Value::Ignore (this value is not returned to the client).
* Fixed PUBSUB NUMPAT

* Add Redis tests

Added official redis test suite from their repo. A few tests were
disabled because they are not yet supported or they won't be ever
supported.

The goal going forward is to keep updating the tests regularly.

Credits: https://github.com/redis/redis
César D. Rodas 2 years ago
parent
commit
8504a488ca
100 changed files with 10471 additions and 760 deletions
  1. 16 0
      .github/workflows/ci.yml
  2. 158 55
      Cargo.lock
  3. 5 1
      Cargo.toml
  4. 36 0
      Makefile
  5. 2 2
      redis-config-parser/src/de/args.rs
  6. 2 0
      redis-config-parser/src/de/mod.rs
  7. 26 3
      redis-config-parser/src/de/value.rs
  8. 14 0
      runtest
  9. 39 10
      src/cmd/client.rs
  10. 59 51
      src/cmd/hash.rs
  11. 62 71
      src/cmd/key.rs
  12. 355 88
      src/cmd/list.rs
  13. 26 11
      src/cmd/pubsub.rs
  14. 61 4
      src/cmd/server.rs
  15. 174 45
      src/cmd/set.rs
  16. 165 133
      src/cmd/string.rs
  17. 28 3
      src/cmd/transaction.rs
  18. 1 4
      src/config.rs
  19. 5 0
      src/connection/connections.rs
  20. 70 11
      src/connection/mod.rs
  21. 26 18
      src/connection/pubsub_connection.rs
  22. 40 34
      src/connection/pubsub_server.rs
  23. 6 0
      src/db/expiration.rs
  24. 312 142
      src/db/mod.rs
  25. 65 0
      src/db/utils.rs
  26. 1 1
      src/dispatcher/command.rs
  27. 58 4
      src/dispatcher/mod.rs
  28. 70 47
      src/error.rs
  29. 25 4
      src/macros.rs
  30. 11 5
      src/main.rs
  31. 19 4
      src/server.rs
  32. 77 0
      src/value/expiration.rs
  33. 74 0
      src/value/float.rs
  34. 90 9
      src/value/mod.rs
  35. 57 0
      tests/README.md
  36. BIN
      tests/assets/corrupt_empty_keys.rdb
  37. BIN
      tests/assets/corrupt_ziplist.rdb
  38. 27 0
      tests/assets/default.conf
  39. BIN
      tests/assets/encodings.rdb
  40. BIN
      tests/assets/hash-ziplist.rdb
  41. BIN
      tests/assets/hash-zipmap.rdb
  42. 5 0
      tests/assets/minimal.conf
  43. 2 0
      tests/assets/nodefaultuser.acl
  44. 3 0
      tests/assets/user.acl
  45. 177 0
      tests/cluster/cluster.tcl
  46. 29 0
      tests/cluster/run.tcl
  47. 59 0
      tests/cluster/tests/00-base.tcl
  48. 38 0
      tests/cluster/tests/01-faildet.tcl
  49. 65 0
      tests/cluster/tests/02-failover.tcl
  50. 115 0
      tests/cluster/tests/03-failover-loop.tcl
  51. 194 0
      tests/cluster/tests/04-resharding.tcl
  52. 171 0
      tests/cluster/tests/05-slave-selection.tcl
  53. 73 0
      tests/cluster/tests/06-slave-stop-cond.tcl
  54. 103 0
      tests/cluster/tests/07-replica-migration.tcl
  55. 90 0
      tests/cluster/tests/08-update-msg.tcl
  56. 40 0
      tests/cluster/tests/09-pubsub.tcl
  57. 192 0
      tests/cluster/tests/10-manual-failover.tcl
  58. 59 0
      tests/cluster/tests/11-manual-takeover.tcl
  59. 74 0
      tests/cluster/tests/12-replica-migration-2.tcl
  60. 71 0
      tests/cluster/tests/12.1-replica-migration-3.tcl
  61. 61 0
      tests/cluster/tests/13-no-failover-option.tcl
  62. 117 0
      tests/cluster/tests/14-consistency-check.tcl
  63. 63 0
      tests/cluster/tests/15-cluster-slots.tcl
  64. 71 0
      tests/cluster/tests/16-transactions-on-replica.tcl
  65. 86 0
      tests/cluster/tests/17-diskless-load-swapdb.tcl
  66. 45 0
      tests/cluster/tests/18-info.tcl
  67. 71 0
      tests/cluster/tests/19-cluster-nodes-slots.tcl
  68. 98 0
      tests/cluster/tests/20-half-migrated-slot.tcl
  69. 64 0
      tests/cluster/tests/21-many-slot-migration.tcl
  70. 16 0
      tests/cluster/tests/helpers/onlydots.tcl
  71. 70 0
      tests/cluster/tests/includes/init-tests.tcl
  72. 25 0
      tests/cluster/tests/includes/utils.tcl
  73. 2 0
      tests/cluster/tmp/.gitignore
  74. 55 0
      tests/helpers/bg_block_op.tcl
  75. 13 0
      tests/helpers/bg_complex_data.tcl
  76. 58 0
      tests/helpers/fake_redis_node.tcl
  77. 18 0
      tests/helpers/gen_write_load.tcl
  78. 673 0
      tests/instances.tcl
  79. 36 0
      tests/integration/aof-race.tcl
  80. 323 0
      tests/integration/aof.tcl
  81. 51 0
      tests/integration/block-repl.tcl
  82. 28 0
      tests/integration/convert-ziplist-hash-on-load.tcl
  83. 39 0
      tests/integration/convert-zipmap-hash-on-load.tcl
  84. 217 0
      tests/integration/corrupt-dump-fuzzer.tcl
  85. 730 0
      tests/integration/corrupt-dump.tcl
  86. 101 0
      tests/integration/dismiss-mem.tcl
  87. 294 0
      tests/integration/failover.tcl
  88. 55 0
      tests/integration/logging.tcl
  89. 244 0
      tests/integration/psync2-pingoff.tcl
  90. 82 0
      tests/integration/psync2-reg.tcl
  91. 445 0
      tests/integration/psync2.tcl
  92. 314 0
      tests/integration/rdb.tcl
  93. 168 0
      tests/integration/redis-benchmark.tcl
  94. 402 0
      tests/integration/redis-cli.tcl
  95. 92 0
      tests/integration/replication-2.tcl
  96. 147 0
      tests/integration/replication-3.tcl
  97. 143 0
      tests/integration/replication-4.tcl
  98. 143 0
      tests/integration/replication-psync.tcl
  99. 928 0
      tests/integration/replication.tcl
  100. 61 0
      tests/modules/Makefile

+ 16 - 0
.github/workflows/ci.yml

@@ -0,0 +1,16 @@
+name: CI
+
+on: [push, pull_request]
+
+jobs:
+
+  test-ubuntu-latest:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - name: build
+      run: cargo build --release
+    - name: test
+      run: |
+        sudo apt-get install tcl8.6 tclx
+        make test

+ 158 - 55
Cargo.lock

@@ -121,6 +121,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
+name = "block-buffer"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
 name = "byteorder"
 version = "1.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -139,6 +148,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
+name = "cpufeatures"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
 name = "crc32fast"
 version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -149,9 +167,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.4"
+version = "0.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53"
+checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c"
 dependencies = [
  "cfg-if",
  "crossbeam-utils",
@@ -159,12 +177,22 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.8"
+version = "0.8.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
+checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83"
 dependencies = [
  "cfg-if",
- "lazy_static",
+ "once_cell",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ccfd8c0ee4cce11e45b3fd6f9d5e69e0cc62912aa6a0cb1bf4617b0eba5a12f"
+dependencies = [
+ "generic-array",
+ "typenum",
 ]
 
 [[package]]
@@ -213,6 +241,16 @@ dependencies = [
 ]
 
 [[package]]
+name = "digest"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
 name = "doc-comment"
 version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -348,14 +386,46 @@ dependencies = [
 ]
 
 [[package]]
+name = "generic-array"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
 name = "getrandom"
-version = "0.2.6"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
+checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
 dependencies = [
  "cfg-if",
  "libc",
- "wasi 0.10.2+wasi-snapshot-preview1",
+ "wasi",
+]
+
+[[package]]
+name = "git-version"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899"
+dependencies = [
+ "git-version-macro",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "git-version-macro"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f"
+dependencies = [
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
@@ -366,9 +436,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
 
 [[package]]
 name = "hashbrown"
-version = "0.11.2"
+version = "0.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"
 
 [[package]]
 name = "hdrhistogram"
@@ -409,6 +479,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
 name = "ident_case"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -416,9 +492,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 
 [[package]]
 name = "indexmap"
-version = "1.8.2"
+version = "1.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a"
+checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
 dependencies = [
  "autocfg",
  "hashbrown",
@@ -521,9 +597,12 @@ dependencies = [
  "crc32fast",
  "flexi_logger",
  "futures",
+ "git-version",
  "glob",
+ "hex",
  "log",
  "metered",
+ "num-traits",
  "parking_lot 0.11.2",
  "rand",
  "redis-config-parser",
@@ -533,6 +612,7 @@ dependencies = [
  "serde-enum-str",
  "serde_json",
  "serde_prometheus",
+ "sha2",
  "strum",
  "strum_macros",
  "thiserror",
@@ -558,13 +638,13 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "0.8.3"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
+checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
 dependencies = [
  "libc",
  "log",
- "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasi",
  "windows-sys",
 ]
 
@@ -608,9 +688,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.12.0"
+version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
+checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
 
 [[package]]
 name = "parking_lot"
@@ -679,19 +759,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
 
 [[package]]
+name = "proc-macro-hack"
+version = "0.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+
+[[package]]
 name = "proc-macro2"
-version = "1.0.39"
+version = "1.0.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
+checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.18"
+version = "1.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
 dependencies = [
  "proc-macro2",
 ]
@@ -737,9 +823,9 @@ dependencies = [
 
 [[package]]
 name = "redis-zero-protocol-parser"
-version = "0.2.1"
+version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "299d79f6c9095164339b8ed3c47951772538a375e6811e76ffe9a4544f2cdbf5"
+checksum = "ce09ac67be4dea57370b1f198244d9da12df26c09a3ccd33a88a6a2478528773"
 
 [[package]]
 name = "redox_syscall"
@@ -752,9 +838,9 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.5.6"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
+checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -763,15 +849,15 @@ dependencies = [
 
 [[package]]
 name = "regex-syntax"
-version = "0.6.26"
+version = "0.6.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
+checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
 
 [[package]]
 name = "rustversion"
-version = "1.0.6"
+version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f"
+checksum = "a0a5f7c728f5d284929a1cccb5bc19884422bfe6ef4d6c409da2c41838983fcf"
 
 [[package]]
 name = "ryu"
@@ -793,9 +879,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
 
 [[package]]
 name = "serde"
-version = "1.0.137"
+version = "1.0.138"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
+checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47"
 dependencies = [
  "serde_derive",
 ]
@@ -832,9 +918,9 @@ checksum = "fd2930103714ccef4f1fe5b6a5f2b6fdcfe462a6c802464714bd41e5b5097c33"
 
 [[package]]
 name = "serde_derive"
-version = "1.0.137"
+version = "1.0.138"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
+checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -843,9 +929,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.81"
+version = "1.0.82"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
+checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7"
 dependencies = [
  "itoa 1.0.2",
  "ryu",
@@ -868,6 +954,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "sha2"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
 name = "signal-hook-registry"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -884,9 +981,9 @@ checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
 
 [[package]]
 name = "smallvec"
-version = "1.8.0"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
 
 [[package]]
 name = "snafu"
@@ -921,15 +1018,15 @@ dependencies = [
 
 [[package]]
 name = "strum"
-version = "0.24.0"
+version = "0.24.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e96acfc1b70604b8b2f1ffa4c57e59176c7dbb05d556c71ecd2f5498a1dee7f8"
+checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
 
 [[package]]
 name = "strum_macros"
-version = "0.24.0"
+version = "0.24.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6878079b17446e4d3eba6192bb0a2950d5b14f0ed8424b852310e5a94345d0ef"
+checksum = "4faebde00e8ff94316c01800f9054fd2ba77d30d9e922541913051d1d978918b"
 dependencies = [
  "heck 0.4.0",
  "proc-macro2",
@@ -940,9 +1037,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.96"
+version = "1.0.98"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf"
+checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1000,9 +1097,9 @@ checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
 
 [[package]]
 name = "tokio"
-version = "1.19.1"
+version = "1.19.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95eec79ea28c00a365f539f1961e9278fbcaf81c0ff6aaf0e93c181352446948"
+checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439"
 dependencies = [
  "bytes",
  "libc",
@@ -1059,9 +1156,9 @@ dependencies = [
 
 [[package]]
 name = "tracing"
-version = "0.1.34"
+version = "0.1.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
+checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160"
 dependencies = [
  "cfg-if",
  "pin-project-lite",
@@ -1070,18 +1167,24 @@ dependencies = [
 
 [[package]]
 name = "tracing-core"
-version = "0.1.26"
+version = "0.1.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f"
+checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7"
 dependencies = [
- "lazy_static",
+ "once_cell",
 ]
 
 [[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
+[[package]]
 name = "unicode-ident"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
+checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
 
 [[package]]
 name = "unicode-segmentation"
@@ -1090,10 +1193,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
 
 [[package]]
-name = "wasi"
-version = "0.10.2+wasi-snapshot-preview1"
+name = "version_check"
+version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
 [[package]]
 name = "wasi"

+ 5 - 1
Cargo.toml

@@ -9,13 +9,15 @@ edition = "2018"
 [dependencies]
 bytes = "1"
 byteorder = "1.2.2"
-redis-zero-protocol-parser = "^0.2"
+redis-zero-protocol-parser = "^0.3"
 redis-config-parser = {path = "redis-config-parser"}
 tokio={version="1", features = ["full", "tracing"] }
 parking_lot="0.11.2"
 tokio-util={version="^0.6", features = ["full"] }
 crc32fast="1.3.2"
 futures = { version = "0.3.0", features = ["thread-pool"]}
+hex = "0.4.3"
+git-version = "0.3.5"
 tokio-stream="0.1"
 seahash = "4"
 glob="0.3.0"
@@ -25,11 +27,13 @@ serde="1.0.136"
 serde-enum-str = "0.2"
 serde_json = "1.0.70"
 serde_prometheus="0.1.6"
+sha2 = "0.10.2"
 rand = "0.8.0"
 log="0.4"
 thiserror = "1.0.30"
 strum = "0.24"
 strum_macros = "0.24"
+num-traits = "0.2.15"
 
 [workspace]
 members = ["redis-config-parser"]

+ 36 - 0
Makefile

@@ -0,0 +1,36 @@
+build:
+	cargo build --release
+test: build
+	./runtest  --clients 5 \
+		--skipunit unit/dump \
+		--skipunit unit/auth \
+		--skipunit unit/protocol \
+		--skipunit unit/keyspace \
+		--skipunit unit/scan \
+		--skipunit unit/info \
+		--skipunit unit/type/zset \
+		--skipunit unit/bitops \
+		--skipunit unit/type/stream \
+		--skipunit unit/type/stream-cgroups \
+		--skipunit unit/sort \
+		--skipunit unit/other \
+		--skipunit unit/aofrw \
+		--skipunit unit/acl \
+		--skipunit unit/latency-monitor \
+		--skipunit unit/slowlog \
+		--skipunit unit/scripting \
+		--skipunit unit/introspection \
+		--skipunit unit/introspection-2 \
+		--skipunit unit/bitfield \
+		--skipunit unit/geo \
+		--skipunit unit/pause \
+		--skipunit unit/hyperloglog \
+		--skipunit unit/lazyfree \
+		--skipunit unit/tracking \
+		--skipunit unit/querybuf \
+		--ignore-encoding \
+		--tags -needs:repl \
+		--tags -leaks \
+		--tags -needs:debug \
+		--tags -external:skip \
+		--tags -cli --tags -needs:config-maxmemory --stop 2>&1

+ 2 - 2
redis-config-parser/src/de/args.rs

@@ -33,8 +33,8 @@ impl<'de> de::Deserializer<'de> for ArgsDeserializer<'de> {
 }
 
 pub struct SeqArgsDeserializer<'a> {
-    input: Vec<Cow<'a, str>>,
-    id: usize,
+    pub input: Vec<Cow<'a, str>>,
+    pub id: usize,
 }
 
 impl<'de> de::SeqAccess<'de> for SeqArgsDeserializer<'de> {

+ 2 - 0
redis-config-parser/src/de/mod.rs

@@ -209,6 +209,7 @@ mod test {
         #[serde(flatten)]
         log: Log,
         databases: u8,
+        bind: Vec<String>,
     }
 
     #[derive(Deserialize, Debug, Default)]
@@ -265,5 +266,6 @@ mod test {
         assert_eq!(LogLevel::Verbose, x.log.level);
         assert_eq!("", x.log.file);
         assert_eq!(16, x.databases);
+        assert_eq!(vec!["127.0.0.1".to_owned()], x.bind);
     }
 }

+ 26 - 3
redis-config-parser/src/de/value.rs

@@ -1,11 +1,24 @@
-use super::Error;
+use super::{args::SeqArgsDeserializer, Error};
 use serde::de;
-use std::{borrow::Cow, str::FromStr};
+use std::{borrow::Cow, fmt, str::FromStr};
 
 pub struct ValueDeserializer<'a> {
     pub input: Cow<'a, str>,
 }
 
+pub struct Fmt<F>(pub F)
+where
+    F: Fn(&mut fmt::Formatter) -> fmt::Result;
+
+impl<F> fmt::Debug for Fmt<F>
+where
+    F: Fn(&mut fmt::Formatter) -> fmt::Result,
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        (self.0)(f)
+    }
+}
+
 impl<'de> de::Deserializer<'de> for ValueDeserializer<'de> {
     type Error = Error;
 
@@ -18,7 +31,17 @@ impl<'de> de::Deserializer<'de> for ValueDeserializer<'de> {
             Err(_) => match self.input.to_ascii_lowercase().as_str() {
                 "true" | "yes" => visitor.visit_bool(true),
                 "false" | "no" => visitor.visit_bool(false),
-                _ => visitor.visit_str(&self.input),
+                _ => {
+                    // is there a better hack?
+                    match format!("{:?}", Fmt(|f| visitor.expecting(f))).as_str() {
+                        "a sequence" => visitor.visit_seq(SeqArgsDeserializer {
+                            input: vec![self.input],
+                            id: 0,
+                        }),
+                        "option" => visitor.visit_some(ValueDeserializer { input: self.input }),
+                        _ => visitor.visit_str(&self.input),
+                    }
+                }
             },
         }
     }

+ 14 - 0
runtest

@@ -0,0 +1,14 @@
+#!/bin/sh
+TCL_VERSIONS="8.5 8.6"
+TCLSH=""
+
+for VERSION in $TCL_VERSIONS; do
+	TCL=`which tclsh$VERSION 2>/dev/null` && TCLSH=$TCL
+done
+
+if [ -z $TCLSH ]
+then
+    echo "You need tcl 8.5 or newer in order to run the Redis test"
+    exit 1
+fi
+$TCLSH tests/test_helper.tcl "${@}"

+ 39 - 10
src/cmd/client.rs

@@ -1,10 +1,10 @@
 //!  # Client-group command handlers
 
 use crate::{
-    connection::Connection,
+    connection::{Connection, ConnectionStatus, UnblockReason},
     error::Error,
     option,
-    value::{bytes_to_number, Value},
+    value::{bytes_to_int, bytes_to_number, Value},
 };
 use bytes::Bytes;
 use std::sync::Arc;
@@ -17,15 +17,18 @@ pub async fn client(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
     let sub = String::from_utf8_lossy(&args[1]);
 
     let expected = match sub.to_lowercase().as_str() {
-        "setname" => 3,
-        _ => 2,
+        "setname" => Some(3),
+        "unblock" => None,
+        _ => Some(2),
     };
 
-    if args.len() != expected {
-        return Err(Error::WrongArgument(
-            "client".to_owned(),
-            sub.to_uppercase(),
-        ));
+    if let Some(expected) = expected {
+        if args.len() != expected {
+            return Err(Error::WrongArgument(
+                "client".to_owned(),
+                sub.to_uppercase(),
+            ));
+        }
     }
 
     match sub.to_lowercase().as_str() {
@@ -38,6 +41,29 @@ pub async fn client(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
                 .iter(&mut |conn: Arc<Connection>| list_client.push_str(&conn.to_string()));
             Ok(list_client.into())
         }
+        "unblock" => {
+            let reason = match args.get(3) {
+                Some(x) => match String::from_utf8_lossy(&x).to_uppercase().as_str() {
+                    "TIMEOUT" => UnblockReason::Timeout,
+                    "ERROR" => UnblockReason::Error,
+                    _ => return Err(Error::Syntax),
+                },
+                None => UnblockReason::Timeout,
+            };
+            let other_conn = match conn
+                .all_connections()
+                .get_by_conn_id(bytes_to_int(&args[2])?)
+            {
+                Some(conn) => conn,
+                None => return Ok(0.into()),
+            };
+
+            Ok(if other_conn.unblock(reason) {
+                1.into()
+            } else {
+                0.into()
+            })
+        }
         "setname" => {
             let name = String::from_utf8_lossy(&args[2]).to_string();
             conn.set_name(name);
@@ -68,7 +94,10 @@ pub async fn select(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 ///
 /// Documentation:
 ///  * <https://redis.io/commands/ping>
-pub async fn ping(_conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
+pub async fn ping(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
+    if conn.status() == ConnectionStatus::Pubsub {
+        return Ok(Value::Array(vec!["pong".into(), args.get(1).into()]));
+    }
     match args.len() {
         1 => Ok(Value::String("PONG".to_owned())),
         2 => Ok(Value::new(&args[1])),

+ 59 - 51
src/cmd/hash.rs

@@ -1,6 +1,10 @@
 //! # Hash command handlers
 use crate::{
-    check_arg, connection::Connection, error::Error, value::bytes_to_number, value::Value,
+    check_arg,
+    connection::Connection,
+    error::Error,
+    value::Value,
+    value::{bytes_to_number, float::Float},
 };
 use bytes::Bytes;
 use rand::Rng;
@@ -15,6 +19,7 @@ use std::{
 /// within this hash are ignored. If key does not exist, it is treated as an empty hash and this
 /// command returns 0.
 pub async fn hdel(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
+    let mut is_empty = false;
     let result = conn.db().get_map_or(
         &args[1],
         |v| match v {
@@ -28,6 +33,8 @@ pub async fn hdel(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
                     }
                 }
 
+                is_empty = h.len() == 0;
+
                 Ok(total.into())
             }
             _ => Err(Error::WrongType),
@@ -35,7 +42,11 @@ pub async fn hdel(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
         || Ok(0.into()),
     )?;
 
-    conn.db().bump_version(&args[1]);
+    if is_empty {
+        let _ = conn.db().del(&[args[1].clone()]);
+    } else {
+        conn.db().bump_version(&args[1]);
+    }
 
     Ok(result)
 }
@@ -61,11 +72,11 @@ pub async fn hget(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
     conn.db().get_map_or(
         &args[1],
         |v| match v {
-            Value::Hash(h) => Ok(if let Some(v) = h.read().get(&args[2]) {
-                Value::new(&v)
-            } else {
-                Value::Null
-            }),
+            Value::Hash(h) => Ok(h
+                .read()
+                .get(&args[2])
+                .map(|v| Value::new(v))
+                .unwrap_or_default()),
             _ => Err(Error::WrongType),
         },
         || Ok(Value::Null),
@@ -98,37 +109,24 @@ pub async fn hgetall(conn: &Connection, args: &[Bytes]) -> Result<Value, Error>
 /// specified increment. If the increment value is negative, the result is to have the hash field
 /// value decremented instead of incremented. If the field does not exist, it is set to 0 before
 /// performing the operation.
-pub async fn hincrby<
-    T: ToString + FromStr + AddAssign + for<'a> TryFrom<&'a Value, Error = Error> + Into<Value> + Copy,
->(
-    conn: &Connection,
-    args: &[Bytes],
-) -> Result<Value, Error> {
-    let result = conn.db().get_map_or(
-        &args[1],
-        |v| match v {
-            Value::Hash(h) => {
-                let mut incr_by: T = bytes_to_number(&args[3])?;
-                let mut h = h.write();
-                if let Some(n) = h.get(&args[2]) {
-                    incr_by += bytes_to_number(n)?;
-                }
+pub async fn hincrby_int(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
+    let result = conn
+        .db()
+        .hincrby::<i64>(&args[1], &args[2], &args[3], "an integer")?;
 
-                h.insert(args[2].clone(), incr_by.to_string().into());
+    conn.db().bump_version(&args[1]);
 
-                Ok(incr_by.into())
-            }
-            _ => Err(Error::WrongType),
-        },
-        || {
-            let incr_by: T = bytes_to_number(&args[3])?;
-            #[allow(clippy::mutable_key_type)]
-            let mut h = HashMap::new();
-            h.insert(args[2].clone(), incr_by.to_string().into());
-            conn.db().set(&args[1], h.into(), None);
-            Ok(incr_by.into())
-        },
-    )?;
+    Ok(result)
+}
+
+/// Increment the specified field of a hash stored at key, and representing a number, by the
+/// specified increment. If the increment value is negative, the result is to have the hash field
+/// value decremented instead of incremented. If the field does not exist, it is set to 0 before
+/// performing the operation.
+pub async fn hincrby_float(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
+    let result = conn
+        .db()
+        .hincrby::<Float>(&args[1], &args[2], &args[3], "a float")?;
 
     conn.db().bump_version(&args[1]);
 
@@ -177,19 +175,19 @@ pub async fn hmget(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 
                 Ok((&args[2..])
                     .iter()
-                    .map(|key| {
-                        if let Some(value) = h.get(key) {
-                            Value::new(&value)
-                        } else {
-                            Value::Null
-                        }
-                    })
+                    .map(|key| h.get(key).map(|v| Value::new(v)).unwrap_or_default())
                     .collect::<Vec<Value>>()
                     .into())
             }
             _ => Err(Error::WrongType),
         },
-        || Ok(Value::Array(vec![])),
+        || {
+            Ok((&args[2..])
+                .iter()
+                .map(|_| Value::Null)
+                .collect::<Vec<Value>>()
+                .into())
+        },
     )
 }
 
@@ -261,6 +259,7 @@ pub async fn hrandfield(conn: &Connection, args: &[Bytes]) -> Result<Value, Erro
 /// Sets field in the hash stored at key to value. If key does not exist, a new key holding a hash
 /// is created. If field already exists in the hash, it is overwritten.
 pub async fn hset(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
+    let is_hmset = check_arg!(args, 0, "HMSET");
     if args.len() % 2 == 1 {
         return Err(Error::InvalidArgsCount("hset".to_owned()));
     }
@@ -275,7 +274,11 @@ pub async fn hset(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
                         e += 1;
                     }
                 }
-                Ok(e.into())
+                if is_hmset {
+                    Ok(Value::Ok)
+                } else {
+                    Ok(e.into())
+                }
             }
             _ => Err(Error::WrongType),
         },
@@ -287,7 +290,11 @@ pub async fn hset(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
             }
             let len = h.len();
             conn.db().set(&args[1], h.into(), None);
-            Ok(len.into())
+            if is_hmset {
+                Ok(Value::Ok)
+            } else {
+                Ok(len.into())
+            }
         },
     )?;
 
@@ -340,11 +347,12 @@ pub async fn hstrlen(conn: &Connection, args: &[Bytes]) -> Result<Value, Error>
     conn.db().get_map_or(
         &args[1],
         |v| match v {
-            Value::Hash(h) => Ok(if let Some(v) = h.read().get(&args[2]) {
-                v.len().into()
-            } else {
-                0.into()
-            }),
+            Value::Hash(h) => Ok(h
+                .read()
+                .get(&args[2])
+                .map(|v| v.len())
+                .unwrap_or_default()
+                .into()),
             _ => Err(Error::WrongType),
         },
         || Ok(0.into()),

+ 62 - 71
src/cmd/key.rs

@@ -3,9 +3,11 @@ use super::now;
 use crate::{
     check_arg,
     connection::Connection,
-    db::scan::Scan,
+    db::{scan::Scan, utils::ExpirationOpts},
     error::Error,
-    value::{bytes_to_number, cursor::Cursor, typ::Typ, Value},
+    value::{
+        bytes_to_int, bytes_to_number, cursor::Cursor, expiration::Expiration, typ::Typ, Value,
+    },
 };
 use bytes::Bytes;
 use std::{
@@ -85,22 +87,17 @@ pub async fn exists(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// The timeout can also be cleared, turning the key back into a persistent key, using the PERSIST
 /// command.
 pub async fn expire(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    let expires_in: i64 = bytes_to_number(&args[2])?;
+    let is_milliseconds = check_arg!(args, 0, "PEXPIRE");
 
-    if expires_in <= 0 {
+    let expires_at = Expiration::new(&args[2], is_milliseconds, false, &args[0])?;
+
+    if expires_at.is_negative {
         // Delete key right away
         return Ok(conn.db().del(&args[1..2]));
     }
 
-    let expires_in: u64 = expires_in as u64;
-
-    let expires_at = if check_arg!(args, 0, "EXPIRE") {
-        Duration::from_secs(expires_in)
-    } else {
-        Duration::from_millis(expires_in)
-    };
-
-    Ok(conn.db().set_ttl(&args[1], expires_at))
+    conn.db()
+        .set_ttl(&args[1], expires_at.try_into()?, (&args[3..]).try_into()?)
 }
 
 /// Returns the string representation of the type of the value stored at key.
@@ -114,28 +111,16 @@ pub async fn data_type(conn: &Connection, args: &[Bytes]) -> Result<Value, Error
 /// seconds representing the TTL (time to live), it takes an absolute Unix timestamp (seconds since
 /// January 1, 1970). A timestamp in the past will delete the key immediately.
 pub async fn expire_at(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    let secs = check_arg!(args, 0, "EXPIREAT");
-    let expires_at: i64 = bytes_to_number(&args[2])?;
-    let expires_in: i64 = if secs {
-        expires_at - now().as_secs() as i64
-    } else {
-        expires_at - now().as_millis() as i64
-    };
+    let is_milliseconds = check_arg!(args, 0, "PEXPIREAT");
+    let expires_at = Expiration::new(&args[2], is_milliseconds, true, &args[0])?;
 
-    if expires_in <= 0 {
+    if expires_at.is_negative {
         // Delete key right away
         return Ok(conn.db().del(&args[1..2]));
     }
 
-    let expires_in: u64 = expires_in as u64;
-
-    let expires_at = if secs {
-        Duration::from_secs(expires_in)
-    } else {
-        Duration::from_millis(expires_in)
-    };
-
-    Ok(conn.db().set_ttl(&args[1], expires_at))
+    conn.db()
+        .set_ttl(&args[1], expires_at.try_into()?, (&args[3..]).try_into()?)
 }
 
 /// Returns the absolute Unix timestamp (since January 1, 1970) in seconds at which the given key
@@ -146,10 +131,10 @@ pub async fn expire_time(conn: &Connection, args: &[Bytes]) -> Result<Value, Err
             // Is there a better way? There should be!
             if check_arg!(args, 0, "EXPIRETIME") {
                 let secs: i64 = (ttl - Instant::now()).as_secs() as i64;
-                secs + (now().as_secs() as i64)
+                secs + 1 + (now().as_secs() as i64)
             } else {
                 let secs: i64 = (ttl - Instant::now()).as_millis() as i64;
-                secs + (now().as_millis() as i64)
+                secs + 1 + (now().as_millis() as i64)
             }
         }
         Some(None) => -1,
@@ -228,40 +213,29 @@ pub async fn rename(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 pub async fn scan(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
     let cursor: Cursor = (&args[1]).try_into()?;
     let mut current = 2;
-    let pattern = if check_arg!(args, current, "MATCH") {
-        current += 2;
-        Some(
-            args.get(current - 1)
-                .ok_or_else(|| Error::InvalidArgsCount("SCAN".to_owned()))?,
-        )
-    } else {
-        None
-    };
-    let count = if check_arg!(args, current, "COUNT") {
-        current += 2;
-        let number: usize = bytes_to_number(
-            args.get(current - 1)
-                .ok_or_else(|| Error::InvalidArgsCount("SCAN".to_owned()))?,
-        )?;
-        Some(number)
-    } else {
-        None
-    };
-    let typ = if check_arg!(args, current, "TYPE") {
-        current += 2;
-        Some(
-            Typ::from_str(&String::from_utf8_lossy(
-                args.get(current - 1)
-                    .ok_or_else(|| Error::InvalidArgsCount("SCAN".to_owned()))?,
-            ))
-            .map_err(|_| Error::Syntax)?,
-        )
-    } else {
-        None
-    };
-
-    if current != args.len() {
-        return Err(Error::Syntax);
+    let mut pattern = None;
+    let mut count = None;
+    let mut typ = None;
+
+    for i in (2..args.len()).step_by(2) {
+        let value = args
+            .get(i + 1)
+            .ok_or(Error::InvalidArgsCount("SCAN".to_owned()))?;
+        match String::from_utf8_lossy(&args[i]).to_uppercase().as_str() {
+            "MATCH" => pattern = Some(value),
+            "COUNT" => {
+                count = Some(
+                    bytes_to_number(value)
+                        .map_err(|_| Error::InvalidArgsCount("SCAN".to_owned()))?,
+                )
+            }
+            "TYPE" => {
+                typ = Some(
+                    Typ::from_str(&String::from_utf8_lossy(&value)).map_err(|_| Error::Syntax)?,
+                )
+            }
+            _ => return Err(Error::Syntax),
+        }
     }
 
     Ok(conn.db().scan(cursor, pattern, count, typ)?.into())
@@ -275,7 +249,7 @@ pub async fn ttl(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
         Some(Some(ttl)) => {
             let ttl = ttl - Instant::now();
             if check_arg!(args, 0, "TTL") {
-                ttl.as_secs() as i64
+                ttl.as_secs() as i64 + 1
             } else {
                 ttl.as_millis() as i64
             }
@@ -385,6 +359,19 @@ mod test {
     }
 
     #[tokio::test]
+    async fn expire2() {
+        let c = create_connection();
+        assert_eq!(
+            Ok(Value::Integer(1)),
+            run_command(&c, &["incr", "foo"]).await
+        );
+        assert_eq!(
+            Err(Error::InvalidExpire("pexpire".to_owned())),
+            run_command(&c, &["pexpire", "foo", "9223372036854770000"]).await
+        );
+    }
+
+    #[tokio::test]
     async fn copy() {
         let c = create_connection();
         assert_eq!(Ok(1.into()), run_command(&c, &["incr", "foo"]).await);
@@ -500,13 +487,13 @@ mod test {
     #[tokio::test]
     async fn renamenx() {
         let c = create_connection();
-        assert_eq!(Ok(1.into()), run_command(&c, &["incr", "foo"]).await);
+        assert_eq!(Ok(1i64.into()), run_command(&c, &["incr", "foo"]).await);
         assert_eq!(
-            Ok(1.into()),
+            Ok(1i64.into()),
             run_command(&c, &["renamenx", "foo", "bar-1650"]).await
         );
         assert_eq!(
-            Ok(1.into()),
+            Ok(1i64.into()),
             run_command(&c, &["renamenx", "bar-1650", "xxx"]).await
         );
         assert_eq!(
@@ -519,9 +506,13 @@ mod test {
             run_command(&c, &["set", "bar-1650", "xxx"]).await
         );
         assert_eq!(
-            Ok(0.into()),
+            Ok(0i64.into()),
             run_command(&c, &["renamenx", "xxx", "bar-1650"]).await
         );
+        assert_eq!(
+            Ok(Value::Blob("1".into())),
+            run_command(&c, &["get", "xxx"]).await
+        );
     }
 
     #[tokio::test]

+ 355 - 88
src/cmd/list.rs

@@ -1,8 +1,10 @@
 //! # List command handlers
 use crate::{
     check_arg,
-    connection::{Connection, ConnectionStatus},
+    connection::{Connection, ConnectionStatus, UnblockReason},
+    db::utils::far_future,
     error::Error,
+    try_get_arg, try_get_arg_str,
     value::bytes_to_number,
     value::checksum,
     value::Value,
@@ -15,45 +17,55 @@ use tokio::time::{sleep, Duration, Instant};
 fn remove_element(
     conn: &Connection,
     key: &Bytes,
-    count: usize,
+    limit: Option<usize>,
     front: bool,
 ) -> Result<Value, Error> {
-    let result = conn.db().get_map_or(
+    let db = conn.db();
+    let mut new_len = 0;
+    let result = db.get_map_or(
         key,
         |v| match v {
             Value::List(x) => {
                 let mut x = x.write();
 
-                if count == 0 {
+                let limit = if let Some(limit) = limit {
+                    limit
+                } else {
                     // Return a single element
-                    return Ok((if front { x.pop_front() } else { x.pop_back() })
+                    let ret = Ok((if front { x.pop_front() } else { x.pop_back() })
                         .map_or(Value::Null, |x| x.clone_value()));
-                }
+                    new_len = x.len();
+                    return ret;
+                };
 
-                let mut ret = vec![None; count];
+                let mut ret = vec![None; limit];
 
-                for i in 0..count {
+                for i in 0..limit {
                     if front {
                         ret[i] = x.pop_front();
                     } else {
                         ret[i] = x.pop_back();
                     }
                 }
-
-                let ret: Vec<Value> = ret.iter().flatten().map(|m| m.clone_value()).collect();
-
-                Ok(if ret.is_empty() {
-                    Value::Null
-                } else {
-                    ret.into()
-                })
+                new_len = x.len();
+
+                Ok(ret
+                    .iter()
+                    .flatten()
+                    .map(|m| m.clone_value())
+                    .collect::<Vec<Value>>()
+                    .into())
             }
             _ => Err(Error::WrongType),
         },
         || Ok(Value::Null),
     )?;
 
-    conn.db().bump_version(key);
+    if new_len == 0 {
+        let _ = db.del(&[key.clone()]);
+    } else {
+        db.bump_version(key);
+    }
 
     Ok(result)
 }
@@ -63,49 +75,97 @@ fn remove_element(
 /// popped from the head of the first list that is non-empty, with the given keys being checked in
 /// the order that they are given.
 pub async fn blpop(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    let timeout =
-        Instant::now() + Duration::from_secs(bytes_to_number::<u64>(&args[args.len() - 1])?);
+    let timeout = parse_timeout(&args[args.len() - 1])?;
     let len = args.len() - 1;
 
+    conn.block();
+
     loop {
         for key in args[1..len].iter() {
-            match remove_element(conn, key, 0, true)? {
+            match remove_element(conn, key, None, true)? {
                 Value::Null => (),
                 n => return Ok(vec![Value::new(&key), n].into()),
             };
         }
 
-        if Instant::now() >= timeout || conn.status() == ConnectionStatus::ExecutingTx {
+        if let Some(timeout) = timeout {
+            if Instant::now() >= timeout {
+                conn.unblock(UnblockReason::Timeout);
+                break;
+            }
+        }
+
+        if conn.status() == ConnectionStatus::ExecutingTx {
+            conn.unblock(UnblockReason::Timeout);
             break;
         }
 
+        if let Some(reason) = conn.is_unblocked() {
+            return match reason {
+                UnblockReason::Error => Err(Error::UnblockByError),
+                UnblockReason::Timeout => Ok(Value::Null),
+            };
+        }
+
         sleep(Duration::from_millis(100)).await;
     }
 
     Ok(Value::Null)
 }
 
+fn parse_timeout(arg: &Bytes) -> Result<Option<Instant>, Error> {
+    let raw_timeout = bytes_to_number::<f64>(arg)?;
+    if raw_timeout < 0f64 {
+        return Err(Error::NegativeNumber("timeout".to_owned()));
+    }
+
+    if raw_timeout == 0.0 {
+        return Ok(None);
+    }
+
+    Ok(Some(
+        Instant::now()
+            .checked_add(Duration::from_micros((raw_timeout * 1000f64).round() as u64))
+            .unwrap_or_else(far_future),
+    ))
+}
+
 /// BRPOP is a blocking list pop primitive. It is the blocking version of RPOP because it blocks
 /// the connection when there are no elements to pop from any of the given lists. An element is
 /// popped from the tail of the first list that is non-empty, with the given keys being checked in
 /// the order that they are given.
 pub async fn brpop(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    let timeout =
-        Instant::now() + Duration::from_secs(bytes_to_number::<u64>(&args[args.len() - 1])?);
+    let timeout = parse_timeout(&args[args.len() - 1])?;
     let len = args.len() - 1;
 
+    conn.block();
+
     loop {
         for key in args[1..len].iter() {
-            match remove_element(conn, key, 0, false)? {
+            match remove_element(conn, key, None, false)? {
                 Value::Null => (),
                 n => return Ok(vec![Value::new(&key), n].into()),
             };
         }
 
-        if Instant::now() >= timeout || conn.status() == ConnectionStatus::ExecutingTx {
+        if let Some(timeout) = timeout {
+            if Instant::now() >= timeout {
+                conn.unblock(UnblockReason::Timeout);
+                break;
+            }
+        }
+        if conn.status() == ConnectionStatus::ExecutingTx {
+            conn.unblock(UnblockReason::Timeout);
             break;
         }
 
+        if let Some(reason) = conn.is_unblocked() {
+            return match reason {
+                UnblockReason::Error => Err(Error::UnblockByError),
+                UnblockReason::Timeout => Ok(Value::Null),
+            };
+        }
+
         sleep(Duration::from_millis(100)).await;
     }
 
@@ -124,16 +184,19 @@ pub async fn lindex(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
                 let mut index: i64 = bytes_to_number(&args[2])?;
                 let x = x.read();
 
-                if index < 0 {
-                    index += x.len() as i64;
-                }
+                let index = if index < 0 {
+                    x.len()
+                        .checked_sub((index * -1) as usize)
+                        .unwrap_or(x.len())
+                } else {
+                    index as usize
+                };
 
-                Ok(x.get(index as usize)
-                    .map_or(Value::Null, |x| x.clone_value()))
+                Ok(x.get(index).map_or(Value::Null, |x| x.clone_value()))
             }
             _ => Err(Error::WrongType),
         },
-        || Ok(0.into()),
+        || Ok(Value::Null),
     )
 }
 
@@ -284,10 +347,9 @@ pub async fn lmove(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// with the optional count argument, the reply will consist of up to count elements, depending on
 /// the list's length.
 pub async fn lpop(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    let count = if args.len() > 2 {
-        bytes_to_number(&args[2])?
-    } else {
-        0
+    let count = match args.get(2) {
+        Some(v) => Some(bytes_to_number(&v)?),
+        None => None,
     };
 
     remove_element(conn, &args[1], count, true)
@@ -298,63 +360,113 @@ pub async fn lpop(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// "element". If the element is found, its index (the zero-based position in the list) is
 /// returned. Otherwise, if no match is found, nil is returned.
 pub async fn lpos(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    let mut index = 3;
     let element = checksum::Ref::new(&args[2]);
-    let rank = if check_arg!(args, index, "RANK") {
-        index += 2;
-        Some(bytes_to_number::<usize>(&args[index - 1])?)
-    } else {
-        None
-    };
-    let count = if check_arg!(args, index, "COUNT") {
-        index += 2;
-        Some(bytes_to_number::<usize>(&args[index - 1])?)
-    } else {
-        None
-    };
-    let max_len = if check_arg!(args, index, "MAXLEN") {
+    let mut rank = None;
+    let mut count = None;
+    let mut max_len = None;
+
+    let mut index = 3;
+    loop {
+        if args.len() <= index {
+            break;
+        }
+
+        let next = try_get_arg!(args, index + 1);
+        match try_get_arg_str!(args, index).to_uppercase().as_str() {
+            "RANK" => rank = Some(bytes_to_number::<i64>(&next)?),
+            "COUNT" => count = Some(bytes_to_number::<usize>(&next)?),
+            "MAXLEN" => max_len = Some(bytes_to_number::<usize>(&next)?),
+            _ => return Err(Error::Syntax),
+        }
+
         index += 2;
-        bytes_to_number::<i64>(&args[index - 1])?
+    }
+
+    let (must_reverse, rank) = if let Some(rank) = rank {
+        if rank == 0 {
+            return Err(Error::InvalidRank("RANK".to_owned()));
+        }
+        if rank < 0 {
+            (true, Some((rank * -1) as usize))
+        } else {
+            (false, Some(rank as usize))
+        }
     } else {
-        -1
+        (false, None)
     };
 
-    if index != args.len() {
-        return Err(Error::Syntax);
-    }
+    let max_len = max_len.unwrap_or_default();
 
     conn.db().get_map_or(
         &args[1],
         |v| match v {
             Value::List(x) => {
                 let x = x.read();
-                let mut ret: Vec<Value> = vec![];
+                let mut result: Vec<Value> = vec![];
+
+                let mut values = x
+                    .iter()
+                    .enumerate()
+                    .collect::<Vec<(usize, &checksum::Value)>>();
+
+                if must_reverse {
+                    values.reverse();
+                }
 
-                for (i, val) in x.iter().enumerate() {
-                    if *val == element {
+                let mut checks = 1;
+
+                for (id, val) in values.iter() {
+                    if **val == element {
                         // Match!
                         if let Some(count) = count {
-                            ret.push(i.into());
-                            if ret.len() > count {
-                                return Ok(ret.into());
+                            result.push((*id).into());
+                            if result.len() == count && count != 0 && rank.is_none() {
+                                // There is no point in keep looping. No RANK provided, COUNT is not 0
+                                // therefore we can return the vector of result as IS
+                                return Ok(result.into());
                             }
                         } else if let Some(rank) = rank {
-                            ret.push(i.into());
-                            if ret.len() == rank {
-                                return Ok(ret[rank - 1].clone());
+                            result.push((*id).into());
+                            if result.len() == rank {
+                                return Ok((*id).into());
                             }
                         } else {
                             // return first match!
-                            return Ok(i.into());
+                            return Ok((*id).into());
                         }
                     }
-                    if (i as i64) == max_len {
+                    if checks == max_len {
                         break;
                     }
+                    checks += 1;
+                }
+
+                if let Some(rank) = rank {
+                    let rank = rank - 1;
+
+                    let result = if rank < result.len() {
+                        (&result[rank..]).to_vec()
+                    } else {
+                        vec![]
+                    };
+
+                    return Ok(if let Some(count) = count {
+                        if count > 0 && count < result.len() {
+                            (&result[0..count]).to_vec().into()
+                        } else {
+                            result.to_vec().into()
+                        }
+                    } else {
+                        result
+                            .to_vec()
+                            .get(0)
+                            .map(|c| c.clone())
+                            .unwrap_or_default()
+                    });
                 }
 
                 if count.is_some() {
-                    Ok(ret.into())
+                    Ok(result.into())
                 } else {
                     Ok(Value::Null)
                 }
@@ -427,23 +539,34 @@ pub async fn lrange(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
         &args[1],
         |v| match v {
             Value::List(x) => {
-                let mut start: i64 = bytes_to_number(&args[2])?;
-                let mut end: i64 = bytes_to_number(&args[3])?;
+                let start: i64 = bytes_to_number(&args[2])?;
+                let end: i64 = bytes_to_number(&args[3])?;
                 let mut ret = vec![];
                 let x = x.read();
 
-                if start < 0 {
-                    start += x.len() as i64;
-                }
+                let start = if start < 0 {
+                    x.len()
+                        .checked_sub((start * -1) as usize)
+                        .unwrap_or_default()
+                } else {
+                    (start as usize)
+                };
 
-                if end < 0 {
-                    end += x.len() as i64;
-                }
+                let end = if end < 0 {
+                    if let Some(x) = x.len().checked_sub((end * -1) as usize) {
+                        x
+                    } else {
+                        return Ok(Value::Array((vec![])));
+                    }
+                } else {
+                    end as usize
+                };
 
-                for (i, val) in x.iter().enumerate() {
-                    if i >= start as usize && i <= end as usize {
-                        ret.push(val.clone_value());
+                for (i, val) in x.iter().enumerate().skip(start) {
+                    if i > end {
+                        break;
                     }
+                    ret.push(val.clone_value());
                 }
                 Ok(ret.into())
             }
@@ -583,10 +706,9 @@ pub async fn ltrim(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// optional count argument, the reply will consist of up to count elements, depending on the
 /// list's length.
 pub async fn rpop(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    let count = if args.len() > 2 {
-        bytes_to_number(&args[2])?
-    } else {
-        0
+    let count = match args.get(2) {
+        Some(v) => Some(bytes_to_number(&v)?),
+        None => None,
     };
 
     remove_element(conn, &args[1], count, false)
@@ -684,7 +806,7 @@ mod test {
             run_command(&c, &["blpop", "foobar", "1"]).await
         );
 
-        assert!(Instant::now() - x > Duration::from_millis(1000));
+        assert!(Instant::now() - x <= Duration::from_millis(1000));
     }
 
     #[tokio::test]
@@ -864,7 +986,7 @@ mod test {
             run_command(&c, &["brpop", "foobar", "1"]).await
         );
 
-        assert!(Instant::now() - x > Duration::from_millis(1000));
+        assert!(Instant::now() - x < Duration::from_millis(1000));
     }
 
     #[tokio::test]
@@ -1162,6 +1284,104 @@ mod test {
     }
 
     #[tokio::test]
+    async fn lpos_with_negative_rank_with_count() {
+        let c = create_connection();
+        assert_eq!(
+            Ok(Value::Integer(8)),
+            run_command(
+                &c,
+                &["RPUSH", "mylist", "a", "b", "c", "1", "2", "3", "c", "c"]
+            )
+            .await
+        );
+
+        assert_eq!(
+            Ok(Value::Array(vec![Value::Integer(7), Value::Integer(6)])),
+            run_command(&c, &["lpos", "mylist", "c", "count", "2", "rank", "-1"]).await
+        );
+    }
+
+    #[tokio::test]
+    async fn lpos_with_negative_rank_with_count_max_len() {
+        let c = create_connection();
+        assert_eq!(
+            Ok(Value::Integer(8)),
+            run_command(
+                &c,
+                &["RPUSH", "mylist", "a", "b", "c", "1", "2", "3", "c", "c"]
+            )
+            .await
+        );
+
+        assert_eq!(
+            Ok(Value::Array(vec![Value::Integer(7), Value::Integer(6)])),
+            run_command(
+                &c,
+                &["lpos", "mylist", "c", "count", "0", "maxlen", "3", "rank", "-1"]
+            )
+            .await
+        );
+    }
+
+    #[tokio::test]
+    async fn lpos_rank_with_count() {
+        let c = create_connection();
+        assert_eq!(
+            Ok(Value::Integer(8)),
+            run_command(
+                &c,
+                &["RPUSH", "mylist", "a", "b", "c", "1", "2", "3", "c", "c"]
+            )
+            .await
+        );
+
+        assert_eq!(
+            Ok(Value::Array(vec![Value::Integer(6), Value::Integer(7)])),
+            run_command(&c, &["lpos", "mylist", "c", "count", "0", "rank", "2"]).await
+        );
+    }
+
+    #[tokio::test]
+    async fn lpos_all_settings() {
+        let c = create_connection();
+        assert_eq!(
+            Ok(Value::Integer(8)),
+            run_command(
+                &c,
+                &["RPUSH", "mylist", "a", "b", "c", "1", "2", "3", "c", "c"]
+            )
+            .await
+        );
+
+        assert_eq!(
+            Ok(Value::Array(vec![Value::Integer(6)])),
+            run_command(
+                &c,
+                &["lpos", "mylist", "c", "count", "0", "rank", "2", "maxlen", "7"]
+            )
+            .await
+        );
+    }
+
+    #[tokio::test]
+    async fn lpos_negative_rank() {
+        let c = create_connection();
+        assert_eq!(
+            Ok(Value::Integer(8)),
+            run_command(
+                &c,
+                &["RPUSH", "mylist", "a", "b", "c", "1", "2", "3", "c", "c"]
+            )
+            .await
+        );
+
+        assert_eq!(
+            Ok(Value::Integer(7)),
+            run_command(&c, &["lpos", "mylist", "c", "rank", "-1"]).await
+        );
+    }
+
+    #[tokio::test]
     async fn lpos_single_skip() {
         let c = create_connection();
         assert_eq!(
@@ -1223,11 +1443,7 @@ mod test {
         );
 
         assert_eq!(
-            Ok(Value::Array(vec![
-                Value::Integer(6),
-                Value::Integer(8),
-                Value::Integer(9),
-            ])),
+            Ok(Value::Array(vec![Value::Integer(6), Value::Integer(8),])),
             run_command(&c, &["lpos", "mylist", "3", "count", "5", "maxlen", "9"]).await
         );
     }
@@ -1572,4 +1788,55 @@ mod test {
             run_command(&c, &["rpushx", "foobar", "6", "7", "8", "9", "10"]).await
         );
     }
+
+    #[tokio::test]
+    async fn lrange_test_1() {
+        let c = create_connection();
+
+        assert_eq!(
+            Ok(Value::Integer(10)),
+            run_command(
+                &c,
+                &[
+                    "rpush",
+                    "mylist",
+                    "largevalue",
+                    "1",
+                    "2",
+                    "3",
+                    "4",
+                    "5",
+                    "6",
+                    "7",
+                    "8",
+                    "9"
+                ]
+            )
+            .await
+        );
+
+        assert_eq!(
+            Ok(Value::Array(vec![
+                "1".into(),
+                "2".into(),
+                "3".into(),
+                "4".into(),
+                "5".into(),
+                "6".into(),
+                "7".into(),
+                "8".into()
+            ])),
+            run_command(&c, &["lrange", "mylist", "1", "-2"]).await
+        );
+
+        assert_eq!(
+            Ok(Value::Array(vec!["7".into(), "8".into(), "9".into()])),
+            run_command(&c, &["lrange", "mylist", "-3", "-1"]).await
+        );
+
+        assert_eq!(
+            Ok(Value::Array(vec!["4".into()])),
+            run_command(&c, &["lrange", "mylist", "4", "4"]).await
+        );
+    }
 }

+ 26 - 11
src/cmd/pubsub.rs

@@ -64,7 +64,9 @@ pub async fn punsubscribe(conn: &Connection, args: &[Bytes]) -> Result<Value, Er
             .collect::<Result<Vec<Pattern>, Error>>()?
     };
 
-    Ok(conn.pubsub_client().punsubscribe(&channels, conn).into())
+    let _ = conn.pubsub_client().punsubscribe(&channels, conn);
+
+    Ok(Value::Ignore)
 }
 
 /// Unsubscribes the client from the given channels, or from all of them if none is given.
@@ -75,7 +77,8 @@ pub async fn unsubscribe(conn: &Connection, args: &[Bytes]) -> Result<Value, Err
         (&args[1..]).to_vec()
     };
 
-    Ok(conn.pubsub_client().unsubscribe(&channels, conn).into())
+    let _ = conn.pubsub_client().unsubscribe(&channels, conn);
+    Ok(Value::Ignore)
 }
 
 #[cfg(test)]
@@ -117,7 +120,7 @@ mod test {
         let (mut recv, c1) = create_connection_and_pubsub();
 
         assert_eq!(
-            Ok(Value::Ok),
+            Ok(Value::Ignore),
             run_command(&c1, &["subscribe", "foo", "bar"]).await
         );
 
@@ -144,9 +147,15 @@ mod test {
     async fn test_subscribe_multiple_channels_one_by_one() {
         let (mut recv, c1) = create_connection_and_pubsub();
 
-        assert_eq!(Ok(Value::Ok), run_command(&c1, &["subscribe", "foo"]).await);
+        assert_eq!(
+            Ok(Value::Ignore),
+            run_command(&c1, &["subscribe", "foo"]).await
+        );
 
-        assert_eq!(Ok(Value::Ok), run_command(&c1, &["subscribe", "bar"]).await);
+        assert_eq!(
+            Ok(Value::Ignore),
+            run_command(&c1, &["subscribe", "bar"]).await
+        );
 
         assert_eq!(
             Some(Value::Array(vec![
@@ -172,12 +181,12 @@ mod test {
         let (mut recv, c1) = create_connection_and_pubsub();
 
         assert_eq!(
-            Ok(Value::Ok),
+            Ok(Value::Ignore),
             run_command(&c1, &["subscribe", "foo", "bar"]).await
         );
 
         assert_eq!(
-            Ok(Value::Integer(2)),
+            Ok(Value::Ignore),
             run_command(&c1, &["unsubscribe", "foo", "bar"]).await
         );
 
@@ -212,7 +221,7 @@ mod test {
             Some(Value::Array(vec![
                 "unsubscribe".into(),
                 "bar".into(),
-                1.into()
+                0.into()
             ])),
             recv.recv().await
         );
@@ -224,8 +233,14 @@ mod test {
         let (mut sub2, c2) = create_new_connection_from_connection(&c1);
         let (_, c3) = create_new_connection_from_connection(&c1);
 
-        assert_eq!(Ok(Value::Ok), run_command(&c1, &["subscribe", "foo"]).await);
-        assert_eq!(Ok(Value::Ok), run_command(&c2, &["subscribe", "foo"]).await);
+        assert_eq!(
+            Ok(Value::Ignore),
+            run_command(&c1, &["subscribe", "foo"]).await
+        );
+        assert_eq!(
+            Ok(Value::Ignore),
+            run_command(&c2, &["subscribe", "foo"]).await
+        );
 
         let msg = "foo - message";
 
@@ -248,7 +263,7 @@ mod test {
         let _ = run_command(&c2, &["psubscribe", "foo", "bar*", "xxx*"]).await;
 
         assert_eq!(
-            Ok(Value::Integer(1)),
+            Ok(Value::Integer(3)),
             run_command(&c1, &["pubsub", "numpat"]).await
         );
     }

+ 61 - 4
src/cmd/server.rs

@@ -1,11 +1,15 @@
 //! # Server command handlers
 use crate::{
-    check_arg, connection::Connection, error::Error, value::bytes_to_number, value::Value,
+    check_arg, connection::Connection, error::Error, try_get_arg, value::bytes_to_number,
+    value::Value,
 };
 use bytes::Bytes;
-use std::time::SystemTime;
-use std::time::UNIX_EPOCH;
-use std::{convert::TryInto, ops::Neg};
+use git_version::git_version;
+use std::{
+    convert::TryInto,
+    ops::Neg,
+    time::{SystemTime, UNIX_EPOCH},
+};
 use tokio::time::Duration;
 
 /// Returns Array reply of details about all Redis commands.
@@ -62,6 +66,53 @@ pub async fn command(conn: &Connection, args: &[Bytes]) -> Result<Value, Error>
     }
 }
 
+/// The DEBUG command is an internal command. It is meant to be used for
+/// developing and testing Redis.
+pub async fn debug(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
+    match String::from_utf8_lossy(&args[1]).to_lowercase().as_str() {
+        "object" => Ok(conn.db().debug(try_get_arg!(args, 2))?.into()),
+        "set-active-expire" => Ok(Value::Ok),
+        "digest-value" => Ok(Value::Array(conn.db().digest(&args[2..])?)),
+        _ => Err(Error::Syntax),
+    }
+}
+
+/// The INFO command returns information and statistics about the server in a
+/// format that is simple to parse by computers and easy to read by humans.
+pub async fn info(_: &Connection, _: &[Bytes]) -> Result<Value, Error> {
+    Ok(Value::Blob(
+        format!(
+            "redis_version: {}\r\nredis_git_sha1:{}\r\n",
+            git_version!(),
+            git_version!()
+        )
+        .as_str()
+        .into(),
+    ))
+}
+
+/// Delete all the keys of the currently selected DB. This command never fails.
+pub async fn flushdb(conn: &Connection, _: &[Bytes]) -> Result<Value, Error> {
+    conn.db().flushdb()
+}
+
+/// Delete all the keys of all the existing databases, not just the currently
+/// selected one. This command never fails.
+pub async fn flushall(conn: &Connection, _: &[Bytes]) -> Result<Value, Error> {
+    conn.all_connections()
+        .get_databases()
+        .into_iter()
+        .map(|db| db.flushdb())
+        .for_each(drop);
+
+    Ok(Value::Ok)
+}
+
+/// Return the number of keys in the currently-selected database.
+pub async fn dbsize(conn: &Connection, _: &[Bytes]) -> Result<Value, Error> {
+    conn.db().len().map(|s| s.into())
+}
+
 /// The TIME command returns the current server time as a two items lists: a
 /// Unix timestamp and the amount of microseconds already elapsed in the current
 /// second. Basically the interface is very similar to the one of the
@@ -74,3 +125,9 @@ pub async fn time(_conn: &Connection, _args: &[Bytes]) -> Result<Value, Error> {
 
     Ok(vec![seconds.as_str(), millis.as_str()].into())
 }
+
+/// Ask the server to close the connection. The connection is closed as soon as
+/// all pending replies have been written to the client.
+pub async fn quit(_: &Connection, _: &[Bytes]) -> Result<Value, Error> {
+    Err(Error::Quit)
+}

+ 174 - 45
src/cmd/set.rs

@@ -32,10 +32,12 @@ where
                 let mut all_entries = x.read().clone();
                 for key in keys[1..].iter() {
                     let mut do_break = false;
+                    let mut found = false;
                     let _ = conn.db().get_map_or(
                         key,
                         |v| match v {
                             Value::Set(x) => {
+                                found = true;
                                 if !op(&mut all_entries, &x.read()) {
                                     do_break = true;
                                 }
@@ -45,6 +47,9 @@ where
                         },
                         || Ok(Value::Null),
                     )?;
+                    if !found && !op(&mut all_entries, &HashSet::new()) {
+                        break;
+                    }
                     if do_break {
                         break;
                     }
@@ -58,7 +63,35 @@ where
             }
             _ => Err(Error::WrongType),
         },
-        || Ok(Value::Array(vec![])),
+        || {
+            #[allow(clippy::mutable_key_type)]
+            let mut all_entries: HashSet<Bytes> = HashSet::new();
+            for key in keys[1..].iter() {
+                let mut do_break = false;
+                let _ = conn.db().get_map_or(
+                    key,
+                    |v| match v {
+                        Value::Set(x) => {
+                            if !op(&mut all_entries, &x.read()) {
+                                do_break = true;
+                            }
+                            Ok(Value::Null)
+                        }
+                        _ => Err(Error::WrongType),
+                    },
+                    || Ok(Value::Null),
+                )?;
+                if do_break {
+                    break;
+                }
+            }
+
+            Ok(all_entries
+                .iter()
+                .map(|entry| Value::new(&entry))
+                .collect::<Vec<Value>>()
+                .into())
+        },
     )
 }
 
@@ -140,7 +173,12 @@ pub async fn sdiff(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// If destination already exists, it is overwritten.
 pub async fn sdiffstore(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
     if let Value::Array(values) = sdiff(conn, &args[1..]).await? {
-        Ok(store(conn, &args[1], &values).into())
+        if values.len() > 0 {
+            Ok(store(conn, &args[1], &values).into())
+        } else {
+            let _ = conn.db().del(&[args[1].clone()]);
+            Ok(0.into())
+        }
     } else {
         Ok(0.into())
     }
@@ -187,7 +225,12 @@ pub async fn sintercard(conn: &Connection, args: &[Bytes]) -> Result<Value, Erro
 /// If destination already exists, it is overwritten.
 pub async fn sinterstore(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
     if let Value::Array(values) = sinter(conn, &args[1..]).await? {
-        Ok(store(conn, &args[1], &values).into())
+        if values.len() > 0 {
+            Ok(store(conn, &args[1], &values).into())
+        } else {
+            let _ = conn.db().del(&[args[1].clone()]);
+            Ok(0.into())
+        }
     } else {
         Ok(0.into())
     }
@@ -248,7 +291,13 @@ pub async fn smismember(conn: &Connection, args: &[Bytes]) -> Result<Value, Erro
             }
             _ => Err(Error::WrongType),
         },
-        || Ok(0.into()),
+        || {
+            Ok((&args[2..])
+                .iter()
+                .map(|_| 0.into())
+                .collect::<Vec<Value>>()
+                .into())
+        },
     )
 }
 
@@ -264,35 +313,38 @@ pub async fn smove(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
     let result = conn.db().get_map_or(
         &args[1],
         |v| match v {
-            Value::Set(set1) => {
-                if !set1.read().contains(&args[3]) {
-                    return Ok(0.into());
-                }
+            Value::Set(set1) => conn.db().get_map_or(
+                &args[2],
+                |v| match v {
+                    Value::Set(set2) => {
+                        let mut set1 = set1.write();
+                        if !set1.contains(&args[3]) {
+                            return Ok(0.into());
+                        }
 
-                conn.db().get_map_or(
-                    &args[2],
-                    |v| match v {
-                        Value::Set(set2) => {
-                            let mut set2 = set2.write();
-                            set1.write().remove(&args[3]);
-                            if set2.insert(args[3].clone()) {
-                                Ok(1.into())
-                            } else {
-                                Ok(0.into())
-                            }
+                        if args[1] == args[2] {
+                            return Ok(1.into());
                         }
-                        _ => Err(Error::WrongType),
-                    },
-                    || {
-                        set1.write().remove(&args[3]);
-                        #[allow(clippy::mutable_key_type)]
-                        let mut x = HashSet::new();
-                        x.insert(args[3].clone());
-                        conn.db().set(&args[2], x.into(), None);
-                        Ok(1.into())
-                    },
-                )
-            }
+
+                        let mut set2 = set2.write();
+                        set1.remove(&args[3]);
+                        if set2.insert(args[3].clone()) {
+                            Ok(1.into())
+                        } else {
+                            Ok(0.into())
+                        }
+                    }
+                    _ => Err(Error::WrongType),
+                },
+                || {
+                    set1.write().remove(&args[3]);
+                    #[allow(clippy::mutable_key_type)]
+                    let mut x = HashSet::new();
+                    x.insert(args[3].clone());
+                    conn.db().set(&args[2], x.into(), None);
+                    Ok(1.into())
+                },
+            ),
             _ => Err(Error::WrongType),
         },
         || Ok(0.into()),
@@ -314,6 +366,7 @@ pub async fn smove(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// cardinality.
 pub async fn spop(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
     let rand = srandmember(conn, args).await?;
+    let mut should_remove = false;
     let result = conn.db().get_map_or(
         &args[1],
         |v| match v {
@@ -321,25 +374,31 @@ pub async fn spop(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
                 let mut x = x.write();
                 match &rand {
                     Value::Blob(value) => {
-                        x.remove(value.as_ref().into());
+                        x.remove(value.as_ref());
                     }
                     Value::Array(values) => {
                         for value in values.iter() {
                             if let Value::Blob(value) = value {
-                                x.remove(value.as_ref().into());
+                                x.remove(value.as_ref());
                             }
                         }
                     }
                     _ => unreachable!(),
                 };
+
+                should_remove = x.is_empty();
                 Ok(rand)
             }
             _ => Err(Error::WrongType),
         },
-        || Ok(0.into()),
+        || Ok(Value::Null),
     )?;
 
-    conn.db().bump_version(&args[1]);
+    if should_remove {
+        let _ = conn.db().del(&[args[1].clone()]);
+    } else {
+        conn.db().bump_version(&args[1]);
+    }
 
     Ok(result)
 }
@@ -369,20 +428,56 @@ pub async fn srandmember(conn: &Connection, args: &[Bytes]) -> Result<Value, Err
                 items.sort_by(|a, b| a.1.cmp(&b.1));
 
                 if args.len() == 2 {
-                    let item = items[0].0.clone();
-                    Ok(Value::new(&item))
+                    // Two arguments provided, return the first element or null if the array is null
+                    if items.is_empty() {
+                        Ok(Value::Null)
+                    } else {
+                        let item = items[0].0.clone();
+                        Ok(Value::new(&item))
+                    }
                 } else {
-                    let len: usize = min(items.len(), bytes_to_number(&args[2])?);
-                    Ok(items[0..len]
-                        .iter()
-                        .map(|item| Value::new(&item.0))
-                        .collect::<Vec<Value>>()
-                        .into())
+                    if items.is_empty() {
+                        return Ok(Value::Array(vec![]));
+                    }
+                    let len = bytes_to_number::<i64>(&args[2])?;
+
+                    if len > 0 {
+                        // required length is positive, return *up* to the requested number and no duplicated allowed
+                        let len: usize = min(items.len(), len as usize);
+                        Ok(items[0..len]
+                            .iter()
+                            .map(|item| Value::new(&item.0))
+                            .collect::<Vec<Value>>()
+                            .into())
+                    } else {
+                        // duplicated results are allowed and the requested number must be returned
+                        let len = (len * -1) as usize;
+                        let total = items.len() - 1;
+                        let mut i = 0;
+                        let items = (0..len)
+                            .map(|_| {
+                                let r = (items[i].0, rng.gen());
+                                i = if i >= total { 0 } else { i + 1 };
+                                r
+                            })
+                            .collect::<Vec<(&Bytes, i128)>>();
+                        Ok(items
+                            .iter()
+                            .map(|item| Value::new(&item.0))
+                            .collect::<Vec<Value>>()
+                            .into())
+                    }
                 }
             }
             _ => Err(Error::WrongType),
         },
-        || Ok(0.into()),
+        || {
+            Ok(if args.len() == 2 {
+                Value::Null
+            } else {
+                Value::Array(vec![])
+            })
+        },
     )
 }
 
@@ -433,7 +528,12 @@ pub async fn sunion(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// If destination already exists, it is overwritten.
 pub async fn sunionstore(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
     if let Value::Array(values) = sunion(conn, &args[1..]).await? {
-        Ok(store(conn, &args[1], &values).into())
+        if values.len() > 0 {
+            Ok(store(conn, &args[1], &values).into())
+        } else {
+            let _ = conn.db().del(&[args[1].clone()]);
+            Ok(0.into())
+        }
     } else {
         Ok(0.into())
     }
@@ -791,4 +891,33 @@ mod test {
             }
         );
     }
+
+    #[tokio::test]
+    async fn sunion_first_key_do_not_exists() {
+        let c = create_connection();
+
+        assert_eq!(
+            run_command(&c, &["sadd", "1", "a", "b", "c", "d"]).await,
+            run_command(&c, &["scard", "1"]).await
+        );
+
+        assert_eq!(
+            run_command(&c, &["sadd", "2", "c", "x"]).await,
+            run_command(&c, &["scard", "2"]).await
+        );
+
+        assert_eq!(
+            run_command(&c, &["sadd", "3", "a", "c", "e"]).await,
+            run_command(&c, &["scard", "3"]).await
+        );
+
+        assert_eq!(
+            6,
+            if let Ok(Value::Array(x)) = run_command(&c, &["sunion", "0", "1", "2", "3"]).await {
+                x.len()
+            } else {
+                0
+            }
+        );
+    }
 }

+ 165 - 133
src/cmd/string.rs

@@ -3,10 +3,10 @@ use super::now;
 use crate::{
     check_arg,
     connection::Connection,
-    db::Override,
+    db::utils::Override,
     error::Error,
     try_get_arg,
-    value::{bytes_to_number, Value},
+    value::{bytes_to_int, bytes_to_number, expiration::Expiration, float::Float, Value},
 };
 use bytes::Bytes;
 use std::{
@@ -28,7 +28,7 @@ pub async fn append(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// contains a string that can not be represented as integer. This operation is limited to 64 bit
 /// signed integers.
 pub async fn incr(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    conn.db().incr(&args[1], 1_i64)
+    conn.db().incr(&args[1], 1_i64).map(|n| n.into())
 }
 
 /// Increments the number stored at key by increment. If the key does not exist, it is set to 0
@@ -37,7 +37,7 @@ pub async fn incr(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// 64 bit signed integers.
 pub async fn incr_by(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
     let by: i64 = bytes_to_number(&args[2])?;
-    conn.db().incr(&args[1], by)
+    conn.db().incr(&args[1], by).map(|n| n.into())
 }
 
 /// Increment the string representing a floating point number stored at key by the specified
@@ -45,8 +45,17 @@ pub async fn incr_by(conn: &Connection, args: &[Bytes]) -> Result<Value, Error>
 /// is decremented (by the obvious properties of addition). If the key does not exist, it is set to
 /// 0 before performing the operation.
 pub async fn incr_by_float(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    let by: f64 = bytes_to_number(&args[2])?;
-    conn.db().incr(&args[1], by)
+    let by = bytes_to_number::<Float>(&args[2])?;
+    if by.is_infinite() || by.is_nan() {
+        return Err(Error::IncrByInfOrNan);
+    }
+    conn.db().incr(&args[1], by).map(|f| {
+        if f.fract() == 0.0 {
+            (*f as i64).into()
+        } else {
+            f.to_string().into()
+        }
+    })
 }
 
 /// Decrements the number stored at key by one. If the key does not exist, it is set to 0 before
@@ -54,7 +63,7 @@ pub async fn incr_by_float(conn: &Connection, args: &[Bytes]) -> Result<Value, E
 /// contains a string that can not be represented as integer. This operation is limited to 64 bit
 /// signed integers.
 pub async fn decr(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    conn.db().incr(&args[1], -1_i64)
+    conn.db().incr(&args[1], -1_i64).map(|n| n.into())
 }
 
 /// Decrements the number stored at key by decrement. If the key does not exist, it is set to 0
@@ -63,7 +72,7 @@ pub async fn decr(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// 64 bit signed integers.
 pub async fn decr_by(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
     let by: i64 = (&Value::new(&args[2])).try_into()?;
-    conn.db().incr(&args[1], by.neg())
+    conn.db().incr(&args[1], by.neg()).map(|n| n.into())
 }
 
 /// Get the value of key. If the key does not exist the special value nil is returned. An error is
@@ -75,42 +84,42 @@ pub async fn get(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// Get the value of key and optionally set its expiration. GETEX is similar to
 /// GET, but is a write command with additional options.
 pub async fn getex(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    let (expire_at, persist) = match args.len() {
+    let (expires_in, persist) = match args.len() {
         2 => (None, false),
         3 => {
             if check_arg!(args, 2, "PERSIST") {
-                (None, Default::default())
+                (None, true)
             } else {
                 return Err(Error::Syntax);
             }
         }
-        4 => {
-            let expires_in: i64 = bytes_to_number(&args[3])?;
-            if expires_in <= 0 {
-                // Delete key right away after returning
-                return Ok(conn.db().getdel(&args[1]));
-            }
-
-            let expires_in: u64 = expires_in as u64;
-
-            match String::from_utf8_lossy(&args[2]).to_uppercase().as_str() {
-                "EX" => (Some(Duration::from_secs(expires_in)), false),
-                "PX" => (Some(Duration::from_millis(expires_in)), false),
-                "EXAT" => (
-                    Some(Duration::from_secs(expires_in - now().as_secs())),
-                    false,
-                ),
-                "PXAT" => (
-                    Some(Duration::from_millis(expires_in - now().as_millis() as u64)),
-                    false,
-                ),
-                "PERSIST" => (None, Default::default()),
-                _ => return Err(Error::Syntax),
-            }
-        }
+        4 => match String::from_utf8_lossy(&args[2]).to_uppercase().as_str() {
+            "EX" => (
+                Some(Expiration::new(&args[3], false, false, &args[0])?),
+                false,
+            ),
+            "PX" => (
+                Some(Expiration::new(&args[3], true, false, &args[0])?),
+                false,
+            ),
+            "EXAT" => (
+                Some(Expiration::new(&args[3], false, true, &args[0])?),
+                false,
+            ),
+            "PXAT" => (
+                Some(Expiration::new(&args[3], true, true, &args[0])?),
+                false,
+            ),
+            "PERSIST" => (None, Default::default()),
+            _ => return Err(Error::Syntax),
+        },
         _ => return Err(Error::Syntax),
     };
-    Ok(conn.db().getex(&args[1], expire_at, persist))
+    Ok(conn.db().getex(
+        &args[1],
+        expires_in.map(|t| t.try_into()).transpose()?,
+        persist,
+    ))
 }
 
 /// Get the value of key. If the key does not exist the special value nil is returned. An error is
@@ -139,7 +148,7 @@ pub async fn getrange(conn: &Connection, args: &[Bytes]) -> Result<Value, Error>
             } else {
                 end.try_into().expect("Positive number")
             };
-            let end = min(end, len - 1);
+            let end = min(end, len.checked_sub(1).unwrap_or_default());
 
             if end < start {
                 return Ok("".into());
@@ -181,97 +190,89 @@ pub async fn mget(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// of its type. Any previous time to live associated with the key is discarded on successful SET
 /// operation.
 pub async fn set(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    match args.len() {
-        3 => Ok(conn.db().set(&args[1], Value::new(&args[2]), None)),
-        4 | 5 | 6 | 7 => {
-            let mut offset = 3;
-            let mut expiration = None;
-            let mut override_value = Override::Yes;
-            let mut return_previous = false;
-            let mut keep_ttl = false;
-            match String::from_utf8_lossy(&args[offset])
-                .to_uppercase()
-                .as_str()
-            {
-                "EX" => {
-                    expiration = Some(Duration::from_secs(bytes_to_number::<u64>(try_get_arg!(
-                        args, 4
-                    ))?));
-                    offset += 2;
-                }
-                "PX" => {
-                    expiration = Some(Duration::from_millis(bytes_to_number::<u64>(
-                        try_get_arg!(args, 4),
-                    )?));
-                    offset += 2;
-                }
-                "EXAT" => {
-                    expiration = Some(Duration::from_secs(
-                        bytes_to_number::<u64>(try_get_arg!(args, 4))? - now().as_secs(),
-                    ));
-                    offset += 2;
+    let len = args.len();
+    let mut i = 3;
+    let mut expiration = None;
+    let mut keep_ttl = false;
+    let mut override_value = Override::Yes;
+    let mut return_previous = false;
+
+    loop {
+        if i >= len {
+            break;
+        }
+        match String::from_utf8_lossy(&args[i]).to_uppercase().as_str() {
+            "EX" => {
+                if expiration.is_some() {
+                    return Err(Error::Syntax);
                 }
-                "PXAT" => {
-                    expiration = Some(Duration::from_millis(
-                        bytes_to_number::<u64>(try_get_arg!(args, 4))? - (now().as_millis() as u64),
-                    ));
-                    offset += 2;
+                expiration = Some(Expiration::new(
+                    try_get_arg!(args, i + 1),
+                    false,
+                    false,
+                    &args[0],
+                )?);
+                i += 1;
+            }
+            "PX" => {
+                if expiration.is_some() {
+                    return Err(Error::Syntax);
                 }
-                "KEEPTTL" => {
-                    keep_ttl = true;
-                    offset += 1;
+                expiration = Some(Expiration::new(
+                    try_get_arg!(args, i + 1),
+                    true,
+                    false,
+                    &args[0],
+                )?);
+                i += 1;
+            }
+            "EXAT" => {
+                if expiration.is_some() {
+                    return Err(Error::Syntax);
                 }
-                "NX" | "XX" | "GET" => {}
-                _ => return Err(Error::Syntax),
-            };
-
-            if offset < args.len() {
-                match String::from_utf8_lossy(&args[offset])
-                    .to_uppercase()
-                    .as_str()
-                {
-                    "NX" => {
-                        override_value = Override::No;
-                        offset += 1;
-                    }
-                    "XX" => {
-                        override_value = Override::Only;
-                        offset += 1;
-                    }
-                    "GET" => {}
-                    _ => return Err(Error::Syntax),
-                };
+                expiration = Some(Expiration::new(
+                    try_get_arg!(args, i + 1),
+                    false,
+                    true,
+                    &args[0],
+                )?);
+                i += 1;
             }
-
-            if offset < args.len() {
-                if String::from_utf8_lossy(&args[offset])
-                    .to_uppercase()
-                    .as_str()
-                    == "GET"
-                {
-                    return_previous = true;
-                } else {
+            "PXAT" => {
+                if expiration.is_some() {
                     return Err(Error::Syntax);
                 }
+                expiration = Some(Expiration::new(
+                    try_get_arg!(args, i + 1),
+                    true,
+                    true,
+                    &args[0],
+                )?);
+                i += 1;
             }
-
-            Ok(
-                match conn.db().set_advanced(
-                    &args[1],
-                    Value::new(&args[2]),
-                    expiration,
-                    override_value,
-                    keep_ttl,
-                    return_previous,
-                ) {
-                    Value::Integer(1) => Value::Ok,
-                    Value::Integer(0) => Value::Null,
-                    any_return => any_return,
-                },
-            )
+            "KEEPTTL" => keep_ttl = true,
+            "NX" => override_value = Override::No,
+            "XX" => override_value = Override::Only,
+            "GET" => return_previous = true,
+            _ => return Err(Error::Syntax),
         }
-        _ => Err(Error::Syntax),
+
+        i += 1;
     }
+    Ok(
+        match conn.db().set_advanced(
+            &args[1],
+            Value::new(&args[2]),
+            expiration.map(|t| t.try_into()).transpose()?,
+            override_value,
+            keep_ttl,
+            return_previous,
+        ) {
+            Value::Integer(1) => Value::Ok,
+            Value::Integer(0) => Value::Null,
+            any_return => any_return,
+        },
+    )
 }
 
 /// Sets the given keys to their respective values. MSET replaces existing
@@ -282,7 +283,12 @@ pub async fn set(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// It is not possible for clients to see that some of the keys were
 /// updated while others are unchanged.
 pub async fn mset(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    Ok(conn.db().multi_set(&args[1..], true))
+    conn.db().multi_set(&args[1..], true).map_err(|e| match e {
+        Error::Syntax => {
+            Error::WrongNumberArgument(String::from_utf8_lossy(&args[0]).to_uppercase())
+        }
+        e => e,
+    })
 }
 
 /// Sets the given keys to their respective values. MSETNX will not perform any
@@ -296,7 +302,12 @@ pub async fn mset(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// clients to see that some of the keys were updated while others are
 /// unchanged.
 pub async fn msetnx(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    Ok(conn.db().multi_set(&args[1..], false))
+    conn.db().multi_set(&args[1..], false).map_err(|e| match e {
+        Error::Syntax => {
+            Error::WrongNumberArgument(String::from_utf8_lossy(&args[0]).to_uppercase())
+        }
+        e => e,
+    })
 }
 
 /// Set key to hold the string value and set key to timeout after a given number of seconds. This
@@ -305,13 +316,13 @@ pub async fn msetnx(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
 /// SET mykey value
 /// EXPIRE mykey seconds
 pub async fn setex(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
-    let ttl = if check_arg!(args, 0, "SETEX") {
-        Duration::from_secs(bytes_to_number(&args[2])?)
-    } else {
-        Duration::from_millis(bytes_to_number(&args[2])?)
-    };
+    let is_milliseconds = check_arg!(args, 0, "PSETEX");
+
+    let expires_in = Expiration::new(&args[2], is_milliseconds, false, &args[0])?;
 
-    Ok(conn.db().set(&args[1], Value::new(&args[3]), Some(ttl)))
+    Ok(conn
+        .db()
+        .set(&args[1], Value::new(&args[3]), Some(expires_in.try_into()?)))
 }
 
 /// Set key to hold string value if key does not exist. In that case, it is
@@ -399,13 +410,13 @@ mod test {
         assert_eq!(Ok(Value::Integer(1)), r);
 
         let r = run_command(&c, &["ttl", "foo"]).await;
-        assert_eq!(Ok(Value::Integer(59)), r);
+        assert_eq!(Ok(Value::Integer(60)), r);
 
         let r = run_command(&c, &["incr", "foo"]).await;
         assert_eq!(Ok(Value::Integer(2)), r);
 
         let r = run_command(&c, &["ttl", "foo"]).await;
-        assert_eq!(Ok(Value::Integer(59)), r);
+        assert_eq!(Ok(Value::Integer(60)), r);
     }
 
     #[tokio::test]
@@ -429,13 +440,13 @@ mod test {
         assert_eq!(Ok(Value::Integer(1)), r);
 
         let r = run_command(&c, &["ttl", "foo"]).await;
-        assert_eq!(Ok(Value::Integer(59)), r);
+        assert_eq!(Ok(Value::Integer(60)), r);
 
         let r = run_command(&c, &["decr", "foo"]).await;
         assert_eq!(Ok(Value::Integer(-2)), r);
 
         let r = run_command(&c, &["ttl", "foo"]).await;
-        assert_eq!(Ok(Value::Integer(59)), r);
+        assert_eq!(Ok(Value::Integer(60)), r);
     }
 
     #[tokio::test]
@@ -458,6 +469,18 @@ mod test {
     }
 
     #[tokio::test]
+    async fn mset_incorrect_values() {
+        let c = create_connection();
+        let x = run_command(&c, &["mset", "foo", "bar", "bar"]).await;
+        assert_eq!(Err(Error::WrongNumberArgument("MSET".to_owned())), x);
+
+        assert_eq!(
+            Ok(Value::Array(vec![Value::Null, Value::Null])),
+            run_command(&c, &["mget", "foo", "bar"]).await
+        );
+    }
+
+    #[tokio::test]
     async fn mset() {
         let c = create_connection();
         let x = run_command(&c, &["mset", "foo", "bar", "bar", "foo"]).await;
@@ -510,7 +533,7 @@ mod test {
             run_command(&c, &["set", "foo", "bar1", "keepttl"]).await
         );
         assert_eq!(Ok("bar1".into()), run_command(&c, &["get", "foo"]).await);
-        assert_eq!(Ok(59.into()), run_command(&c, &["ttl", "foo"]).await);
+        assert_eq!(Ok(60.into()), run_command(&c, &["ttl", "foo"]).await);
 
         assert_eq!(
             Ok(Value::Ok),
@@ -572,7 +595,7 @@ mod test {
     async fn set_incorrect_params() {
         let c = create_connection();
         assert_eq!(
-            Err(Error::NotANumber),
+            Err(Error::NotANumberType("an integer".to_owned())),
             run_command(&c, &["set", "foo", "bar1", "ex", "xx"]).await
         );
         assert_eq!(
@@ -677,7 +700,7 @@ mod test {
             run_command(&c, &["setex", "foo", "10", "bar"]).await
         );
         assert_eq!(Ok("bar".into()), run_command(&c, &["get", "foo"]).await);
-        assert_eq!(Ok(9.into()), run_command(&c, &["ttl", "foo"]).await);
+        assert_eq!(Ok(10.into()), run_command(&c, &["ttl", "foo"]).await);
     }
 
     #[tokio::test]
@@ -722,4 +745,13 @@ mod test {
             run_command(&c, &["get", "foo"]).await,
         );
     }
+
+    #[tokio::test]
+    async fn test_invalid_ts() {
+        let c = create_connection();
+        assert_eq!(
+            Err(Error::InvalidExpire("set".to_owned())),
+            run_command(&c, &["set", "foo", "bar", "EX", "10000000000000000"]).await
+        );
+    }
 }

+ 28 - 3
src/cmd/transaction.rs

@@ -26,9 +26,14 @@ pub async fn multi(conn: &Connection, _: &[Bytes]) -> Result<Value, Error> {
 /// When using WATCH, EXEC will execute commands only if the watched keys were not modified,
 /// allowing for a check-and-set mechanism.
 pub async fn exec(conn: &Connection, _: &[Bytes]) -> Result<Value, Error> {
-    if conn.status() != ConnectionStatus::Multi {
-        return Err(Error::NotInTx);
-    }
+    match conn.status() {
+        ConnectionStatus::Multi => Ok(()),
+        ConnectionStatus::FailedTx => {
+            conn.stop_transaction();
+            Err(Error::TxAborted)
+        }
+        _ => Err(Error::NotInTx),
+    }?;
 
     if conn.did_keys_change() {
         let _ = conn.stop_transaction();
@@ -61,6 +66,9 @@ pub async fn exec(conn: &Connection, _: &[Bytes]) -> Result<Value, Error> {
 
 /// Marks the given keys to be watched for conditional execution of a transaction.
 pub async fn watch(conn: &Connection, args: &[Bytes]) -> Result<Value, Error> {
+    if conn.status() == ConnectionStatus::Multi {
+        return Err(Error::WatchInsideTx);
+    }
     conn.watch_key(
         &(&args[1..])
             .iter()
@@ -261,6 +269,23 @@ mod test {
         assert_eq!(Err(Error::NotInTx), run_command(&c, &["exec"]).await);
     }
 
+    #[tokio::test]
+    async fn test_exec_fails_abort() {
+        let c = create_connection();
+
+        assert_eq!(Ok(Value::Ok), run_command(&c, &["multi"]).await);
+        assert_eq!(
+            Err(Error::CommandNotFound("GETX".to_owned())),
+            run_command(&c, &["getx", "foo"]).await
+        );
+        assert_eq!(
+            Ok(Value::Queued),
+            run_command(&c, &["set", "foo", "foo"]).await
+        );
+        assert_eq!(Err(Error::TxAborted), run_command(&c, &["exec"]).await,);
+        assert_eq!(Err(Error::NotInTx), run_command(&c, &["exec"]).await,);
+    }
+
     fn get_keys(args: &[&str]) -> Vec<Bytes> {
         let args: Vec<Bytes> = args.iter().map(|s| Bytes::from(s.to_string())).collect();
         let d = Dispatcher::new();

+ 1 - 4
src/config.rs

@@ -57,11 +57,8 @@ pub enum LogLevel {
     #[serde(rename = "trace")]
     Trace,
     /// Debug
-    #[serde(rename = "debug")]
-    Debug,
-    /// Verbose
     #[serde(rename = "verbose")]
-    Verbose,
+    Debug,
     /// Notice
     #[serde(rename = "notice")]
     Notice,

+ 5 - 0
src/connection/connections.rs

@@ -74,6 +74,11 @@ impl Connections {
         (pubsub_receiver, conn)
     }
 
+    /// Get a connection by their connection id
+    pub fn get_by_conn_id(&self, conn_id: u128) -> Option<Arc<Connection>> {
+        self.connections.read().get(&conn_id).cloned()
+    }
+
     /// Iterates over all connections
     pub fn iter(&self, f: &mut dyn FnMut(Arc<Connection>)) {
         for (_, value) in self.connections.read().iter() {

+ 70 - 11
src/connection/mod.rs

@@ -15,6 +15,8 @@ pub mod pubsub_server;
 pub enum ConnectionStatus {
     /// The connection is in a MULTI stage and commands are being queued
     Multi,
+    /// Failed Tx
+    FailedTx,
     /// The connection is executing a transaction
     ExecutingTx,
     /// The connection is in pub-sub only mode
@@ -29,6 +31,15 @@ impl Default for ConnectionStatus {
     }
 }
 
+#[derive(Debug, Copy, Clone)]
+/// Reason while a client was unblocked
+pub enum UnblockReason {
+    /// Timeout
+    Timeout,
+    /// Throw an error
+    Error,
+}
+
 /// Connection information
 #[derive(Debug)]
 pub struct ConnectionInfo {
@@ -39,6 +50,8 @@ pub struct ConnectionInfo {
     tx_keys: HashSet<Bytes>,
     status: ConnectionStatus,
     commands: Option<Vec<Vec<Bytes>>>,
+    is_blocked: bool,
+    unblock_reason: Option<UnblockReason>,
 }
 
 /// Connection
@@ -62,6 +75,8 @@ impl ConnectionInfo {
             tx_keys: HashSet::new(),
             commands: None,
             status: ConnectionStatus::Normal,
+            is_blocked: false,
+            unblock_reason: None,
         }
     }
 }
@@ -80,6 +95,12 @@ impl Connection {
         self.all_connections.pubsub()
     }
 
+    /// Queue response, this is the only way that a handler has to send multiple
+    /// responses leveraging internally the pubsub to itself.
+    pub fn append_response(&self, message: Value) {
+        self.pubsub_client.send(message)
+    }
+
     /// Returns a reference to the pubsub client
     pub fn pubsub_client(&self) -> &pubsub_connection::PubsubClient {
         &self.pubsub_client
@@ -91,12 +112,36 @@ impl Connection {
         match info.status {
             ConnectionStatus::Normal | ConnectionStatus::Pubsub => {
                 info.status = ConnectionStatus::Pubsub;
-                Ok(Value::Ok)
+                Ok(Value::Ignore)
             }
             _ => Err(Error::NestedTx),
         }
     }
 
+    /// Block the connection
+    pub fn block(&self) {
+        let mut info = self.info.write();
+        info.is_blocked = true;
+        info.unblock_reason = None;
+    }
+
+    /// Unblock connection
+    pub fn unblock(&self, reason: UnblockReason) -> bool {
+        let mut info = self.info.write();
+        if info.is_blocked {
+            info.is_blocked = false;
+            info.unblock_reason = Some(reason);
+            true
+        } else {
+            false
+        }
+    }
+
+    /// If the current connection has been externally unblocked
+    pub fn is_unblocked(&self) -> Option<UnblockReason> {
+        self.info.read().unblock_reason
+    }
+
     /// Connection ID
     pub fn id(&self) -> u128 {
         self.id
@@ -107,18 +152,27 @@ impl Connection {
     /// If the connection was not in a MULTI stage an error is thrown.
     pub fn stop_transaction(&self) -> Result<Value, Error> {
         let mut info = self.info.write();
-        if info.status == ConnectionStatus::Multi || info.status == ConnectionStatus::ExecutingTx {
-            info.commands = None;
-            info.watch_keys.clear();
-            info.tx_keys.clear();
-            info.status = ConnectionStatus::Normal;
+        match info.status {
+            ConnectionStatus::Multi
+            | ConnectionStatus::FailedTx
+            | ConnectionStatus::ExecutingTx => {
+                info.commands = None;
+                info.watch_keys.clear();
+                info.tx_keys.clear();
+                info.status = ConnectionStatus::Normal;
 
-            Ok(Value::Ok)
-        } else {
-            Err(Error::NotInTx)
+                Ok(Value::Ok)
+            }
+            _ => Err(Error::NotInTx),
         }
     }
 
+    /// Flag the transaction as failed
+    pub fn fail_transaction(&self) {
+        let mut info = self.info.write();
+        info.status = ConnectionStatus::FailedTx;
+    }
+
     /// Starts a transaction/multi
     ///
     /// Nested transactions are not possible.
@@ -142,8 +196,13 @@ impl Connection {
         info.tx_keys = HashSet::new();
 
         let pubsub = self.pubsub();
-        pubsub.unsubscribe(&self.pubsub_client.subscriptions(), self);
-        pubsub.punsubscribe(&self.pubsub_client.psubscriptions(), self);
+        let pubsub_client = self.pubsub_client();
+        if !pubsub_client.subscriptions().is_empty() {
+            pubsub.unsubscribe(&self.pubsub_client.subscriptions(), self);
+        }
+        if !pubsub_client.psubscriptions().is_empty() {
+            pubsub.punsubscribe(&self.pubsub_client.psubscriptions(), self);
+        }
     }
 
     /// Returns the status of the connection

+ 26 - 18
src/connection/pubsub_connection.rs

@@ -22,7 +22,6 @@ struct MetaData {
     subscriptions: HashMap<Bytes, bool>,
     psubscriptions: HashMap<Pattern, bool>,
     is_psubcribed: bool,
-    id: usize,
 }
 
 impl PubsubClient {
@@ -33,38 +32,39 @@ impl PubsubClient {
                 subscriptions: HashMap::new(),
                 psubscriptions: HashMap::new(),
                 is_psubcribed: false,
-                id: 0,
             }),
             sender,
         }
     }
 
     /// Unsubscribe from pattern subscriptions
-    pub fn punsubscribe(&self, channels: &[Pattern], conn: &Connection) -> u32 {
+    pub fn punsubscribe(&self, channels: &[Pattern], conn: &Connection) {
         let mut meta = self.meta.write();
         channels
             .iter()
             .map(|channel| meta.psubscriptions.remove(channel))
             .for_each(drop);
-        if meta.psubscriptions.len() + meta.subscriptions.len() == 0 {
-            drop(meta);
+        drop(meta);
+        conn.pubsub().punsubscribe(channels, conn);
+
+        if self.total_subs() == 0 {
             conn.reset();
         }
-        conn.pubsub().punsubscribe(channels, conn)
     }
 
     /// Unsubscribe from channels
-    pub fn unsubscribe(&self, channels: &[Bytes], conn: &Connection) -> u32 {
+    pub fn unsubscribe(&self, channels: &[Bytes], conn: &Connection) {
         let mut meta = self.meta.write();
         channels
             .iter()
             .map(|channel| meta.subscriptions.remove(channel))
             .for_each(drop);
-        if meta.psubscriptions.len() + meta.subscriptions.len() == 0 {
-            drop(meta);
+        drop(meta);
+        conn.pubsub().unsubscribe(channels, conn);
+
+        if self.total_subs() == 0 {
             conn.reset();
         }
-        conn.pubsub().unsubscribe(channels, conn)
     }
 
     /// Return list of subscriptions for this connection
@@ -87,20 +87,22 @@ impl PubsubClient {
             .collect::<Vec<Pattern>>()
     }
 
-    /// Creates a new subscription and returns the ID for this new subscription.
-    pub fn new_subscription(&self, channel: &Bytes) -> usize {
+    /// Return total number of subscriptions + psubscription
+    pub fn total_subs(&self) -> usize {
+        let meta = self.meta.read();
+        meta.subscriptions.len() + meta.psubscriptions.len()
+    }
+
+    /// Creates a new subscription
+    pub fn new_subscription(&self, channel: &Bytes) {
         let mut meta = self.meta.write();
         meta.subscriptions.insert(channel.clone(), true);
-        meta.id += 1;
-        meta.id
     }
 
-    /// Creates a new pattern subscription and returns the ID for this new subscription.
-    pub fn new_psubscription(&self, channel: &Pattern) -> usize {
+    /// Creates a new pattern subscription
+    pub fn new_psubscription(&self, channel: &Pattern) {
         let mut meta = self.meta.write();
         meta.psubscriptions.insert(channel.clone(), true);
-        meta.id += 1;
-        meta.id
     }
 
     /// Does this connection has a pattern subscription?
@@ -118,4 +120,10 @@ impl PubsubClient {
     pub fn sender(&self) -> mpsc::Sender<Value> {
         self.sender.clone()
     }
+
+    /// Sends a message
+    #[inline]
+    pub fn send(&self, message: Value) {
+        let _ = self.sender.try_send(message);
+    }
 }

+ 40 - 34
src/connection/pubsub_server.rs

@@ -16,7 +16,6 @@ type Subscription = HashMap<u128, Sender>;
 pub struct Pubsub {
     subscriptions: RwLock<HashMap<Bytes, Subscription>>,
     psubscriptions: RwLock<HashMap<Pattern, Subscription>>,
-    number_of_psubscriptions: RwLock<i64>,
 }
 
 impl Pubsub {
@@ -25,7 +24,6 @@ impl Pubsub {
         Self {
             subscriptions: RwLock::new(HashMap::new()),
             psubscriptions: RwLock::new(HashMap::new()),
-            number_of_psubscriptions: RwLock::new(0),
         }
     }
 
@@ -35,8 +33,8 @@ impl Pubsub {
     }
 
     /// Returns numbers of pattern-subscriptions
-    pub fn get_number_of_psubscribers(&self) -> i64 {
-        *(self.number_of_psubscriptions.read())
+    pub fn get_number_of_psubscribers(&self) -> usize {
+        self.psubscriptions.read().len()
     }
 
     /// Returns numbers of subscribed for given channels
@@ -71,16 +69,16 @@ impl Pubsub {
                 subscriptions.insert(channel.clone(), h);
             }
             if !conn.pubsub_client().is_psubcribed() {
-                let mut psubs = self.number_of_psubscriptions.write();
                 conn.pubsub_client().make_psubcribed();
-                *psubs += 1;
             }
 
-            let _ = conn.pubsub_client().sender().try_send(
+            conn.pubsub_client().new_psubscription(&channel);
+
+            conn.append_response(
                 vec![
                     "psubscribe".into(),
                     Value::new(&bytes_channel),
-                    conn.pubsub_client().new_psubscription(&channel).into(),
+                    conn.pubsub_client().total_subs().into(),
                 ]
                 .into(),
             );
@@ -131,35 +129,39 @@ impl Pubsub {
     }
 
     /// Unsubscribe from a pattern subscription
-    pub fn punsubscribe(&self, channels: &[Pattern], conn: &Connection) -> u32 {
+    pub fn punsubscribe(&self, channels: &[Pattern], conn: &Connection) {
+        if channels.is_empty() {
+            return conn.append_response(Value::Array(vec![
+                "punsubscribe".into(),
+                Value::Null,
+                0usize.into(),
+            ]));
+        }
         let mut all_subs = self.psubscriptions.write();
         let conn_id = conn.id();
-        let mut removed = 0;
         channels
             .iter()
             .map(|channel| {
                 if let Some(subs) = all_subs.get_mut(channel) {
-                    if let Some(sender) = subs.remove(&conn_id) {
-                        let _ = sender.try_send(Value::Array(vec![
-                            "punsubscribe".into(),
-                            channel.as_str().into(),
-                            1.into(),
-                        ]));
-                        removed += 1;
-                    }
+                    subs.remove(&conn_id);
                     if subs.is_empty() {
                         all_subs.remove(channel);
                     }
                 }
+
+                conn.append_response(Value::Array(vec![
+                    "punsubscribe".into(),
+                    channel.as_str().into(),
+                    conn.pubsub_client().total_subs().into(),
+                ]));
             })
             .for_each(drop);
-
-        removed
     }
 
     /// Subscribe connection to channels
     pub fn subscribe(&self, channels: &[Bytes], conn: &Connection) {
         let mut subscriptions = self.subscriptions.write();
+        let total_psubs = self.psubscriptions.read().len();
 
         channels
             .iter()
@@ -172,11 +174,12 @@ impl Pubsub {
                     subscriptions.insert(channel.clone(), h);
                 }
 
-                let _ = conn.pubsub_client().sender().try_send(
+                conn.pubsub_client().new_subscription(channel);
+                conn.append_response(
                     vec![
                         "subscribe".into(),
                         Value::new(&channel),
-                        conn.pubsub_client().new_subscription(channel).into(),
+                        conn.pubsub_client().total_subs().into(),
                     ]
                     .into(),
                 );
@@ -185,29 +188,32 @@ impl Pubsub {
     }
 
     /// Removes connection subscription to channels.
-    pub fn unsubscribe(&self, channels: &[Bytes], conn: &Connection) -> u32 {
+    pub fn unsubscribe(&self, channels: &[Bytes], conn: &Connection) {
+        if channels.is_empty() {
+            return conn.append_response(Value::Array(vec![
+                "unsubscribe".into(),
+                Value::Null,
+                0usize.into(),
+            ]));
+        }
         let mut all_subs = self.subscriptions.write();
+        let total_psubs = self.psubscriptions.read().len();
         let conn_id = conn.id();
-        let mut removed = 0;
         channels
             .iter()
             .map(|channel| {
                 if let Some(subs) = all_subs.get_mut(channel) {
-                    if let Some(sender) = subs.remove(&conn_id) {
-                        let _ = sender.try_send(Value::Array(vec![
-                            "unsubscribe".into(),
-                            Value::new(&channel),
-                            1.into(),
-                        ]));
-                        removed += 1;
-                    }
+                    subs.remove(&conn_id);
                     if subs.is_empty() {
                         all_subs.remove(channel);
                     }
                 }
+                conn.append_response(Value::Array(vec![
+                    "unsubscribe".into(),
+                    Value::new(&channel),
+                    (all_subs.len() + total_psubs).into(),
+                ]));
             })
             .for_each(drop);
-
-        removed
     }
 }

+ 6 - 0
src/db/expiration.rs

@@ -61,6 +61,12 @@ impl ExpirationDb {
         self.keys.get(key).is_some()
     }
 
+    pub fn flush(&mut self) -> bool {
+        self.expiring_keys.clear();
+        self.keys.clear();
+        true
+    }
+
     pub fn remove(&mut self, key: &Bytes) -> bool {
         if let Some(prev) = self.keys.remove(key) {
             // Another key with expiration is already known, it has

+ 312 - 142
src/db/mod.rs

@@ -2,61 +2,40 @@
 //!
 //! This database module is the core of the miniredis project. All other modules around this
 //! database module.
-mod entry;
-mod expiration;
-pub mod pool;
-pub mod scan;
-
+use self::utils::{far_future, ExpirationOpts, Override};
 use crate::{
     error::Error,
     value::{
+        bytes_to_number,
         cursor::Cursor,
         typ::{Typ, ValueTyp},
-        Value,
+        VDebug, Value,
     },
 };
 use bytes::{BufMut, Bytes, BytesMut};
+use core::num;
 use entry::{new_version, Entry};
 use expiration::ExpirationDb;
 use glob::Pattern;
 use log::trace;
+use num_traits::CheckedAdd;
 use parking_lot::{Mutex, RwLock};
 use seahash::hash;
 use std::{
     collections::HashMap,
     convert::{TryFrom, TryInto},
     ops::{AddAssign, Deref},
+    str::FromStr,
     sync::Arc,
     thread,
 };
 use tokio::time::{Duration, Instant};
 
-/// Override database entries
-#[derive(PartialEq, Debug, Clone, Copy)]
-pub enum Override {
-    /// Allow override
-    Yes,
-    /// Do not allow override, only new entries
-    No,
-    /// Allow only override
-    Only,
-}
-
-impl From<bool> for Override {
-    fn from(v: bool) -> Override {
-        if v {
-            Override::Yes
-        } else {
-            Override::No
-        }
-    }
-}
-
-impl Default for Override {
-    fn default() -> Self {
-        Self::Yes
-    }
-}
+mod entry;
+mod expiration;
+pub mod pool;
+pub mod scan;
+pub(crate) mod utils;
 
 /// Database structure
 ///
@@ -64,7 +43,7 @@ impl Default for Override {
 /// The slots property is shared for all connections.
 ///
 /// To avoid lock contention this database is *not* a single HashMap, instead it is a vector of
-/// HashMaps. Each key is presharded and a bucket is selected. By doing this pre-step instead of
+/// HashMaps. Each key is pre-sharded and a bucket is selected. By doing this pre-step instead of
 /// locking the entire database, only a small portion is locked (shared or exclusively) at a time,
 /// making this database implementation thread-friendly. The number of number_of_slots available cannot be
 /// changed at runtime.
@@ -222,44 +201,172 @@ impl Db {
         }
     }
 
+    /// Return debug info for a key
+    pub fn debug(&self, key: &Bytes) -> Result<VDebug, Error> {
+        let slot = self.slots[self.get_slot(key)].read();
+        Ok(slot
+            .get(key)
+            .filter(|x| x.is_valid())
+            .ok_or(Error::NotFound)?
+            .value
+            .debug())
+    }
+
+    /// Return the digest for each key. This used for testing only
+    pub fn digest(&self, keys: &[Bytes]) -> Result<Vec<Value>, Error> {
+        Ok(keys
+            .iter()
+            .map(|key| {
+                let slot = self.slots[self.get_slot(key)].read();
+                Value::Blob(
+                    slot.get(key)
+                        .filter(|v| v.is_valid())
+                        .map(|v| hex::encode(&v.value.digest()))
+                        .unwrap_or("00000".into())
+                        .as_str()
+                        .into(),
+                )
+            })
+            .collect::<Vec<Value>>())
+    }
+
+    /// Flushes the entire database
+    pub fn flushdb(&self) -> Result<Value, Error> {
+        self.expirations.lock().flush();
+        self.slots
+            .iter()
+            .map(|s| {
+                let mut s = s.write();
+                s.clear();
+            })
+            .for_each(drop);
+        Ok(Value::Ok)
+    }
+
+    /// Returns the number of elements in the database
+    pub fn len(&self) -> Result<usize, Error> {
+        self.purge();
+        Ok(self.slots.iter().map(|s| s.read().len()).sum())
+    }
+
+    /// Round numbers to store efficiently, specially float numbers. For instance `1.00` will be converted to `1`.
+    fn round_numbers<T>(number: T) -> BytesMut
+    where
+        T: ToString,
+    {
+        let number_to_str = number.to_string();
+
+        if number_to_str.find('.').is_none() {
+            return number_to_str.as_bytes().into();
+        }
+
+        let number_to_str = number_to_str
+            .trim_end_matches(|c| c == '0' || c == '.')
+            .to_string();
+
+        if number_to_str.is_empty() {
+            "0"
+        } else {
+            number_to_str.as_str()
+        }
+        .into()
+    }
+
+    // Converts a given number to a correct Value, it should be used with Self::round_numbers()
+    fn number_to_value(number: &[u8]) -> Result<Value, Error> {
+        if number.iter().find(|x| **x == b'.').is_some() {
+            Ok(Value::new(number))
+        } else {
+            Ok(Value::Integer(bytes_to_number(number)?))
+        }
+    }
+
+    /// Increment a sub-key in a hash
+    ///
+    /// If the stored value cannot be converted into a number an error will be thrown
+    pub fn hincrby<T>(
+        &self,
+        key: &Bytes,
+        sub_key: &Bytes,
+        incr_by: &Bytes,
+        typ: &str,
+    ) -> Result<Value, Error>
+    where
+        T: ToString
+            + FromStr
+            + CheckedAdd
+            + for<'a> TryFrom<&'a Value, Error = Error>
+            + Into<Value>
+            + Copy,
+    {
+        let mut slot = self.slots[self.get_slot(key)].write();
+        let mut incr_by: T =
+            bytes_to_number(incr_by).map_err(|_| Error::NotANumberType(typ.to_owned()))?;
+        match slot.get_mut(key).filter(|x| x.is_valid()).map(|x| x.get()) {
+            Some(Value::Hash(h)) => {
+                let mut h = h.write();
+                if let Some(n) = h.get(sub_key) {
+                    incr_by = incr_by
+                        .checked_add(
+                            &bytes_to_number(n)
+                                .map_err(|_| Error::NotANumberType(typ.to_owned()))?,
+                        )
+                        .ok_or(Error::Overflow)?;
+                }
+                let incr_by_bytes = Self::round_numbers(incr_by).freeze();
+                h.insert(sub_key.clone(), incr_by_bytes.clone());
+
+                Self::number_to_value(&incr_by_bytes)
+            }
+            None => {
+                #[allow(clippy::mutable_key_type)]
+                let mut h = HashMap::new();
+                let incr_by_bytes = Self::round_numbers(incr_by).freeze();
+                h.insert(sub_key.clone(), incr_by_bytes.clone());
+                let _ = slot.insert(key.clone(), Entry::new(h.into(), None));
+                Self::number_to_value(&incr_by_bytes)
+            }
+            _ => Err(Error::WrongType),
+        }
+    }
+
     /// Increments a key's value by a given number
     ///
     /// If the stored value cannot be converted into a number an error will be
     /// thrown.
-    pub fn incr<
-        T: ToString + AddAssign + for<'a> TryFrom<&'a Value, Error = Error> + Into<Value> + Copy,
-    >(
-        &self,
-        key: &Bytes,
-        incr_by: T,
-    ) -> Result<Value, Error> {
-        let mut slots = self.slots[self.get_slot(key)].write();
-        match slots.get_mut(key) {
+    pub fn incr<T>(&self, key: &Bytes, incr_by: T) -> Result<T, Error>
+    where
+        T: ToString + CheckedAdd + for<'a> TryFrom<&'a Value, Error = Error> + Into<Value> + Copy,
+    {
+        let mut slot = self.slots[self.get_slot(key)].write();
+        match slot.get_mut(key).filter(|x| x.is_valid()) {
             Some(x) => {
+                if !x.is_clonable() {
+                    return Err(Error::WrongType);
+                }
                 let value = x.get();
                 let mut number: T = value.try_into()?;
 
-                number += incr_by;
+                number = incr_by.checked_add(&number).ok_or(Error::Overflow)?;
 
-                x.change_value(Value::Blob(number.to_string().as_str().into()));
+                x.change_value(Value::Blob(Self::round_numbers(number)));
 
-                Ok(number.into())
+                Ok(number)
             }
             None => {
-                slots.insert(
+                slot.insert(
                     key.clone(),
-                    Entry::new(Value::Blob(incr_by.to_string().as_str().into()), None),
+                    Entry::new(Value::Blob(Self::round_numbers(incr_by)), None),
                 );
-                Ok((incr_by as T).into())
+                Ok(incr_by)
             }
         }
     }
 
     /// Removes any expiration associated with a given key
     pub fn persist(&self, key: &Bytes) -> Value {
-        let mut slots = self.slots[self.get_slot(key)].write();
-        slots
-            .get_mut(key)
+        let mut slot = self.slots[self.get_slot(key)].write();
+        slot.get_mut(key)
             .filter(|x| x.is_valid())
             .map_or(0.into(), |x| {
                 if x.has_ttl() {
@@ -273,18 +380,58 @@ impl Db {
     }
 
     /// Set time to live for a given key
-    pub fn set_ttl(&self, key: &Bytes, expires_in: Duration) -> Value {
-        let mut slots = self.slots[self.get_slot(key)].write();
-        let expires_at = Instant::now() + expires_in;
+    pub fn set_ttl(
+        &self,
+        key: &Bytes,
+        expires_in: Duration,
+        opts: ExpirationOpts,
+    ) -> Result<Value, Error> {
+        if (opts.NX && opts.XX) || (opts.NX && opts.GT) || (opts.NX && opts.LT) {
+            return Err(Error::OptsNotCompatible("NX and XX, GT or LT".to_owned()));
+        }
+
+        if opts.GT && opts.LT {
+            return Err(Error::OptsNotCompatible("GT and LT".to_owned()));
+        }
 
-        slots
+        let mut slot = self.slots[self.get_slot(key)].write();
+        let expires_at = Instant::now()
+            .checked_add(expires_in)
+            .unwrap_or_else(far_future);
+
+        Ok(slot
             .get_mut(key)
             .filter(|x| x.is_valid())
             .map_or(0.into(), |x| {
+                let current_expire = x.get_ttl();
+                if opts.NX && current_expire.is_some() {
+                    return 0.into();
+                }
+                if opts.XX && current_expire.is_none() {
+                    return 0.into();
+                }
+                if opts.GT {
+                    if let Some(current_expire) = current_expire {
+                        if expires_at <= current_expire {
+                            return 0.into();
+                        }
+                    } else {
+                        return 0.into();
+                    }
+                }
+
+                if opts.LT {
+                    if let Some(current_expire) = current_expire {
+                        if expires_at >= current_expire {
+                            return 0.into();
+                        }
+                    }
+                }
+
                 self.expirations.lock().add(key, expires_at);
                 x.set_ttl(expires_at);
                 1.into()
-            })
+            }))
     }
 
     /// Overwrites part of the string stored at key, starting at the specified
@@ -293,9 +440,9 @@ impl Db {
     /// make offset fit. Non-existing keys are considered as empty strings, so this
     /// command will make sure it holds a string large enough to be able to set
     /// value at offset.
-    pub fn set_range(&self, key: &Bytes, offset: u64, data: &[u8]) -> Result<Value, Error> {
-        let mut slots = self.slots[self.get_slot(key)].write();
-        let value = slots.get_mut(key).map(|value| {
+    pub fn set_range(&self, key: &Bytes, offset: i128, data: &[u8]) -> Result<Value, Error> {
+        let mut slot = self.slots[self.get_slot(key)].write();
+        let value = slot.get_mut(key).map(|value| {
             if !value.is_valid() {
                 self.expirations.lock().remove(key);
                 value.persist();
@@ -303,6 +450,14 @@ impl Db {
             value.get_mut()
         });
 
+        if offset < 0 {
+            return Err(Error::OutOfRange);
+        }
+
+        if offset >= 512 * 1024 * 1024 - 4 {
+            return Err(Error::MaxAllowedSize);
+        }
+
         let length = offset as usize + data.len();
         match value {
             Some(Value::Blob(bytes)) => {
@@ -314,11 +469,14 @@ impl Db {
                 Ok(bytes.len().into())
             }
             None => {
+                if data.len() == 0 {
+                    return Ok(0.into());
+                }
                 let mut bytes = BytesMut::new();
                 bytes.resize(length, 0);
                 let writer = &mut bytes[offset as usize..];
                 writer.copy_from_slice(data);
-                slots.insert(key.clone(), Entry::new(Value::new(&bytes), None));
+                slot.insert(key.clone(), Entry::new(Value::new(&bytes), None));
                 Ok(bytes.len().into())
             }
             _ => Err(Error::WrongType),
@@ -333,13 +491,13 @@ impl Db {
         replace: Override,
         target_db: Option<Arc<Db>>,
     ) -> Result<bool, Error> {
-        let slots = self.slots[self.get_slot(source)].read();
-        let value = if let Some(value) = slots.get(source).filter(|x| x.is_valid()) {
+        let slot = self.slots[self.get_slot(source)].read();
+        let value = if let Some(value) = slot.get(source).filter(|x| x.is_valid()) {
             value.clone()
         } else {
             return Ok(false);
         };
-        drop(slots);
+        drop(slot);
 
         if let Some(db) = target_db {
             if db.db_id == self.db_id && source == target {
@@ -365,8 +523,8 @@ impl Db {
             if replace == Override::No && self.exists(&[target.clone()]) > 0 {
                 return Ok(false);
             }
-            let mut slots = self.slots[self.get_slot(target)].write();
-            slots.insert(target.clone(), value);
+            let mut slot = self.slots[self.get_slot(target)].write();
+            slot.insert(target.clone(), value);
 
             Ok(true)
         }
@@ -410,30 +568,25 @@ impl Db {
         if slot1 == slot2 {
             let mut slot = self.slots[slot1].write();
 
+            if override_value == Override::No && slot.get(target).is_some() {
+                return Ok(false);
+            }
+
             if let Some(value) = slot.remove(source) {
-                Ok(
-                    if override_value == Override::No && slot.get(target).is_some() {
-                        false
-                    } else {
-                        slot.insert(target.clone(), value);
-                        true
-                    },
-                )
+                slot.insert(target.clone(), value);
+                Ok(true)
             } else {
                 Err(Error::NotFound)
             }
         } else {
             let mut slot1 = self.slots[slot1].write();
             let mut slot2 = self.slots[slot2].write();
+            if override_value == Override::No && slot2.get(target).is_some() {
+                return Ok(false);
+            }
             if let Some(value) = slot1.remove(source) {
-                Ok(
-                    if override_value == Override::No && slot2.get(target).is_some() {
-                        false
-                    } else {
-                        slot2.insert(target.clone(), value);
-                        true
-                    },
-                )
+                slot2.insert(target.clone(), value);
+                Ok(true)
             } else {
                 Err(Error::NotFound)
             }
@@ -481,9 +634,9 @@ impl Db {
         let mut matches = 0;
         keys.iter()
             .map(|key| {
-                let slots = self.slots[self.get_slot(key)].read();
-                if slots.get(key).is_some() {
-                    matches += 1;
+                let slot = self.slots[self.get_slot(key)].read();
+                if let Some(key) = slot.get(key) {
+                    matches += if key.is_valid() { 1 } else { 0 };
                 }
             })
             .for_each(drop);
@@ -508,14 +661,14 @@ impl Db {
         F1: FnOnce(&Value) -> Result<Value, Error>,
         F2: FnOnce() -> Result<Value, Error>,
     {
-        let slots = self.slots[self.get_slot(key)].read();
-        let entry = slots.get(key).filter(|x| x.is_valid()).map(|e| e.get());
+        let slot = self.slots[self.get_slot(key)].read();
+        let entry = slot.get(key).filter(|x| x.is_valid()).map(|e| e.get());
 
         if let Some(entry) = entry {
             found(entry)
         } else {
             // drop lock
-            drop(slots);
+            drop(slot);
 
             not_found()
         }
@@ -523,9 +676,8 @@ impl Db {
 
     /// Updates the entry version of a given key
     pub fn bump_version(&self, key: &Bytes) -> bool {
-        let mut slots = self.slots[self.get_slot(key)].write();
-        slots
-            .get_mut(key)
+        let mut slot = self.slots[self.get_slot(key)].write();
+        slot.get_mut(key)
             .filter(|x| x.is_valid())
             .map(|entry| {
                 entry.bump_version();
@@ -535,19 +687,17 @@ impl Db {
 
     /// Returns the version of a given key
     pub fn get_version(&self, key: &Bytes) -> u128 {
-        let slots = self.slots[self.get_slot(key)].read();
-        slots
-            .get(key)
+        let slot = self.slots[self.get_slot(key)].read();
+        slot.get(key)
             .filter(|x| x.is_valid())
             .map(|entry| entry.version())
-            .unwrap_or_else(new_version)
+            .unwrap_or_default()
     }
 
     /// Returns the name of the value type
     pub fn get_data_type(&self, key: &Bytes) -> String {
-        let slots = self.slots[self.get_slot(key)].read();
-        slots
-            .get(key)
+        let slot = self.slots[self.get_slot(key)].read();
+        slot.get(key)
             .filter(|x| x.is_valid())
             .map_or("none".to_owned(), |x| {
                 Typ::get_type(x.get()).to_string().to_lowercase()
@@ -556,25 +706,25 @@ impl Db {
 
     /// Get a copy of an entry
     pub fn get(&self, key: &Bytes) -> Value {
-        let slots = self.slots[self.get_slot(key)].read();
-        slots
-            .get(key)
+        let slot = self.slots[self.get_slot(key)].read();
+        slot.get(key)
             .filter(|x| x.is_valid())
             .map_or(Value::Null, |x| x.clone_value())
     }
 
     /// Get a copy of an entry and modifies the expiration of the key
     pub fn getex(&self, key: &Bytes, expires_in: Option<Duration>, make_persistent: bool) -> Value {
-        let mut slots = self.slots[self.get_slot(key)].write();
-        slots
-            .get_mut(key)
+        let mut slot = self.slots[self.get_slot(key)].write();
+        slot.get_mut(key)
             .filter(|x| x.is_valid())
             .map(|value| {
                 if make_persistent {
                     self.expirations.lock().remove(key);
                     value.persist();
                 } else if let Some(expires_in) = expires_in {
-                    let expires_at = Instant::now() + expires_in;
+                    let expires_at = Instant::now()
+                        .checked_add(expires_in)
+                        .unwrap_or_else(far_future);
                     self.expirations.lock().add(key, expires_at);
                     value.set_ttl(expires_at);
                 }
@@ -587,9 +737,8 @@ impl Db {
     pub fn get_multi(&self, keys: &[Bytes]) -> Value {
         keys.iter()
             .map(|key| {
-                let slots = self.slots[self.get_slot(key)].read();
-                slots
-                    .get(key)
+                let slot = self.slots[self.get_slot(key)].read();
+                slot.get(key)
                     .filter(|x| x.is_valid() && x.is_clonable())
                     .map_or(Value::Null, |x| x.clone_value())
             })
@@ -599,29 +748,28 @@ impl Db {
 
     /// Get a key or set a new value for the given key.
     pub fn getset(&self, key: &Bytes, value: Value) -> Value {
-        let mut slots = self.slots[self.get_slot(key)].write();
+        let mut slot = self.slots[self.get_slot(key)].write();
         self.expirations.lock().remove(key);
-        slots
-            .insert(key.clone(), Entry::new(value, None))
+        slot.insert(key.clone(), Entry::new(value, None))
             .filter(|x| x.is_valid())
             .map_or(Value::Null, |x| x.clone_value())
     }
 
     /// Takes an entry from the database.
     pub fn getdel(&self, key: &Bytes) -> Value {
-        let mut slots = self.slots[self.get_slot(key)].write();
-        slots.remove(key).map_or(Value::Null, |x| {
+        let mut slot = self.slots[self.get_slot(key)].write();
+        slot.remove(key).map_or(Value::Null, |x| {
             self.expirations.lock().remove(key);
             x.clone_value()
         })
     }
-    ///
+
     /// Set a key, value with an optional expiration time
     pub fn append(&self, key: &Bytes, value_to_append: &Bytes) -> Result<Value, Error> {
-        let mut slots = self.slots[self.get_slot(key)].write();
-        let mut entry = slots.get_mut(key).filter(|x| x.is_valid());
+        let mut slot = self.slots[self.get_slot(key)].write();
+        let mut entry = slot.get_mut(key).filter(|x| x.is_valid());
 
-        if let Some(entry) = slots.get_mut(key).filter(|x| x.is_valid()) {
+        if let Some(entry) = slot.get_mut(key).filter(|x| x.is_valid()) {
             match entry.get_mut() {
                 Value::Blob(value) => {
                     value.put(value_to_append.as_ref());
@@ -630,7 +778,7 @@ impl Db {
                 _ => Err(Error::WrongType),
             }
         } else {
-            slots.insert(key.clone(), Entry::new(Value::new(value_to_append), None));
+            slot.insert(key.clone(), Entry::new(Value::new(value_to_append), None));
             Ok(value_to_append.len().into())
         }
     }
@@ -641,7 +789,11 @@ impl Db {
     /// If override_all is set to false, all entries must be new entries or the
     /// entire operation fails, in this case 1 or is returned. Otherwise `Ok` is
     /// returned.
-    pub fn multi_set(&self, key_values: &[Bytes], override_all: bool) -> Value {
+    pub fn multi_set(&self, key_values: &[Bytes], override_all: bool) -> Result<Value, Error> {
+        if key_values.len() % 2 == 1 {
+            return Err(Error::Syntax);
+        }
+
         let keys = key_values
             .iter()
             .step_by(2)
@@ -652,17 +804,17 @@ impl Db {
 
         if !override_all {
             for key in keys.iter() {
-                let slots = self.slots[self.get_slot(key)].read();
-                if slots.get(key).is_some() {
+                let slot = self.slots[self.get_slot(key)].read();
+                if slot.get(key).is_some() {
                     self.unlock_keys(&keys);
-                    return 0.into();
+                    return Ok(0.into());
                 }
             }
         }
 
         for (i, _) in key_values.iter().enumerate().step_by(2) {
-            let mut slots = self.slots[self.get_slot(&key_values[i])].write();
-            slots.insert(
+            let mut slot = self.slots[self.get_slot(&key_values[i])].write();
+            slot.insert(
                 key_values[i].clone(),
                 Entry::new(Value::new(&key_values[i + 1]), None),
             );
@@ -671,9 +823,9 @@ impl Db {
         self.unlock_keys(&keys);
 
         if override_all {
-            Value::Ok
+            Ok(Value::Ok)
         } else {
-            1.into()
+            Ok(1.into())
         }
     }
 
@@ -692,9 +844,13 @@ impl Db {
         keep_ttl: bool,
         return_previous: bool,
     ) -> Value {
-        let mut slots = self.slots[self.get_slot(key)].write();
-        let expires_at = expires_in.map(|duration| Instant::now() + duration);
-        let previous = slots.get(key);
+        let mut slot = self.slots[self.get_slot(key)].write();
+        let expires_at = expires_in.map(|duration| {
+            Instant::now()
+                .checked_add(duration)
+                .unwrap_or_else(far_future)
+        });
+        let previous = slot.get(key).filter(|x| x.is_valid());
 
         let expires_at = if keep_ttl {
             if let Some(previous) = previous {
@@ -707,7 +863,13 @@ impl Db {
         };
 
         let to_return = if return_previous {
-            Some(previous.map_or(Value::Null, |v| v.clone_value()))
+            let previous = previous.map_or(Value::Null, |v| v.clone_value());
+            if previous.is_err() {
+                // Error while trying to clone the previous value to return, we
+                // must halt and return immediately.
+                return previous;
+            }
+            Some(previous)
         } else {
             None
         };
@@ -715,12 +877,20 @@ impl Db {
         match override_value {
             Override::No => {
                 if previous.is_some() {
-                    return 0.into();
+                    return if let Some(to_return) = to_return {
+                        to_return
+                    } else {
+                        0.into()
+                    };
                 }
             }
             Override::Only => {
                 if previous.is_none() {
-                    return 0.into();
+                    return if let Some(to_return) = to_return {
+                        to_return
+                    } else {
+                        0.into()
+                    };
                 }
             }
             _ => {}
@@ -734,7 +904,7 @@ impl Db {
             self.expirations.lock().remove(key);
         }
 
-        slots.insert(key.clone(), Entry::new(value, expires_at));
+        slot.insert(key.clone(), Entry::new(value, expires_at));
 
         if let Some(to_return) = to_return {
             to_return
@@ -747,8 +917,8 @@ impl Db {
 
     /// Returns the TTL of a given key
     pub fn ttl(&self, key: &Bytes) -> Option<Option<Instant>> {
-        let slots = self.slots[self.get_slot(key)].read();
-        slots.get(key).filter(|x| x.is_valid()).map(|x| x.get_ttl())
+        let slot = self.slots[self.get_slot(key)].read();
+        slot.get(key).filter(|x| x.is_valid()).map(|x| x.get_ttl())
     }
 
     /// Check whether a given key is in the list of keys to be purged or not.
@@ -775,8 +945,8 @@ impl Db {
 
         keys.iter()
             .map(|key| {
-                let mut slots = self.slots[self.get_slot(key)].write();
-                if slots.remove(key).is_some() {
+                let mut slot = self.slots[self.get_slot(key)].write();
+                if slot.remove(key).is_some() {
                     trace!("Removed key {:?} due timeout", key);
                     removed += 1;
                 }
@@ -861,7 +1031,7 @@ impl scan::Scan for Db {
 #[cfg(test)]
 mod test {
     use super::*;
-    use crate::{bytes, db::scan::Scan};
+    use crate::{bytes, db::scan::Scan, value::float::Float};
     use std::str::FromStr;
 
     #[test]
@@ -881,7 +1051,7 @@ mod test {
         let db = Db::new(100);
         db.set(&bytes!(b"num"), Value::Blob(bytes!("1.1")), None);
 
-        assert_eq!(Ok(Value::Float(2.2)), db.incr(&bytes!("num"), 1.1));
+        assert_eq!(Ok(2.2.into()), db.incr::<Float>(&bytes!("num"), 1.1.into()));
         assert_eq!(Value::Blob(bytes!("2.2")), db.get(&bytes!("num")));
     }
 
@@ -890,7 +1060,7 @@ mod test {
         let db = Db::new(100);
         db.set(&bytes!(b"num"), Value::Blob(bytes!("1")), None);
 
-        assert_eq!(Ok(Value::Float(2.1)), db.incr(&bytes!("num"), 1.1));
+        assert_eq!(Ok(2.1.into()), db.incr::<Float>(&bytes!("num"), 1.1.into()));
         assert_eq!(Value::Blob(bytes!("2.1")), db.get(&bytes!("num")));
     }
 
@@ -899,21 +1069,21 @@ mod test {
         let db = Db::new(100);
         db.set(&bytes!(b"num"), Value::Blob(bytes!("1")), None);
 
-        assert_eq!(Ok(Value::Integer(2)), db.incr(&bytes!("num"), 1));
+        assert_eq!(Ok(2), db.incr(&bytes!("num"), 1));
         assert_eq!(Value::Blob(bytes!("2")), db.get(&bytes!("num")));
     }
 
     #[test]
     fn incr_blob_int_set() {
         let db = Db::new(100);
-        assert_eq!(Ok(Value::Integer(1)), db.incr(&bytes!("num"), 1));
+        assert_eq!(Ok(1), db.incr(&bytes!("num"), 1));
         assert_eq!(Value::Blob(bytes!("1")), db.get(&bytes!("num")));
     }
 
     #[test]
     fn incr_blob_float_set() {
         let db = Db::new(100);
-        assert_eq!(Ok(Value::Float(1.1)), db.incr(&bytes!("num"), 1.1));
+        assert_eq!(Ok(1.1.into()), db.incr::<Float>(&bytes!("num"), 1.1.into()));
         assert_eq!(Value::Blob(bytes!("1.1")), db.get(&bytes!("num")));
     }
 

+ 65 - 0
src/db/utils.rs

@@ -0,0 +1,65 @@
+use crate::error::Error;
+use std::convert::TryFrom;
+use tokio::time::{Duration, Instant};
+
+pub(crate) fn far_future() -> Instant {
+    Instant::now() + Duration::from_secs(86400 * 365 * 30)
+}
+
+/// Override database entries
+#[derive(PartialEq, Debug, Clone, Copy)]
+pub enum Override {
+    /// Allow override
+    Yes,
+    /// Do not allow override, only new entries
+    No,
+    /// Allow only override
+    Only,
+}
+
+impl From<bool> for Override {
+    fn from(v: bool) -> Override {
+        if v {
+            Override::Yes
+        } else {
+            Override::No
+        }
+    }
+}
+
+impl Default for Override {
+    fn default() -> Self {
+        Self::Yes
+    }
+}
+
+/// Override database entries
+#[derive(PartialEq, Debug, Clone, Copy, Default)]
+pub struct ExpirationOpts {
+    /// Set expiry only when the key has no expiry
+    pub NX: bool,
+    /// Set expiry only when the key has an existing expiry
+    pub XX: bool,
+    /// Set expiry only when the new expiry is greater than current one
+    pub GT: bool,
+    /// Set expiry only when the new expiry is less than current one
+    pub LT: bool,
+}
+
+impl TryFrom<&[bytes::Bytes]> for ExpirationOpts {
+    type Error = Error;
+
+    fn try_from(args: &[bytes::Bytes]) -> Result<Self, Self::Error> {
+        let mut expiration_opts = Self::default();
+        for arg in args.iter() {
+            match String::from_utf8_lossy(arg).to_uppercase().as_str() {
+                "NX" => expiration_opts.NX = true,
+                "XX" => expiration_opts.XX = true,
+                "GT" => expiration_opts.GT = true,
+                "LT" => expiration_opts.LT = true,
+                invalid => return Err(Error::UnsupportedOption(invalid.to_owned())),
+            }
+        }
+        Ok(expiration_opts)
+    }
+}

+ 1 - 1
src/dispatcher/command.rs

@@ -131,7 +131,7 @@ impl Command {
 
     /// Can this command be executed in a pub-sub only mode?
     pub fn is_pubsub_executable(&self) -> bool {
-        self.group == "pubsub" || self.name == "ping" || self.name == "reset"
+        self.group == "pubsub" || self.name == "PING" || self.name == "RESET" || self.name == "QUIT"
     }
 
     /// Can this command be queued in a transaction or should it be executed right away?

+ 58 - 4
src/dispatcher/mod.rs

@@ -244,7 +244,7 @@ dispatcher! {
         LPOS {
             cmd::list::lpos,
             [Flag::ReadOnly],
-            -2,
+            -3,
             1,
             1,
             1,
@@ -379,7 +379,7 @@ dispatcher! {
             true,
         },
         HINCRBY {
-            cmd::hash::hincrby::<i64>,
+            cmd::hash::hincrby_int,
             [Flag::Write Flag::DenyOom Flag::Fast],
             4,
             1,
@@ -388,7 +388,7 @@ dispatcher! {
             true,
         },
         HINCRBYFLOAT {
-            cmd::hash::hincrby::<f64>,
+            cmd::hash::hincrby_float,
             [Flag::Write Flag::DenyOom Flag::Fast],
             4,
             1,
@@ -509,7 +509,7 @@ dispatcher! {
         EXPIRE {
             cmd::key::expire,
             [Flag::Write Flag::Fast],
-            3,
+            -3,
             1,
             1,
             1,
@@ -1011,6 +1011,51 @@ dispatcher! {
             0,
             true,
         },
+        DBSIZE {
+            cmd::server::dbsize,
+            [Flag::ReadOnly Flag::Fast],
+            1,
+            0,
+            0,
+            0,
+            true,
+        },
+        DEBUG {
+            cmd::server::debug,
+            [Flag::Random Flag::Loading Flag::Stale],
+            -2,
+            0,
+            0,
+            0,
+            true,
+        },
+        INFO {
+            cmd::server::info,
+            [Flag::Random Flag::Loading Flag::Stale],
+            -1,
+            0,
+            0,
+            0,
+            true,
+        },
+        FLUSHALL {
+            cmd::server::flushall,
+            [Flag::Write],
+            -1,
+            0,
+            0,
+            0,
+            true,
+        },
+        FLUSHDB {
+            cmd::server::flushdb,
+            [Flag::Write],
+            -1,
+            0,
+            0,
+            0,
+            true,
+        },
         TIME {
             cmd::server::time,
             [Flag::Random Flag::Loading Flag::Stale Flag::Fast],
@@ -1020,5 +1065,14 @@ dispatcher! {
             0,
             true,
         },
+        QUIT {
+            cmd::server::quit,
+            [Flag::Random Flag::Loading Flag::Stale Flag::Fast],
+            1,
+            0,
+            0,
+            0,
+            false,
+        },
     }
 }

+ 70 - 47
src/error.rs

@@ -13,60 +13,109 @@ pub enum Error {
     /// Config
     #[error("Config error {0}")]
     Config(#[from] redis_config_parser::de::Error),
+    /// Empty line
+    #[error("No command provided")]
+    EmptyLine,
     /// A command is not found
-    #[error("Command {0} not found")]
+    #[error("unknown command `{0}`")]
     CommandNotFound(String),
     /// A sub-command is not found
-    #[error("Subcommand {0} / {1} not found")]
+    #[error("Unknown subcommand or wrong number of arguments for '{0}'. Try {1} HELP.")]
     SubCommandNotFound(String, String),
     /// Invalid number of arguments
-    #[error("Invalid number of argumetns for command {0}")]
+    #[error("wrong number of arguments for '{0}' command")]
     InvalidArgsCount(String),
+    /// Invalid Rank value
+    #[error("{0} can't be zero: use 1 to start from the first match, 2 from the second, ...")]
+    InvalidRank(String),
     /// The glob-pattern is not valid
-    #[error("Invalid pattern {0}")]
+    #[error("'{0}' is not a valid pattern")]
     InvalidPattern(String),
     /// Internal Error
-    #[error("Internal error")]
+    #[error("internal error")]
     Internal,
     /// Protocol error
-    #[error("Protocol error {0} expecting {1}")]
+    #[error("Protocol error: expected '{1}', got '{0}'")]
     Protocol(String, String),
     /// Unexpected argument
-    #[error("Wrong argument {1} for command {0}")]
+    #[error("Unknown subcommand or wrong number of arguments for '{1}'. Try {0} HELP.")]
     WrongArgument(String, String),
+    /// We cannot incr by infinity
+    #[error("increment would produce NaN or Infinity")]
+    IncrByInfOrNan,
+    /// Wrong number of arguments
+    #[error("wrong number of arguments for '{0}' command")]
+    WrongNumberArgument(String),
     /// Key not found
-    #[error("Key not found")]
+    #[error("no such key")]
     NotFound,
     /// Index out of range
-    #[error("Index out of range")]
+    #[error("index out of range")]
     OutOfRange,
+    /// String is bigger than max allowed size
+    #[error("string exceeds maximum allowed size (proto-max-bulk-len)")]
+    MaxAllowedSize,
     /// Attempting to move or copy to the same key
-    #[error("Cannot move same key")]
+    #[error("source and destination objects are the same")]
     SameEntry,
-    /// The connection is in pubsub only mode and the current command is not compabible.
-    #[error("Invalid command {0} in pubsub mode")]
+    /// The connection is in pubsub only mode and the current command is not compatible.
+    #[error("Can't execute '{0}': only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT / RESET are allowed in this context")]
     PubsubOnly(String),
     /// Syntax error
-    #[error("Syntax error")]
+    #[error("syntax error")]
     Syntax,
     /// Byte cannot be converted to a number
-    #[error("Not a number")]
+    #[error("value is not a valid number or out of range")]
     NotANumber,
+    /// Not a number with specific number type
+    #[error("value is not {0} or out of range")]
+    NotANumberType(String),
+    /// Number overflow
+    #[error("increment or decrement would overflow")]
+    Overflow,
+    /// Unexpected negative number
+    #[error("{0} is negative")]
+    NegativeNumber(String),
+    /// Invalid expire
+    #[error("invalid expire time in {0}")]
+    InvalidExpire(String),
+    /// Invalid expiration options
+    #[error("GT and LT options at the same time are not compatible")]
+    InvalidExpireOpts,
     /// The connection is not in a transaction
-    #[error("Not in a transaction")]
+    #[error(" without MULTI")]
     NotInTx,
+    /// Transaction was aborted
+    #[error("Transaction discarded because of previous errors.")]
+    TxAborted,
     /// The requested database does not exists
-    #[error("Database does not exists")]
+    #[error("DB index is out of range")]
     NotSuchDatabase,
     /// The connection is in a transaction and nested transactions are not supported
-    #[error("Nested transaction not allowed")]
+    #[error("calls can not be nested")]
     NestedTx,
+    /// Watch is not allowed after a Multi has been called
+    #[error("WATCH inside MULTI is not allowed")]
+    WatchInsideTx,
     /// Wrong data type
-    #[error("Wrong type")]
+    #[error("Operation against a key holding the wrong kind of value")]
     WrongType,
     /// Cursor error
     #[error("Error while creating or parsing the cursor: {0}")]
     Cursor(#[from] crate::value::cursor::Error),
+    /// The connection has been unblocked by another connection and it wants to signal it
+    /// through an error.
+    #[error("client unblocked via CLIENT UNBLOCK")]
+    UnblockByError,
+    /// Options provided are not compatible
+    #[error("{0} options at the same time are not compatible")]
+    OptsNotCompatible(String),
+    /// Unsupported option
+    #[error("Unsupported option {0}")]
+    UnsupportedOption(String),
+    /// Client manual disconnection
+    #[error("Manual disconnection")]
+    Quit,
 }
 
 impl From<std::io::Error> for Error {
@@ -81,37 +130,11 @@ impl From<Error> for Value {
             Error::WrongType => "WRONGTYPE",
             Error::NestedTx => "ERR MULTI",
             Error::NotInTx => "ERR EXEC",
+            Error::TxAborted => "EXECABORT",
+            Error::UnblockByError => "UNBLOCKED",
             _ => "ERR",
         };
 
-        let err_msg = match value {
-            Error::Cursor(_) => "internal error".to_owned(),
-            Error::CommandNotFound(x) => format!("unknown command `{}`", x),
-            Error::SubCommandNotFound(x, y) => format!("Unknown subcommand or wrong number of arguments for '{}'. Try {} HELP.", x, y),
-            Error::InvalidArgsCount(x) => format!("wrong number of arguments for '{}' command", x),
-            Error::InvalidPattern(x) => format!("'{}' is not a valid pattern", x),
-            Error::Internal => "internal error".to_owned(),
-            Error::Protocol(x, y) => format!("Protocol error: expected '{}', got '{}'", x, y),
-            Error::NotInTx => " without MULTI".to_owned(),
-            Error::SameEntry => "source and destination objects are the same".to_owned(),
-            Error::NotANumber => "value is not an integer or out of range".to_owned(),
-            Error::OutOfRange => "index out of range".to_owned(),
-            Error::Syntax => "syntax error".to_owned(),
-            Error::NotFound => "no such key".to_owned(),
-            Error::NotSuchDatabase => "DB index is out of range".to_owned(),
-            Error::NestedTx => "calls can not be nested".to_owned(),
-            Error::Io(io) => format!("io error: {}", io),
-            Error::Config(c) => format!("failed to parse config: {}", c),
-            Error::PubsubOnly(x) => format!("Can't execute '{}': only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT / RESET are allowed in this context", x),
-            Error::WrongArgument(x, y) => format!(
-                "Unknown subcommand or wrong number of arguments for '{}'. Try {} HELP.",
-                y, x
-            ),
-            Error::WrongType => {
-                "Operation against a key holding the wrong kind of value".to_owned()
-            }
-        };
-
-        Value::Err(err_type.to_string(), err_msg)
+        Value::Err(err_type.to_string(), value.to_string())
     }
 }

+ 25 - 4
src/macros.rs

@@ -119,12 +119,20 @@ macro_rules! dispatcher {
             #[inline(always)]
             pub fn execute<'a>(&'a self, conn: &'a Connection, args: &'a [Bytes]) -> futures::future::BoxFuture<'a, Result<Value, Error>> {
                 async move {
-                    let command = String::from_utf8_lossy(&args[0]).to_uppercase();
+                    let command = match args.get(0) {
+                        Some(s) => Ok(String::from_utf8_lossy(s).to_uppercase()),
+                        None => Err(Error::EmptyLine),
+                    }?;
                     match command.as_str() {
                         $($(
                             stringify!($command) => {
+                                //log::info!("Command: {} -> {:?}", stringify!($command), args);
                                 let command = &self.$command;
+                                    let status = conn.status();
                                 if ! command.check_number_args(args.len()) {
+                                    if status == ConnectionStatus::Multi {
+                                        conn.fail_transaction();
+                                    }
                                     Err(Error::InvalidArgsCount(command.name().into()))
                                 } else {
                                     let metrics = command.metrics();
@@ -134,11 +142,12 @@ macro_rules! dispatcher {
                                     let response_time = &metrics.response_time;
                                     let throughput = &metrics.throughput;
 
-                                    let status = conn.status();
                                     if status == ConnectionStatus::Multi && command.is_queueable() {
                                         conn.queue_command(args);
                                         conn.tx_keys(command.get_keys(args));
                                         return Ok(Value::Queued);
+                                    } else if status == ConnectionStatus::FailedTx && command.is_queueable() {
+                                        return Ok(Value::Queued);
                                     } else if status == ConnectionStatus::Pubsub && ! command.is_pubsub_executable() {
                                         return Err(Error::PubsubOnly(stringify!($command).to_owned()));
                                     }
@@ -155,7 +164,12 @@ macro_rules! dispatcher {
                                 }
                             }
                         )+)+,
-                        _ => Err(Error::CommandNotFound(command.into())),
+                        _ => {
+                            if conn.status() == ConnectionStatus::Multi {
+                                conn.fail_transaction();
+                            }
+                            Err(Error::CommandNotFound(command.into()))
+                        },
                     }
                 }.boxed()
             }
@@ -218,13 +232,20 @@ macro_rules! check_arg {
 /// is thrown
 #[macro_export]
 macro_rules! try_get_arg {
-    {$args: tt, $pos: tt} => {{
+    {$args: tt, $pos: expr} => {{
         match $args.get($pos) {
             Some(bytes) => bytes,
             None => return Err(Error::Syntax),
         }
     }}
 }
+/// Reads an argument index as an utf-8 string or return an Error::Syntax
+#[macro_export]
+macro_rules! try_get_arg_str {
+    {$args: tt, $pos: expr} => {{
+        String::from_utf8_lossy(try_get_arg!($args, $pos))
+    }}
+}
 
 /// Convert a stream to a Bytes
 #[macro_export]

+ 11 - 5
src/main.rs

@@ -17,13 +17,19 @@ async fn main() -> Result<(), Error> {
     let logger = Logger::try_with_str(config.log.level.to_string()).unwrap();
 
     if let Some(log_path) = config.log.file.as_ref() {
-        logger
-            .log_to_file(FileSpec::try_from(log_path).unwrap())
-            .start()
-            .unwrap();
+        if log_path.is_empty() {
+            logger.log_to_stdout().start().unwrap();
+        } else {
+            logger
+                .log_to_file(FileSpec::try_from(log_path).unwrap())
+                .start()
+                .unwrap();
+        }
     } else {
-        logger.start().unwrap();
+        logger.log_to_stdout().start().unwrap();
     }
 
+    log::info!("PID: {}", std::process::id());
+
     server::serve(config).await
 }

+ 19 - 4
src/server.rs

@@ -46,7 +46,11 @@ impl Decoder for RedisParser {
             let (unused, val) = match parse_server(src) {
                 Ok((buf, val)) => (buf, val),
                 Err(RedisError::Partial) => return Ok(None),
-                Err(_) => return Err(io::Error::new(io::ErrorKind::Other, "something")),
+                Err(e) => {
+                    log::debug!("{:?}", e);
+
+                    return Err(io::Error::new(io::ErrorKind::Other, "something"));
+                }
             };
             (
                 val.iter().map(|e| Bytes::copy_from_slice(e)).collect(),
@@ -111,7 +115,8 @@ async fn serve_tcp(
     all_connections: Arc<Connections>,
 ) -> Result<(), Error> {
     let listener = TcpListener::bind(addr).await?;
-    info!("Listening on {}", addr);
+    info!("Starting server {}", addr);
+    info!("Ready to accept connections on {}", addr);
     loop {
         match listener.accept().await {
             Ok((socket, addr)) => {
@@ -136,7 +141,10 @@ async fn serve_unixsocket(
     default_db: Arc<Db>,
     all_connections: Arc<Connections>,
 ) -> Result<(), Error> {
-    info!("Listening on unix://{}", file);
+    use std::fs::remove_file;
+
+    info!("Ready to accept connections on unix://{}", file);
+    let _ = remove_file(file);
     let listener = UnixListener::bind(file)?;
     loop {
         match listener.accept().await {
@@ -186,13 +194,20 @@ async fn handle_new_connection<T: AsyncReadExt + AsyncWriteExt + Unpin, A: ToStr
             result = transport.next() => match result {
                 Some(Ok(args)) => match all_connections.get_dispatcher().execute(&conn, &args).await {
                     Ok(result) => {
-                        if conn.status() == ConnectionStatus::Pubsub {
+                        if result == Value::Ignore {
                             continue;
                         }
                         if transport.send(result).await.is_err() {
                             break;
                         }
                     },
+                    Err(Error::EmptyLine) => {
+                        // do nothing
+                    },
+                    Err(Error::Quit) => {
+                        let _ = transport.send(Value::Ok).await;
+                        break;
+                    }
                     Err(err) => {
                         if transport.send(err.into()).await.is_err() {
                             break;

+ 77 - 0
src/value/expiration.rs

@@ -0,0 +1,77 @@
+//! # Expiration timestamp struct
+
+use super::{bytes_to_int, typ};
+use crate::{cmd::now, error::Error};
+use std::{convert::TryInto, time::Duration};
+
+/// Expiration timestamp struct
+pub struct Expiration {
+    millis: u64,
+    /// Is the expiration negative?
+    pub is_negative: bool,
+    command: String,
+}
+
+impl Expiration {
+    /// Creates a new timestamp from a vector of bytes
+    pub fn new(
+        bytes: &[u8],
+        is_milliseconds: bool,
+        is_absolute: bool,
+        command: &[u8],
+    ) -> Result<Self, Error> {
+        let command = String::from_utf8_lossy(command).to_lowercase();
+        let input = bytes_to_int::<i64>(bytes)?;
+        let millis = if is_milliseconds {
+            input
+        } else {
+            input
+                .checked_mul(1_000)
+                .ok_or_else(|| Error::InvalidExpire(command.to_string()))?
+        };
+
+        let base_time = now().as_millis() as i64;
+
+        let millis = if is_absolute {
+            if millis.is_negative() {
+                millis.checked_add(base_time)
+            } else {
+                millis.checked_sub(base_time)
+            }
+            .ok_or_else(|| Error::InvalidExpire(command.to_string()))?
+        } else {
+            if millis.checked_add(base_time).is_none() {
+                return Err(Error::InvalidExpire(command.to_string()));
+            }
+
+            millis
+        };
+
+        Ok(Expiration {
+            millis: millis.abs() as u64,
+            is_negative: millis.is_negative(),
+            command: command.to_string(),
+        })
+    }
+
+    /// Fails if the timestamp is negative
+    pub fn must_be_positive(&self) -> Result<(), Error> {
+        if self.is_negative {
+            Err(Error::InvalidExpire(self.command.to_string()))
+        } else {
+            Ok(())
+        }
+    }
+}
+
+impl TryInto<Duration> for Expiration {
+    type Error = Error;
+
+    fn try_into(self) -> Result<Duration, Self::Error> {
+        if self.is_negative {
+            Err(Error::InvalidExpire(self.command.to_string()))
+        } else {
+            Ok(Duration::from_millis(self.millis))
+        }
+    }
+}

+ 74 - 0
src/value/float.rs

@@ -0,0 +1,74 @@
+//! # Thin wrapper for f64 numbers to provide safe maths (checked_add) for incr/hincr operations
+use num_traits::CheckedAdd;
+use std::{
+    convert::{TryFrom, TryInto},
+    num::ParseFloatError,
+    ops::{Add, Deref},
+    str::FromStr,
+};
+
+use crate::error::Error;
+
+use super::Value;
+
+/// Float struct (a thing wrapper on top of f64)
+#[derive(Copy, Debug, Clone, PartialEq)]
+pub struct Float(f64);
+
+impl Deref for Float {
+    type Target = f64;
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl From<f64> for Float {
+    fn from(f: f64) -> Self {
+        Float(f)
+    }
+}
+
+impl TryFrom<&Value> for Float {
+    type Error = Error;
+
+    fn try_from(value: &Value) -> Result<Self, Self::Error> {
+        Ok(Float(value.try_into()?))
+    }
+}
+
+impl Into<Value> for Float {
+    fn into(self) -> Value {
+        Value::Float(self.0)
+    }
+}
+
+impl Add for Float {
+    type Output = Float;
+    fn add(self, rhs: Self) -> Self::Output {
+        Float(self.0 + rhs.0)
+    }
+}
+
+impl ToString for Float {
+    fn to_string(&self) -> String {
+        self.0.to_string()
+    }
+}
+
+impl FromStr for Float {
+    type Err = ParseFloatError;
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(Float(s.parse::<f64>()?))
+    }
+}
+
+impl CheckedAdd for Float {
+    fn checked_add(&self, v: &Self) -> Option<Self> {
+        let n = self.0 + v.0;
+        if n.is_finite() {
+            Some(Float(n))
+        } else {
+            None
+        }
+    }
+}

+ 90 - 9
src/value/mod.rs

@@ -3,16 +3,20 @@
 //! All redis internal data structures and values are absracted in this mod.
 pub mod checksum;
 pub mod cursor;
+pub mod expiration;
+pub mod float;
 pub mod locked;
 pub mod typ;
 
-use crate::{error::Error, value_try_from, value_vec_try_from};
+use crate::{cmd::now, error::Error, value_try_from, value_vec_try_from};
 use bytes::{Bytes, BytesMut};
 use redis_zero_protocol_parser::Value as ParsedValue;
+use sha2::{Digest, Sha256};
 use std::{
     collections::{HashMap, HashSet, VecDeque},
     convert::{TryFrom, TryInto},
     str::FromStr,
+    time::Duration,
 };
 
 /// Redis Value.
@@ -30,7 +34,7 @@ pub enum Value {
     Array(Vec<Value>),
     /// Bytes/Strings/Binary data
     Blob(BytesMut),
-    /// String. This type does not allowe new lines
+    /// String. This type does not allow new lines
     String(String),
     /// An error
     Err(String, String),
@@ -48,6 +52,33 @@ pub enum Value {
     Queued,
     /// Ok
     Ok,
+    /// Empty response that is not send to the client
+    Ignore,
+}
+
+impl Default for Value {
+    fn default() -> Self {
+        Self::Null
+    }
+}
+
+/// Value debug struct
+#[derive(Debug)]
+pub struct VDebug {
+    /// Value encoding
+    pub encoding: &'static str,
+    /// Length of serialized value
+    pub serialize_len: usize,
+}
+
+impl From<VDebug> for Value {
+    fn from(v: VDebug) -> Self {
+        Value::Blob(format!(
+            "Value at:0x6000004a8840 refcount:1 encoding:{} serializedlength:{} lru:13421257 lru_seconds_idle:367",
+            v.encoding, v.serialize_len
+            ).as_str().into()
+        )
+    }
 }
 
 impl Value {
@@ -57,7 +88,7 @@ impl Value {
     }
 
     /// Returns the internal encoding of the redis
-    pub fn encoding(&self) -> &str {
+    pub fn encoding(&self) -> &'static str {
         match self {
             Self::Hash(_) | Self::Set(_) => "hashtable",
             Self::List(_) => "linkedlist",
@@ -65,6 +96,31 @@ impl Value {
             _ => "embstr",
         }
     }
+
+    /// Is the current value an error?
+    pub fn is_err(&self) -> bool {
+        match self {
+            Self::Err(..) => true,
+            _ => false,
+        }
+    }
+
+    /// Return debug information for the type
+    pub fn debug(&self) -> VDebug {
+        let bytes: Vec<u8> = self.into();
+        VDebug {
+            encoding: self.encoding(),
+            serialize_len: bytes.len(),
+        }
+    }
+
+    /// Returns the hash of the value
+    pub fn digest(&self) -> Vec<u8> {
+        let bytes: Vec<u8> = self.into();
+        let mut hasher = Sha256::new();
+        hasher.update(&bytes);
+        hasher.finalize().to_vec()
+    }
 }
 
 impl From<&Value> for Vec<u8> {
@@ -125,18 +181,21 @@ impl TryFrom<&Value> for f64 {
     }
 }
 
-/// Tries to converts bytes data into a number
+/// Tries to convert bytes data into a number
 ///
-/// If the convertion fails a Error::NotANumber error is returned.
+/// If the conversion fails a Error::NotANumber error is returned.
+#[inline]
 pub fn bytes_to_number<T: FromStr>(bytes: &[u8]) -> Result<T, Error> {
     let x = String::from_utf8_lossy(bytes);
     x.parse::<T>().map_err(|_| Error::NotANumber)
 }
 
-impl From<Value> for Vec<u8> {
-    fn from(value: Value) -> Vec<u8> {
-        (&value).into()
-    }
+/// Tries to convert bytes data into an integer number
+#[inline]
+pub fn bytes_to_int<T: FromStr>(bytes: &[u8]) -> Result<T, Error> {
+    let x = String::from_utf8_lossy(bytes);
+    x.parse::<T>()
+        .map_err(|_| Error::NotANumberType("an integer".to_owned()))
 }
 
 impl<'a> From<&ParsedValue<'a>> for Value {
@@ -167,6 +226,28 @@ impl From<usize> for Value {
     }
 }
 
+impl From<Value> for Vec<u8> {
+    fn from(value: Value) -> Vec<u8> {
+        (&value).into()
+    }
+}
+
+impl From<Option<&Bytes>> for Value {
+    fn from(v: Option<&Bytes>) -> Self {
+        if let Some(v) = v {
+            v.into()
+        } else {
+            Value::Null
+        }
+    }
+}
+
+impl From<&Bytes> for Value {
+    fn from(v: &Bytes) -> Self {
+        Value::new(v)
+    }
+}
+
 impl From<&str> for Value {
     fn from(value: &str) -> Value {
         Value::Blob(value.as_bytes().into())

+ 57 - 0
tests/README.md

@@ -0,0 +1,57 @@
+Redis Test Suite
+================
+
+The normal execution mode of the test suite involves starting and manipulating
+local `redis-server` instances, inspecting process state, log files, etc.
+
+The test suite also supports execution against an external server, which is
+enabled using the `--host` and `--port` parameters. When executing against an
+external server, tests tagged `external:skip` are skipped.
+
+There are additional runtime options that can further adjust the test suite to
+match different external server configurations:
+
+| Option               | Impact                                                   |
+| -------------------- | -------------------------------------------------------- |
+| `--singledb`         | Only use database 0, don't assume others are supported. |
+| `--ignore-encoding`  | Skip all checks for specific encoding.  |
+| `--ignore-digest`    | Skip key value digest validations. |
+| `--cluster-mode`     | Run in strict Redis Cluster compatibility mode. |
+
+Tags
+----
+
+Tags are applied to tests to classify them according to the subsystem they test,
+but also to indicate compatibility with different run modes and required
+capabilities.
+
+Tags can be applied in different context levels:
+* `start_server` context
+* `tags` context that bundles several tests together
+* A single test context.
+
+The following compatibility and capability tags are currently used:
+
+| Tag                       | Indicates |
+| ---------------------     | --------- |
+| `external:skip`           | Not compatible with external servers. |
+| `cluster:skip`            | Not compatible with `--cluster-mode`. |
+| `needs:repl`              | Uses replication and needs to be able to `SYNC` from server. |
+| `needs:debug`             | Uses the `DEBUG` command or other debugging focused commands (like `OBJECT`). |
+| `needs:pfdebug`           | Uses the `PFDEBUG` command. |
+| `needs:config-maxmemory`  | Uses `CONFIG SET` to manipulate memory limit, eviction policies, etc. |
+| `needs:config-resetstat`  | Uses `CONFIG RESETSTAT` to reset statistics. |
+| `needs:reset`             | Uses `RESET` to reset client connections. |
+| `needs:save`              | Uses `SAVE` to create an RDB file. |
+
+When using an external server (`--host` and `--port`), filtering using the
+`external:skip` tags is done automatically.
+
+When using `--cluster-mode`, filtering using the `cluster:skip` tag is done
+automatically.
+
+In addition, it is possible to specify additional configuration. For example, to
+run tests on a server that does not permit `SYNC` use:
+
+    ./runtest --host <host> --port <port> --tags -needs:repl
+

BIN
tests/assets/corrupt_empty_keys.rdb


BIN
tests/assets/corrupt_ziplist.rdb


+ 27 - 0
tests/assets/default.conf

@@ -0,0 +1,27 @@
+# Redis configuration for testing.
+
+always-show-logo yes
+notify-keyspace-events KEA
+daemonize no
+pidfile /var/run/redis.pid
+port 6379
+timeout 0
+bind 127.0.0.1
+loglevel verbose
+logfile ''
+databases 16
+latency-monitor-threshold 1
+
+save 900 1
+save 300 10
+save 60 10000
+
+rdbcompression yes
+dbfilename dump.rdb
+dir ./
+
+slave-serve-stale-data yes
+appendonly no
+appendfsync everysec
+no-appendfsync-on-rewrite no
+activerehashing yes

BIN
tests/assets/encodings.rdb


BIN
tests/assets/hash-ziplist.rdb


BIN
tests/assets/hash-zipmap.rdb


+ 5 - 0
tests/assets/minimal.conf

@@ -0,0 +1,5 @@
+# Minimal configuration for testing.
+always-show-logo yes
+daemonize no
+pidfile /var/run/redis.pid
+loglevel verbose

+ 2 - 0
tests/assets/nodefaultuser.acl

@@ -0,0 +1,2 @@
+user alice on nopass ~* +@all
+user bob on nopass ~* &* +@all

+ 3 - 0
tests/assets/user.acl

@@ -0,0 +1,3 @@
+user alice on allcommands allkeys >alice
+user bob on -@all +@set +acl ~set* >bob
+user default on nopass ~* +@all

+ 177 - 0
tests/cluster/cluster.tcl

@@ -0,0 +1,177 @@
+# Cluster-specific test functions.
+#
+# Copyright (C) 2014 Salvatore Sanfilippo antirez@gmail.com
+# This software is released under the BSD License. See the COPYING file for
+# more information.
+
+# Track cluster configuration as created by create_cluster below
+set ::cluster_master_nodes 0
+set ::cluster_replica_nodes 0
+
+# Returns a parsed CLUSTER NODES output as a list of dictionaries.
+proc get_cluster_nodes id {
+    set lines [split [R $id cluster nodes] "\r\n"]
+    set nodes {}
+    foreach l $lines {
+        set l [string trim $l]
+        if {$l eq {}} continue
+        set args [split $l]
+        set node [dict create \
+            id [lindex $args 0] \
+            addr [lindex $args 1] \
+            flags [split [lindex $args 2] ,] \
+            slaveof [lindex $args 3] \
+            ping_sent [lindex $args 4] \
+            pong_recv [lindex $args 5] \
+            config_epoch [lindex $args 6] \
+            linkstate [lindex $args 7] \
+            slots [lrange $args 8 end] \
+        ]
+        lappend nodes $node
+    }
+    return $nodes
+}
+
+# Test node for flag.
+proc has_flag {node flag} {
+    expr {[lsearch -exact [dict get $node flags] $flag] != -1}
+}
+
+# Returns the parsed myself node entry as a dictionary.
+proc get_myself id {
+    set nodes [get_cluster_nodes $id]
+    foreach n $nodes {
+        if {[has_flag $n myself]} {return $n}
+    }
+    return {}
+}
+
+# Get a specific node by ID by parsing the CLUSTER NODES output
+# of the instance Number 'instance_id'
+proc get_node_by_id {instance_id node_id} {
+    set nodes [get_cluster_nodes $instance_id]
+    foreach n $nodes {
+        if {[dict get $n id] eq $node_id} {return $n}
+    }
+    return {}
+}
+
+# Return the value of the specified CLUSTER INFO field.
+proc CI {n field} {
+    get_info_field [R $n cluster info] $field
+}
+
+# Return the value of the specified INFO field.
+proc s {n field} {
+    get_info_field [R $n info] $field
+}
+
+# Assuming nodes are reest, this function performs slots allocation.
+# Only the first 'n' nodes are used.
+proc cluster_allocate_slots {n} {
+    set slot 16383
+    while {$slot >= 0} {
+        # Allocate successive slots to random nodes.
+        set node [randomInt $n]
+        lappend slots_$node $slot
+        incr slot -1
+    }
+    for {set j 0} {$j < $n} {incr j} {
+        R $j cluster addslots {*}[set slots_${j}]
+    }
+}
+
+# Check that cluster nodes agree about "state", or raise an error.
+proc assert_cluster_state {state} {
+    foreach_redis_id id {
+        if {[instance_is_killed redis $id]} continue
+        wait_for_condition 1000 50 {
+            [CI $id cluster_state] eq $state
+        } else {
+            fail "Cluster node $id cluster_state:[CI $id cluster_state]"
+        }
+    }
+}
+
+# Search the first node starting from ID $first that is not
+# already configured as a slave.
+proc cluster_find_available_slave {first} {
+    foreach_redis_id id {
+        if {$id < $first} continue
+        if {[instance_is_killed redis $id]} continue
+        set me [get_myself $id]
+        if {[dict get $me slaveof] eq {-}} {return $id}
+    }
+    fail "No available slaves"
+}
+
+# Add 'slaves' slaves to a cluster composed of 'masters' masters.
+# It assumes that masters are allocated sequentially from instance ID 0
+# to N-1.
+proc cluster_allocate_slaves {masters slaves} {
+    for {set j 0} {$j < $slaves} {incr j} {
+        set master_id [expr {$j % $masters}]
+        set slave_id [cluster_find_available_slave $masters]
+        set master_myself [get_myself $master_id]
+        R $slave_id cluster replicate [dict get $master_myself id]
+    }
+}
+
+# Create a cluster composed of the specified number of masters and slaves.
+proc create_cluster {masters slaves} {
+    cluster_allocate_slots $masters
+    if {$slaves} {
+        cluster_allocate_slaves $masters $slaves
+    }
+    assert_cluster_state ok
+
+    set ::cluster_master_nodes $masters
+    set ::cluster_replica_nodes $slaves
+}
+
+# Set the cluster node-timeout to all the reachalbe nodes.
+proc set_cluster_node_timeout {to} {
+    foreach_redis_id id {
+        catch {R $id CONFIG SET cluster-node-timeout $to}
+    }
+}
+
+# Check if the cluster is writable and readable. Use node "id"
+# as a starting point to talk with the cluster.
+proc cluster_write_test {id} {
+    set prefix [randstring 20 20 alpha]
+    set port [get_instance_attrib redis $id port]
+    set cluster [redis_cluster 127.0.0.1:$port]
+    for {set j 0} {$j < 100} {incr j} {
+        $cluster set key.$j $prefix.$j
+    }
+    for {set j 0} {$j < 100} {incr j} {
+        assert {[$cluster get key.$j] eq "$prefix.$j"}
+    }
+    $cluster close
+}
+
+# Check if cluster configuration is consistent.
+proc cluster_config_consistent {} {
+    for {set j 0} {$j < $::cluster_master_nodes + $::cluster_replica_nodes} {incr j} {
+        if {$j == 0} {
+            set base_cfg [R $j cluster slots]
+        } else {
+            set cfg [R $j cluster slots]
+            if {$cfg != $base_cfg} {
+                return 0
+            }
+        }
+    }
+
+    return 1
+}
+
+# Wait for cluster configuration to propagate and be consistent across nodes.
+proc wait_for_cluster_propagation {} {
+    wait_for_condition 50 100 {
+        [cluster_config_consistent] eq 1
+    } else {
+        fail "cluster config did not reach a consistent state"
+    }
+}

+ 29 - 0
tests/cluster/run.tcl

@@ -0,0 +1,29 @@
+# Cluster test suite. Copyright (C) 2014 Salvatore Sanfilippo antirez@gmail.com
+# This software is released under the BSD License. See the COPYING file for
+# more information.
+
+cd tests/cluster
+source cluster.tcl
+source ../instances.tcl
+source ../../support/cluster.tcl ; # Redis Cluster client.
+
+set ::instances_count 20 ; # How many instances we use at max.
+set ::tlsdir "../../tls"
+
+proc main {} {
+    parse_options
+    spawn_instance redis $::redis_base_port $::instances_count {
+        "cluster-enabled yes"
+        "appendonly yes"
+    }
+    run_tests
+    cleanup
+    end_tests
+}
+
+if {[catch main e]} {
+    puts $::errorInfo
+    if {$::pause_on_error} pause_on_error
+    cleanup
+    exit 1
+}

+ 59 - 0
tests/cluster/tests/00-base.tcl

@@ -0,0 +1,59 @@
+# Check the basic monitoring and failover capabilities.
+
+source "../tests/includes/init-tests.tcl"
+
+if {$::simulate_error} {
+    test "This test will fail" {
+        fail "Simulated error"
+    }
+}
+
+test "Different nodes have different IDs" {
+    set ids {}
+    set numnodes 0
+    foreach_redis_id id {
+        incr numnodes
+        # Every node should just know itself.
+        set nodeid [dict get [get_myself $id] id]
+        assert {$nodeid ne {}}
+        lappend ids $nodeid
+    }
+    set numids [llength [lsort -unique $ids]]
+    assert {$numids == $numnodes}
+}
+
+test "It is possible to perform slot allocation" {
+    cluster_allocate_slots 5
+}
+
+test "After the join, every node gets a different config epoch" {
+    set trynum 60
+    while {[incr trynum -1] != 0} {
+        # We check that this condition is true for *all* the nodes.
+        set ok 1 ; # Will be set to 0 every time a node is not ok.
+        foreach_redis_id id {
+            set epochs {}
+            foreach n [get_cluster_nodes $id] {
+                lappend epochs [dict get $n config_epoch]
+            }
+            if {[lsort $epochs] != [lsort -unique $epochs]} {
+                set ok 0 ; # At least one collision!
+            }
+        }
+        if {$ok} break
+        after 1000
+        puts -nonewline .
+        flush stdout
+    }
+    if {$trynum == 0} {
+        fail "Config epoch conflict resolution is not working."
+    }
+}
+
+test "Nodes should report cluster_state is ok now" {
+    assert_cluster_state ok
+}
+
+test "It is possible to write and read from the cluster" {
+    cluster_write_test 0
+}

+ 38 - 0
tests/cluster/tests/01-faildet.tcl

@@ -0,0 +1,38 @@
+# Check the basic monitoring and failover capabilities.
+
+source "../tests/includes/init-tests.tcl"
+
+test "Create a 5 nodes cluster" {
+    create_cluster 5 5
+}
+
+test "Cluster should start ok" {
+    assert_cluster_state ok
+}
+
+test "Killing two slave nodes" {
+    kill_instance redis 5
+    kill_instance redis 6
+}
+
+test "Cluster should be still up" {
+    assert_cluster_state ok
+}
+
+test "Killing one master node" {
+    kill_instance redis 0
+}
+
+# Note: the only slave of instance 0 is already down so no
+# failover is possible, that would change the state back to ok.
+test "Cluster should be down now" {
+    assert_cluster_state fail
+}
+
+test "Restarting master node" {
+    restart_instance redis 0
+}
+
+test "Cluster should be up again" {
+    assert_cluster_state ok
+}

+ 65 - 0
tests/cluster/tests/02-failover.tcl

@@ -0,0 +1,65 @@
+# Check the basic monitoring and failover capabilities.
+
+source "../tests/includes/init-tests.tcl"
+
+test "Create a 5 nodes cluster" {
+    create_cluster 5 5
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 0
+}
+
+test "Instance #5 is a slave" {
+    assert {[RI 5 role] eq {slave}}
+}
+
+test "Instance #5 synced with the master" {
+    wait_for_condition 1000 50 {
+        [RI 5 master_link_status] eq {up}
+    } else {
+        fail "Instance #5 master link status is not up"
+    }
+}
+
+set current_epoch [CI 1 cluster_current_epoch]
+
+test "Killing one master node" {
+    kill_instance redis 0
+}
+
+test "Wait for failover" {
+    wait_for_condition 1000 50 {
+        [CI 1 cluster_current_epoch] > $current_epoch
+    } else {
+        fail "No failover detected"
+    }
+}
+
+test "Cluster should eventually be up again" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 1
+}
+
+test "Instance #5 is now a master" {
+    assert {[RI 5 role] eq {master}}
+}
+
+test "Restarting the previously killed master node" {
+    restart_instance redis 0
+}
+
+test "Instance #0 gets converted into a slave" {
+    wait_for_condition 1000 50 {
+        [RI 0 role] eq {slave}
+    } else {
+        fail "Old master was not converted into slave"
+    }
+}

+ 115 - 0
tests/cluster/tests/03-failover-loop.tcl

@@ -0,0 +1,115 @@
+# Failover stress test.
+# In this test a different node is killed in a loop for N
+# iterations. The test checks that certain properties
+# are preserved across iterations.
+
+source "../tests/includes/init-tests.tcl"
+
+test "Create a 5 nodes cluster" {
+    create_cluster 5 5
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+set iterations 20
+set cluster [redis_cluster 127.0.0.1:[get_instance_attrib redis 0 port]]
+
+while {[incr iterations -1]} {
+    set tokill [randomInt 10]
+    set other [expr {($tokill+1)%10}] ; # Some other instance.
+    set key [randstring 20 20 alpha]
+    set val [randstring 20 20 alpha]
+    set role [RI $tokill role]
+    if {$role eq {master}} {
+        set slave {}
+        set myid [dict get [get_myself $tokill] id]
+        foreach_redis_id id {
+            if {$id == $tokill} continue
+            if {[dict get [get_myself $id] slaveof] eq $myid} {
+                set slave $id
+            }
+        }
+        if {$slave eq {}} {
+            fail "Unable to retrieve slave's ID for master #$tokill"
+        }
+    }
+
+    puts "--- Iteration $iterations ---"
+
+    if {$role eq {master}} {
+        test "Wait for slave of #$tokill to sync" {
+            wait_for_condition 1000 50 {
+                [string match {*state=online*} [RI $tokill slave0]]
+            } else {
+                fail "Slave of node #$tokill is not ok"
+            }
+        }
+        set slave_config_epoch [CI $slave cluster_my_epoch]
+    }
+
+    test "Cluster is writable before failover" {
+        for {set i 0} {$i < 100} {incr i} {
+            catch {$cluster set $key:$i $val:$i} err
+            assert {$err eq {OK}}
+        }
+        # Wait for the write to propagate to the slave if we
+        # are going to kill a master.
+        if {$role eq {master}} {
+            R $tokill wait 1 20000
+        }
+    }
+
+    test "Killing node #$tokill" {
+        kill_instance redis $tokill
+    }
+
+    if {$role eq {master}} {
+        test "Wait failover by #$slave with old epoch $slave_config_epoch" {
+            wait_for_condition 1000 50 {
+                [CI $slave cluster_my_epoch] > $slave_config_epoch
+            } else {
+                fail "No failover detected, epoch is still [CI $slave cluster_my_epoch]"
+            }
+        }
+    }
+
+    test "Cluster should eventually be up again" {
+        assert_cluster_state ok
+    }
+
+    test "Cluster is writable again" {
+        for {set i 0} {$i < 100} {incr i} {
+            catch {$cluster set $key:$i:2 $val:$i:2} err
+            assert {$err eq {OK}}
+        }
+    }
+
+    test "Restarting node #$tokill" {
+        restart_instance redis $tokill
+    }
+
+    test "Instance #$tokill is now a slave" {
+        wait_for_condition 1000 50 {
+            [RI $tokill role] eq {slave}
+        } else {
+            fail "Restarted instance is not a slave"
+        }
+    }
+
+    test "We can read back the value we set before" {
+        for {set i 0} {$i < 100} {incr i} {
+            catch {$cluster get $key:$i} err
+            assert {$err eq "$val:$i"}
+            catch {$cluster get $key:$i:2} err
+            assert {$err eq "$val:$i:2"}
+        }
+    }
+}
+
+test "Post condition: current_epoch >= my_epoch everywhere" {
+    foreach_redis_id id {
+        assert {[CI $id cluster_current_epoch] >= [CI $id cluster_my_epoch]}
+    }
+}

+ 194 - 0
tests/cluster/tests/04-resharding.tcl

@@ -0,0 +1,194 @@
+# Failover stress test.
+# In this test a different node is killed in a loop for N
+# iterations. The test checks that certain properties
+# are preserved across iterations.
+
+source "../tests/includes/init-tests.tcl"
+source "../../../tests/support/cli.tcl"
+
+test "Create a 5 nodes cluster" {
+    create_cluster 5 5
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "Enable AOF in all the instances" {
+    foreach_redis_id id {
+        R $id config set appendonly yes
+        # We use "appendfsync no" because it's fast but also guarantees that
+        # write(2) is performed before replying to client.
+        R $id config set appendfsync no
+    }
+
+    foreach_redis_id id {
+        wait_for_condition 1000 500 {
+            [RI $id aof_rewrite_in_progress] == 0 &&
+            [RI $id aof_enabled] == 1
+        } else {
+            fail "Failed to enable AOF on instance #$id"
+        }
+    }
+}
+
+# Return non-zero if the specified PID is about a process still in execution,
+# otherwise 0 is returned.
+proc process_is_running {pid} {
+    # PS should return with an error if PID is non existing,
+    # and catch will return non-zero. We want to return non-zero if
+    # the PID exists, so we invert the return value with expr not operator.
+    expr {![catch {exec ps -p $pid}]}
+}
+
+# Our resharding test performs the following actions:
+#
+# - N commands are sent to the cluster in the course of the test.
+# - Every command selects a random key from key:0 to key:MAX-1.
+# - The operation RPUSH key <randomvalue> is performed.
+# - Tcl remembers into an array all the values pushed to each list.
+# - After N/2 commands, the resharding process is started in background.
+# - The test continues while the resharding is in progress.
+# - At the end of the test, we wait for the resharding process to stop.
+# - Finally the keys are checked to see if they contain the value they should.
+
+set numkeys 50000
+set numops 200000
+set start_node_port [get_instance_attrib redis 0 port]
+set cluster [redis_cluster 127.0.0.1:$start_node_port]
+if {$::tls} {
+    # setup a non-TLS cluster client to the TLS cluster
+    set plaintext_port [get_instance_attrib redis 0 plaintext-port]
+    set cluster_plaintext [redis_cluster 127.0.0.1:$plaintext_port 0]
+    puts "Testing TLS cluster on start node 127.0.0.1:$start_node_port, plaintext port $plaintext_port"
+} else {
+    set cluster_plaintext $cluster
+    puts "Testing using non-TLS cluster"
+}
+catch {unset content}
+array set content {}
+set tribpid {}
+
+test "Cluster consistency during live resharding" {
+    set ele 0
+    for {set j 0} {$j < $numops} {incr j} {
+        # Trigger the resharding once we execute half the ops.
+        if {$tribpid ne {} &&
+            ($j % 10000) == 0 &&
+            ![process_is_running $tribpid]} {
+            set tribpid {}
+        }
+
+        if {$j >= $numops/2 && $tribpid eq {}} {
+            puts -nonewline "...Starting resharding..."
+            flush stdout
+            set target [dict get [get_myself [randomInt 5]] id]
+            set tribpid [lindex [exec \
+                ../../../src/redis-cli --cluster reshard \
+                127.0.0.1:[get_instance_attrib redis 0 port] \
+                --cluster-from all \
+                --cluster-to $target \
+                --cluster-slots 100 \
+                --cluster-yes \
+                {*}[rediscli_tls_config "../../../tests"] \
+                | [info nameofexecutable] \
+                ../tests/helpers/onlydots.tcl \
+                &] 0]
+        }
+
+        # Write random data to random list.
+        set listid [randomInt $numkeys]
+        set key "key:$listid"
+        incr ele
+        # We write both with Lua scripts and with plain commands.
+        # This way we are able to stress Lua -> Redis command invocation
+        # as well, that has tests to prevent Lua to write into wrong
+        # hash slots.
+        # We also use both TLS and plaintext connections.
+        if {$listid % 3 == 0} {
+            $cluster rpush $key $ele
+        } elseif {$listid % 3 == 1} {
+            $cluster_plaintext rpush $key $ele
+        } else {
+            $cluster eval {redis.call("rpush",KEYS[1],ARGV[1])} 1 $key $ele
+        }
+        lappend content($key) $ele
+
+        if {($j % 1000) == 0} {
+            puts -nonewline W; flush stdout
+        }
+    }
+
+    # Wait for the resharding process to end
+    wait_for_condition 1000 500 {
+        [process_is_running $tribpid] == 0
+    } else {
+        fail "Resharding is not terminating after some time."
+    }
+
+}
+
+test "Verify $numkeys keys for consistency with logical content" {
+    # Check that the Redis Cluster content matches our logical content.
+    foreach {key value} [array get content] {
+        if {[$cluster lrange $key 0 -1] ne $value} {
+            fail "Key $key expected to hold '$value' but actual content is [$cluster lrange $key 0 -1]"
+        }
+    }
+}
+
+test "Crash and restart all the instances" {
+    foreach_redis_id id {
+        kill_instance redis $id
+        restart_instance redis $id
+    }
+}
+
+test "Cluster should eventually be up again" {
+    assert_cluster_state ok
+}
+
+test "Verify $numkeys keys after the crash & restart" {
+    # Check that the Redis Cluster content matches our logical content.
+    foreach {key value} [array get content] {
+        if {[$cluster lrange $key 0 -1] ne $value} {
+            fail "Key $key expected to hold '$value' but actual content is [$cluster lrange $key 0 -1]"
+        }
+    }
+}
+
+test "Disable AOF in all the instances" {
+    foreach_redis_id id {
+        R $id config set appendonly no
+    }
+}
+
+test "Verify slaves consistency" {
+    set verified_masters 0
+    foreach_redis_id id {
+        set role [R $id role]
+        lassign $role myrole myoffset slaves
+        if {$myrole eq {slave}} continue
+        set masterport [get_instance_attrib redis $id port]
+        set masterdigest [R $id debug digest]
+        foreach_redis_id sid {
+            set srole [R $sid role]
+            if {[lindex $srole 0] eq {master}} continue
+            if {[lindex $srole 2] != $masterport} continue
+            wait_for_condition 1000 500 {
+                [R $sid debug digest] eq $masterdigest
+            } else {
+                fail "Master and slave data digest are different"
+            }
+            incr verified_masters
+        }
+    }
+    assert {$verified_masters >= 5}
+}
+
+test "Dump sanitization was skipped for migrations" {
+    set verified_masters 0
+    foreach_redis_id id {
+        assert {[RI $id dump_payload_sanitizations] == 0}
+    }
+}

+ 171 - 0
tests/cluster/tests/05-slave-selection.tcl

@@ -0,0 +1,171 @@
+# Slave selection test
+# Check the algorithm trying to pick the slave with the most complete history.
+
+source "../tests/includes/init-tests.tcl"
+
+# Create a cluster with 5 master and 10 slaves, so that we have 2
+# slaves for each master.
+test "Create a 5 nodes cluster" {
+    create_cluster 5 10
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "The first master has actually two slaves" {
+    assert {[llength [lindex [R 0 role] 2]] == 2}
+}
+
+test {Slaves of #0 are instance #5 and #10 as expected} {
+    set port0 [get_instance_attrib redis 0 port]
+    assert {[lindex [R 5 role] 2] == $port0}
+    assert {[lindex [R 10 role] 2] == $port0}
+}
+
+test "Instance #5 and #10 synced with the master" {
+    wait_for_condition 1000 50 {
+        [RI 5 master_link_status] eq {up} &&
+        [RI 10 master_link_status] eq {up}
+    } else {
+        fail "Instance #5 or #10 master link status is not up"
+    }
+}
+
+set cluster [redis_cluster 127.0.0.1:[get_instance_attrib redis 0 port]]
+
+test "Slaves are both able to receive and acknowledge writes" {
+    for {set j 0} {$j < 100} {incr j} {
+        $cluster set $j $j
+    }
+    assert {[R 0 wait 2 60000] == 2}
+}
+
+test "Write data while slave #10 is paused and can't receive it" {
+    # Stop the slave with a multi/exec transaction so that the master will
+    # be killed as soon as it can accept writes again.
+    R 10 multi
+    R 10 debug sleep 10
+    R 10 client kill 127.0.0.1:$port0
+    R 10 deferred 1
+    R 10 exec
+
+    # Write some data the slave can't receive.
+    for {set j 0} {$j < 100} {incr j} {
+        $cluster set $j $j
+    }
+
+    # Prevent the master from accepting new slaves.
+    # Use a large pause value since we'll kill it anyway.
+    R 0 CLIENT PAUSE 60000
+
+    # Wait for the slave to return available again
+    R 10 deferred 0
+    assert {[R 10 read] eq {OK OK}}
+
+    # Kill the master so that a reconnection will not be possible.
+    kill_instance redis 0
+}
+
+test "Wait for instance #5 (and not #10) to turn into a master" {
+    wait_for_condition 1000 50 {
+        [RI 5 role] eq {master}
+    } else {
+        fail "No failover detected"
+    }
+}
+
+test "Wait for the node #10 to return alive before ending the test" {
+    R 10 ping
+}
+
+test "Cluster should eventually be up again" {
+    assert_cluster_state ok
+}
+
+test "Node #10 should eventually replicate node #5" {
+    set port5 [get_instance_attrib redis 5 port]
+    wait_for_condition 1000 50 {
+        ([lindex [R 10 role] 2] == $port5) &&
+        ([lindex [R 10 role] 3] eq {connected})
+    } else {
+        fail "#10 didn't became slave of #5"
+    }
+}
+
+source "../tests/includes/init-tests.tcl"
+
+# Create a cluster with 3 master and 15 slaves, so that we have 5
+# slaves for eatch master.
+test "Create a 3 nodes cluster" {
+    create_cluster 3 15
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "The first master has actually 5 slaves" {
+    assert {[llength [lindex [R 0 role] 2]] == 5}
+}
+
+test {Slaves of #0 are instance #3, #6, #9, #12 and #15 as expected} {
+    set port0 [get_instance_attrib redis 0 port]
+    assert {[lindex [R 3 role] 2] == $port0}
+    assert {[lindex [R 6 role] 2] == $port0}
+    assert {[lindex [R 9 role] 2] == $port0}
+    assert {[lindex [R 12 role] 2] == $port0}
+    assert {[lindex [R 15 role] 2] == $port0}
+}
+
+test {Instance #3, #6, #9, #12 and #15 synced with the master} {
+    wait_for_condition 1000 50 {
+        [RI 3 master_link_status] eq {up} &&
+        [RI 6 master_link_status] eq {up} &&
+        [RI 9 master_link_status] eq {up} &&
+        [RI 12 master_link_status] eq {up} &&
+        [RI 15 master_link_status] eq {up}
+    } else {
+        fail "Instance #3 or #6 or #9 or #12 or #15 master link status is not up"
+    }
+}
+
+proc master_detected {instances} {
+    foreach instance [dict keys $instances] {
+        if {[RI $instance role] eq {master}} {
+            return true
+        }
+    }
+
+    return false
+}
+
+test "New Master down consecutively" {
+    set instances [dict create 0 1 3 1 6 1 9 1 12 1 15 1]
+
+    set loops [expr {[dict size $instances]-1}]
+    for {set i 0} {$i < $loops} {incr i} {
+        set master_id -1
+        foreach instance [dict keys $instances] {
+            if {[RI $instance role] eq {master}} {
+                set master_id $instance
+                break;
+            }
+        }
+
+        if {$master_id eq -1} {
+            fail "no master detected, #loop $i"
+        }
+
+        set instances [dict remove $instances $master_id]
+
+        kill_instance redis $master_id
+        wait_for_condition 1000 50 {
+            [master_detected $instances]
+        } else {
+            fail "No failover detected when master $master_id fails"
+        }
+
+        assert_cluster_state ok
+    }
+}

+ 73 - 0
tests/cluster/tests/06-slave-stop-cond.tcl

@@ -0,0 +1,73 @@
+# Slave stop condition test
+# Check that if there is a disconnection time limit, the slave will not try
+# to failover its master.
+
+source "../tests/includes/init-tests.tcl"
+
+# Create a cluster with 5 master and 5 slaves.
+test "Create a 5 nodes cluster" {
+    create_cluster 5 5
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "The first master has actually one slave" {
+    assert {[llength [lindex [R 0 role] 2]] == 1}
+}
+
+test {Slaves of #0 is instance #5 as expected} {
+    set port0 [get_instance_attrib redis 0 port]
+    assert {[lindex [R 5 role] 2] == $port0}
+}
+
+test "Instance #5 synced with the master" {
+    wait_for_condition 1000 50 {
+        [RI 5 master_link_status] eq {up}
+    } else {
+        fail "Instance #5 master link status is not up"
+    }
+}
+
+test "Lower the slave validity factor of #5 to the value of 2" {
+    assert {[R 5 config set cluster-slave-validity-factor 2] eq {OK}}
+}
+
+test "Break master-slave link and prevent further reconnections" {
+    # Stop the slave with a multi/exec transaction so that the master will
+    # be killed as soon as it can accept writes again.
+    R 5 multi
+    R 5 client kill 127.0.0.1:$port0
+    # here we should sleep 6 or more seconds (node_timeout * slave_validity)
+    # but the actual validity time is actually incremented by the
+    # repl-ping-slave-period value which is 10 seconds by default. So we
+    # need to wait more than 16 seconds.
+    R 5 debug sleep 20
+    R 5 deferred 1
+    R 5 exec
+
+    # Prevent the master from accepting new slaves.
+    # Use a large pause value since we'll kill it anyway.
+    R 0 CLIENT PAUSE 60000
+
+    # Wait for the slave to return available again
+    R 5 deferred 0
+    assert {[R 5 read] eq {OK OK}}
+
+    # Kill the master so that a reconnection will not be possible.
+    kill_instance redis 0
+}
+
+test "Slave #5 is reachable and alive" {
+    assert {[R 5 ping] eq {PONG}}
+}
+
+test "Slave #5 should not be able to failover" {
+    after 10000
+    assert {[RI 5 role] eq {slave}}
+}
+
+test "Cluster should be down" {
+    assert_cluster_state fail
+}

+ 103 - 0
tests/cluster/tests/07-replica-migration.tcl

@@ -0,0 +1,103 @@
+# Replica migration test.
+# Check that orphaned masters are joined by replicas of masters having
+# multiple replicas attached, according to the migration barrier settings.
+
+source "../tests/includes/init-tests.tcl"
+
+# Create a cluster with 5 master and 10 slaves, so that we have 2
+# slaves for each master.
+test "Create a 5 nodes cluster" {
+    create_cluster 5 10
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "Each master should have two replicas attached" {
+    foreach_redis_id id {
+        if {$id < 5} {
+            wait_for_condition 1000 50 {
+                [llength [lindex [R 0 role] 2]] == 2
+            } else {
+                fail "Master #$id does not have 2 slaves as expected"
+            }
+        }
+    }
+}
+
+test "Killing all the slaves of master #0 and #1" {
+    kill_instance redis 5
+    kill_instance redis 10
+    kill_instance redis 6
+    kill_instance redis 11
+    after 4000
+}
+
+foreach_redis_id id {
+    if {$id < 5} {
+        test "Master #$id should have at least one replica" {
+            wait_for_condition 1000 50 {
+                [llength [lindex [R $id role] 2]] >= 1
+            } else {
+                fail "Master #$id has no replicas"
+            }
+        }
+    }
+}
+
+# Now test the migration to a master which used to be a slave, after
+# a failver.
+
+source "../tests/includes/init-tests.tcl"
+
+# Create a cluster with 5 master and 10 slaves, so that we have 2
+# slaves for each master.
+test "Create a 5 nodes cluster" {
+    create_cluster 5 10
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "Kill slave #7 of master #2. Only slave left is #12 now" {
+    kill_instance redis 7
+}
+
+set current_epoch [CI 1 cluster_current_epoch]
+
+test "Killing master node #2, #12 should failover" {
+    kill_instance redis 2
+}
+
+test "Wait for failover" {
+    wait_for_condition 1000 50 {
+        [CI 1 cluster_current_epoch] > $current_epoch
+    } else {
+        fail "No failover detected"
+    }
+}
+
+test "Cluster should eventually be up again" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 1
+}
+
+test "Instance 12 is now a master without slaves" {
+    assert {[RI 12 role] eq {master}}
+}
+
+# The remaining instance is now without slaves. Some other slave
+# should migrate to it.
+
+test "Master #12 should get at least one migrated replica" {
+    wait_for_condition 1000 50 {
+        [llength [lindex [R 12 role] 2]] >= 1
+    } else {
+        fail "Master #12 has no replicas"
+    }
+}

+ 90 - 0
tests/cluster/tests/08-update-msg.tcl

@@ -0,0 +1,90 @@
+# Test UPDATE messages sent by other nodes when the currently authorirative
+# master is unavailable. The test is performed in the following steps:
+#
+# 1) Master goes down.
+# 2) Slave failover and becomes new master.
+# 3) New master is partitioned away.
+# 4) Old master returns.
+# 5) At this point we expect the old master to turn into a slave ASAP because
+#    of the UPDATE messages it will receive from the other nodes when its
+#    configuration will be found to be outdated.
+
+source "../tests/includes/init-tests.tcl"
+
+test "Create a 5 nodes cluster" {
+    create_cluster 5 5
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 0
+}
+
+test "Instance #5 is a slave" {
+    assert {[RI 5 role] eq {slave}}
+}
+
+test "Instance #5 synced with the master" {
+    wait_for_condition 1000 50 {
+        [RI 5 master_link_status] eq {up}
+    } else {
+        fail "Instance #5 master link status is not up"
+    }
+}
+
+set current_epoch [CI 1 cluster_current_epoch]
+
+test "Killing one master node" {
+    kill_instance redis 0
+}
+
+test "Wait for failover" {
+    wait_for_condition 1000 50 {
+        [CI 1 cluster_current_epoch] > $current_epoch
+    } else {
+        fail "No failover detected"
+    }
+}
+
+test "Cluster should eventually be up again" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 1
+}
+
+test "Instance #5 is now a master" {
+    assert {[RI 5 role] eq {master}}
+}
+
+test "Killing the new master #5" {
+    kill_instance redis 5
+}
+
+test "Cluster should be down now" {
+    assert_cluster_state fail
+}
+
+test "Restarting the old master node" {
+    restart_instance redis 0
+}
+
+test "Instance #0 gets converted into a slave" {
+    wait_for_condition 1000 50 {
+        [RI 0 role] eq {slave}
+    } else {
+        fail "Old master was not converted into slave"
+    }
+}
+
+test "Restarting the new master node" {
+    restart_instance redis 5
+}
+
+test "Cluster is up again" {
+    assert_cluster_state ok
+}

+ 40 - 0
tests/cluster/tests/09-pubsub.tcl

@@ -0,0 +1,40 @@
+# Test PUBLISH propagation across the cluster.
+
+source "../tests/includes/init-tests.tcl"
+
+test "Create a 5 nodes cluster" {
+    create_cluster 5 5
+}
+
+proc test_cluster_publish {instance instances} {
+    # Subscribe all the instances but the one we use to send.
+    for {set j 0} {$j < $instances} {incr j} {
+        if {$j != $instance} {
+            R $j deferred 1
+            R $j subscribe testchannel
+            R $j read; # Read the subscribe reply
+        }
+    }
+
+    set data [randomValue]
+    R $instance PUBLISH testchannel $data
+
+    # Read the message back from all the nodes.
+    for {set j 0} {$j < $instances} {incr j} {
+        if {$j != $instance} {
+            set msg [R $j read]
+            assert {$data eq [lindex $msg 2]}
+            R $j unsubscribe testchannel
+            R $j read; # Read the unsubscribe reply
+            R $j deferred 0
+        }
+    }
+}
+
+test "Test publishing to master" {
+    test_cluster_publish 0 10
+}
+
+test "Test publishing to slave" {
+    test_cluster_publish 5 10
+}

+ 192 - 0
tests/cluster/tests/10-manual-failover.tcl

@@ -0,0 +1,192 @@
+# Check the manual failover
+
+source "../tests/includes/init-tests.tcl"
+
+test "Create a 5 nodes cluster" {
+    create_cluster 5 5
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 0
+}
+
+test "Instance #5 is a slave" {
+    assert {[RI 5 role] eq {slave}}
+}
+
+test "Instance #5 synced with the master" {
+    wait_for_condition 1000 50 {
+        [RI 5 master_link_status] eq {up}
+    } else {
+        fail "Instance #5 master link status is not up"
+    }
+}
+
+set current_epoch [CI 1 cluster_current_epoch]
+
+set numkeys 50000
+set numops 10000
+set cluster [redis_cluster 127.0.0.1:[get_instance_attrib redis 0 port]]
+catch {unset content}
+array set content {}
+
+test "Send CLUSTER FAILOVER to #5, during load" {
+    for {set j 0} {$j < $numops} {incr j} {
+        # Write random data to random list.
+        set listid [randomInt $numkeys]
+        set key "key:$listid"
+        set ele [randomValue]
+        # We write both with Lua scripts and with plain commands.
+        # This way we are able to stress Lua -> Redis command invocation
+        # as well, that has tests to prevent Lua to write into wrong
+        # hash slots.
+        if {$listid % 2} {
+            $cluster rpush $key $ele
+        } else {
+           $cluster eval {redis.call("rpush",KEYS[1],ARGV[1])} 1 $key $ele
+        }
+        lappend content($key) $ele
+
+        if {($j % 1000) == 0} {
+            puts -nonewline W; flush stdout
+        }
+
+        if {$j == $numops/2} {R 5 cluster failover}
+    }
+}
+
+test "Wait for failover" {
+    wait_for_condition 1000 50 {
+        [CI 1 cluster_current_epoch] > $current_epoch
+    } else {
+        fail "No failover detected"
+    }
+}
+
+test "Cluster should eventually be up again" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 1
+}
+
+test "Instance #5 is now a master" {
+    assert {[RI 5 role] eq {master}}
+}
+
+test "Verify $numkeys keys for consistency with logical content" {
+    # Check that the Redis Cluster content matches our logical content.
+    foreach {key value} [array get content] {
+        assert {[$cluster lrange $key 0 -1] eq $value}
+    }
+}
+
+test "Instance #0 gets converted into a slave" {
+    wait_for_condition 1000 50 {
+        [RI 0 role] eq {slave}
+    } else {
+        fail "Old master was not converted into slave"
+    }
+}
+
+## Check that manual failover does not happen if we can't talk with the master.
+
+source "../tests/includes/init-tests.tcl"
+
+test "Create a 5 nodes cluster" {
+    create_cluster 5 5
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 0
+}
+
+test "Instance #5 is a slave" {
+    assert {[RI 5 role] eq {slave}}
+}
+
+test "Instance #5 synced with the master" {
+    wait_for_condition 1000 50 {
+        [RI 5 master_link_status] eq {up}
+    } else {
+        fail "Instance #5 master link status is not up"
+    }
+}
+
+test "Make instance #0 unreachable without killing it" {
+    R 0 deferred 1
+    R 0 DEBUG SLEEP 10
+}
+
+test "Send CLUSTER FAILOVER to instance #5" {
+    R 5 cluster failover
+}
+
+test "Instance #5 is still a slave after some time (no failover)" {
+    after 5000
+    assert {[RI 5 role] eq {master}}
+}
+
+test "Wait for instance #0 to return back alive" {
+    R 0 deferred 0
+    assert {[R 0 read] eq {OK}}
+}
+
+## Check with "force" failover happens anyway.
+
+source "../tests/includes/init-tests.tcl"
+
+test "Create a 5 nodes cluster" {
+    create_cluster 5 5
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 0
+}
+
+test "Instance #5 is a slave" {
+    assert {[RI 5 role] eq {slave}}
+}
+
+test "Instance #5 synced with the master" {
+    wait_for_condition 1000 50 {
+        [RI 5 master_link_status] eq {up}
+    } else {
+        fail "Instance #5 master link status is not up"
+    }
+}
+
+test "Make instance #0 unreachable without killing it" {
+    R 0 deferred 1
+    R 0 DEBUG SLEEP 10
+}
+
+test "Send CLUSTER FAILOVER to instance #5" {
+    R 5 cluster failover force
+}
+
+test "Instance #5 is a master after some time" {
+    wait_for_condition 1000 50 {
+        [RI 5 role] eq {master}
+    } else {
+        fail "Instance #5 is not a master after some time regardless of FORCE"
+    }
+}
+
+test "Wait for instance #0 to return back alive" {
+    R 0 deferred 0
+    assert {[R 0 read] eq {OK}}
+}

+ 59 - 0
tests/cluster/tests/11-manual-takeover.tcl

@@ -0,0 +1,59 @@
+# Manual takeover test
+
+source "../tests/includes/init-tests.tcl"
+
+test "Create a 5 nodes cluster" {
+    create_cluster 5 5
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 0
+}
+
+test "Killing majority of master nodes" {
+    kill_instance redis 0
+    kill_instance redis 1
+    kill_instance redis 2
+}
+
+test "Cluster should eventually be down" {
+    assert_cluster_state fail
+}
+
+test "Use takeover to bring slaves back" {
+    R 5 cluster failover takeover
+    R 6 cluster failover takeover
+    R 7 cluster failover takeover
+}
+
+test "Cluster should eventually be up again" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 4
+}
+
+test "Instance #5, #6, #7 are now masters" {
+    assert {[RI 5 role] eq {master}}
+    assert {[RI 6 role] eq {master}}
+    assert {[RI 7 role] eq {master}}
+}
+
+test "Restarting the previously killed master nodes" {
+    restart_instance redis 0
+    restart_instance redis 1
+    restart_instance redis 2
+}
+
+test "Instance #0, #1, #2 gets converted into a slaves" {
+    wait_for_condition 1000 50 {
+        [RI 0 role] eq {slave} && [RI 1 role] eq {slave} && [RI 2 role] eq {slave}
+    } else {
+        fail "Old masters not converted into slaves"
+    }
+}

+ 74 - 0
tests/cluster/tests/12-replica-migration-2.tcl

@@ -0,0 +1,74 @@
+# Replica migration test #2.
+#
+# Check that the status of master that can be targeted by replica migration
+# is acquired again, after being getting slots again, in a cluster where the
+# other masters have slaves.
+
+source "../tests/includes/init-tests.tcl"
+source "../../../tests/support/cli.tcl"
+
+# Create a cluster with 5 master and 15 slaves, to make sure there are no
+# empty masters and make rebalancing simpler to handle during the test.
+test "Create a 5 nodes cluster" {
+    create_cluster 5 15
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "Each master should have at least two replicas attached" {
+    foreach_redis_id id {
+        if {$id < 5} {
+            wait_for_condition 1000 50 {
+                [llength [lindex [R 0 role] 2]] >= 2
+            } else {
+                fail "Master #$id does not have 2 slaves as expected"
+            }
+        }
+    }
+}
+
+test "Set allow-replica-migration yes" {
+    foreach_redis_id id {
+        R $id CONFIG SET cluster-allow-replica-migration yes
+    }
+}
+
+set master0_id [dict get [get_myself 0] id]
+test "Resharding all the master #0 slots away from it" {
+    set output [exec \
+        ../../../src/redis-cli --cluster rebalance \
+        127.0.0.1:[get_instance_attrib redis 0 port] \
+        {*}[rediscli_tls_config "../../../tests"] \
+        --cluster-weight ${master0_id}=0 >@ stdout ]
+
+}
+
+test "Master #0 should lose its replicas" {
+    wait_for_condition 1000 50 {
+        [llength [lindex [R 0 role] 2]] == 0
+    } else {
+        fail "Master #0 still has replicas"
+    }
+}
+
+test "Resharding back some slot to master #0" {
+    # Wait for the cluster config to propagate before attempting a
+    # new resharding.
+    after 10000
+    set output [exec \
+        ../../../src/redis-cli --cluster rebalance \
+        127.0.0.1:[get_instance_attrib redis 0 port] \
+        {*}[rediscli_tls_config "../../../tests"] \
+        --cluster-weight ${master0_id}=.01 \
+        --cluster-use-empty-masters  >@ stdout]
+}
+
+test "Master #0 should re-acquire one or more replicas" {
+    wait_for_condition 1000 50 {
+        [llength [lindex [R 0 role] 2]] >= 1
+    } else {
+        fail "Master #0 has no has replicas"
+    }
+}

+ 71 - 0
tests/cluster/tests/12.1-replica-migration-3.tcl

@@ -0,0 +1,71 @@
+# Replica migration test #2.
+#
+# Check that if 'cluster-allow-replica-migration' is set to 'no', slaves do not
+# migrate when master becomes empty.
+
+source "../tests/includes/init-tests.tcl"
+
+# Create a cluster with 5 master and 15 slaves, to make sure there are no
+# empty masters and make rebalancing simpler to handle during the test.
+test "Create a 5 nodes cluster" {
+    create_cluster 5 15
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "Each master should have at least two replicas attached" {
+    foreach_redis_id id {
+        if {$id < 5} {
+            wait_for_condition 1000 50 {
+                [llength [lindex [R 0 role] 2]] >= 2
+            } else {
+                fail "Master #$id does not have 2 slaves as expected"
+            }
+        }
+    }
+}
+
+test "Set allow-replica-migration no" {
+    foreach_redis_id id {
+        R $id CONFIG SET cluster-allow-replica-migration no
+    }
+}
+
+set master0_id [dict get [get_myself 0] id]
+test "Resharding all the master #0 slots away from it" {
+    set output [exec \
+        ../../../src/redis-cli --cluster rebalance \
+        127.0.0.1:[get_instance_attrib redis 0 port] \
+        {*}[rediscli_tls_config "../../../tests"] \
+        --cluster-weight ${master0_id}=0 >@ stdout ]
+}
+
+test "Wait cluster to be stable" {
+    wait_for_condition 1000 50 {
+        [catch {exec ../../../src/redis-cli --cluster \
+            check 127.0.0.1:[get_instance_attrib redis 0 port] \
+            {*}[rediscli_tls_config "../../../tests"] \
+            }] == 0
+    } else {
+        fail "Cluster doesn't stabilize"
+    }
+}
+
+test "Master #0 still should have its replicas" {
+    assert { [llength [lindex [R 0 role] 2]] >= 2 }
+}
+
+test "Each master should have at least two replicas attached" {
+    foreach_redis_id id {
+        if {$id < 5} {
+            wait_for_condition 1000 50 {
+                [llength [lindex [R 0 role] 2]] >= 2
+            } else {
+                fail "Master #$id does not have 2 slaves as expected"
+            }
+        }
+    }
+}
+

+ 61 - 0
tests/cluster/tests/13-no-failover-option.tcl

@@ -0,0 +1,61 @@
+# Check that the no-failover option works
+
+source "../tests/includes/init-tests.tcl"
+
+test "Create a 5 nodes cluster" {
+    create_cluster 5 5
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 0
+}
+
+test "Instance #5 is a slave" {
+    assert {[RI 5 role] eq {slave}}
+
+    # Configure it to never failover the master
+    R 5 CONFIG SET cluster-slave-no-failover yes
+}
+
+test "Instance #5 synced with the master" {
+    wait_for_condition 1000 50 {
+        [RI 5 master_link_status] eq {up}
+    } else {
+        fail "Instance #5 master link status is not up"
+    }
+}
+
+test "The nofailover flag is propagated" {
+    set slave5_id [dict get [get_myself 5] id]
+
+    foreach_redis_id id {
+        wait_for_condition 1000 50 {
+            [has_flag [get_node_by_id $id $slave5_id] nofailover]
+        } else {
+            fail "Instance $id can't see the nofailover flag of slave"
+        }
+    }
+}
+
+set current_epoch [CI 1 cluster_current_epoch]
+
+test "Killing one master node" {
+    kill_instance redis 0
+}
+
+test "Cluster should be still down after some time" {
+    after 10000
+    assert_cluster_state fail
+}
+
+test "Instance #5 is still a slave" {
+    assert {[RI 5 role] eq {slave}}
+}
+
+test "Restarting the previously killed master node" {
+    restart_instance redis 0
+}

+ 117 - 0
tests/cluster/tests/14-consistency-check.tcl

@@ -0,0 +1,117 @@
+source "../tests/includes/init-tests.tcl"
+source "../../../tests/support/cli.tcl"
+
+test "Create a 5 nodes cluster" {
+    create_cluster 5 5
+}
+
+test "Cluster should start ok" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 0
+}
+
+proc find_non_empty_master {} {
+    set master_id_no {}
+    foreach_redis_id id {
+        if {[RI $id role] eq {master} && [R $id dbsize] > 0} {
+            set master_id_no $id
+        }
+    }
+    return $master_id_no
+}
+
+proc get_one_of_my_replica {id} {
+    set replica_port [lindex [lindex [lindex [R $id role] 2] 0] 1]
+    set replica_id_num [get_instance_id_by_port redis $replica_port]
+    return $replica_id_num
+}
+
+proc cluster_write_keys_with_expire {id ttl} {
+    set prefix [randstring 20 20 alpha]
+    set port [get_instance_attrib redis $id port]
+    set cluster [redis_cluster 127.0.0.1:$port]
+    for {set j 100} {$j < 200} {incr j} {
+        $cluster setex key_expire.$j $ttl $prefix.$j
+    }
+    $cluster close
+}
+
+# make sure that replica who restarts from persistence will load keys
+# that have already expired, critical for correct execution of commands
+# that arrive from the master
+proc test_slave_load_expired_keys {aof} {
+    test "Slave expired keys is loaded when restarted: appendonly=$aof" {
+        set master_id [find_non_empty_master]
+        set replica_id [get_one_of_my_replica $master_id]
+
+        set master_dbsize_0 [R $master_id dbsize]
+        set replica_dbsize_0 [R $replica_id dbsize]
+        assert_equal $master_dbsize_0 $replica_dbsize_0
+
+        # config the replica persistency and rewrite the config file to survive restart
+        # note that this needs to be done before populating the volatile keys since
+        # that triggers and AOFRW, and we rather the AOF file to have 'SET PXAT' commands
+        # rather than an RDB with volatile keys
+        R $replica_id config set appendonly $aof
+        R $replica_id config rewrite
+
+        # fill with 100 keys with 3 second TTL
+        set data_ttl 3
+        cluster_write_keys_with_expire $master_id $data_ttl
+
+        # wait for replica to be in sync with master
+        wait_for_condition 500 10 {
+            [R $replica_id dbsize] eq [R $master_id dbsize]
+        } else {
+            fail "replica didn't sync"
+        }
+        
+        set replica_dbsize_1 [R $replica_id dbsize]
+        assert {$replica_dbsize_1 > $replica_dbsize_0}
+
+        # make replica create persistence file
+        if {$aof == "yes"} {
+            # we need to wait for the initial AOFRW to be done, otherwise
+            # kill_instance (which now uses SIGTERM will fail ("Writing initial AOF, can't exit")
+            wait_for_condition 100 10 {
+                [RI $replica_id aof_rewrite_in_progress] eq 0
+            } else {
+                fail "keys didn't expire"
+            }
+        } else {
+            R $replica_id save
+        }
+
+        # kill the replica (would stay down until re-started)
+        kill_instance redis $replica_id
+
+        # Make sure the master doesn't do active expire (sending DELs to the replica)
+        R $master_id DEBUG SET-ACTIVE-EXPIRE 0
+
+        # wait for all the keys to get logically expired
+        after [expr $data_ttl*1000]
+
+        # start the replica again (loading an RDB or AOF file)
+        restart_instance redis $replica_id
+
+        # make sure the keys are still there
+        set replica_dbsize_3 [R $replica_id dbsize]
+        assert {$replica_dbsize_3 > $replica_dbsize_0}
+        
+        # restore settings
+        R $master_id DEBUG SET-ACTIVE-EXPIRE 1
+
+        # wait for the master to expire all keys and replica to get the DELs
+        wait_for_condition 500 10 {
+            [R $replica_id dbsize] eq $master_dbsize_0
+        } else {
+            fail "keys didn't expire"
+        }
+    }
+}
+
+test_slave_load_expired_keys no
+test_slave_load_expired_keys yes

+ 63 - 0
tests/cluster/tests/15-cluster-slots.tcl

@@ -0,0 +1,63 @@
+source "../tests/includes/init-tests.tcl"
+
+proc cluster_allocate_mixedSlots {n} {
+    set slot 16383
+    while {$slot >= 0} {
+        set node [expr {$slot % $n}]
+        lappend slots_$node $slot
+        incr slot -1
+    }
+    for {set j 0} {$j < $n} {incr j} {
+        R $j cluster addslots {*}[set slots_${j}]
+    }
+}
+
+proc create_cluster_with_mixedSlot {masters slaves} {
+    cluster_allocate_mixedSlots $masters
+    if {$slaves} {
+        cluster_allocate_slaves $masters $slaves
+    }
+    assert_cluster_state ok
+}
+
+test "Create a 5 nodes cluster" {
+    create_cluster_with_mixedSlot 5 15
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 0
+}
+
+test "Instance #5 is a slave" {
+    assert {[RI 5 role] eq {slave}}
+}
+
+test "client do not break when cluster slot" {
+    R 0 config set client-output-buffer-limit "normal 33554432 16777216 60"
+    if { [catch {R 0 cluster slots}] } {
+        fail "output overflow when cluster slots"
+    }
+}
+
+test "client can handle keys with hash tag" {
+    set cluster [redis_cluster 127.0.0.1:[get_instance_attrib redis 0 port]]
+    $cluster set foo{tag} bar
+    $cluster close
+}
+
+if {$::tls} {
+    test {CLUSTER SLOTS from non-TLS client in TLS cluster} {
+        set slots_tls [R 0 cluster slots]
+        set host [get_instance_attrib redis 0 host]
+        set plaintext_port [get_instance_attrib redis 0 plaintext-port]
+        set client_plain [redis $host $plaintext_port 0 0]
+        set slots_plain [$client_plain cluster slots]
+        $client_plain close
+        # Compare the ports in the first row
+        assert_no_match [lindex $slots_tls 0 3 1] [lindex $slots_plain 0 3 1]
+    }
+}

+ 71 - 0
tests/cluster/tests/16-transactions-on-replica.tcl

@@ -0,0 +1,71 @@
+# Check basic transactions on a replica.
+
+source "../tests/includes/init-tests.tcl"
+
+test "Create a primary with a replica" {
+    create_cluster 1 1
+}
+
+test "Cluster should start ok" {
+    assert_cluster_state ok
+}
+
+set primary [Rn 0]
+set replica [Rn 1]
+
+test "Can't read from replica without READONLY" {
+    $primary SET a 1
+    wait_for_ofs_sync $primary $replica
+    catch {$replica GET a} err
+    assert {[string range $err 0 4] eq {MOVED}}
+}
+
+test "Can read from replica after READONLY" {
+    $replica READONLY
+    assert {[$replica GET a] eq {1}}
+}
+
+test "Can perform HSET primary and HGET from replica" {
+    $primary HSET h a 1
+    $primary HSET h b 2
+    $primary HSET h c 3
+    wait_for_ofs_sync $primary $replica
+    assert {[$replica HGET h a] eq {1}}
+    assert {[$replica HGET h b] eq {2}}
+    assert {[$replica HGET h c] eq {3}}
+}
+
+test "Can MULTI-EXEC transaction of HGET operations from replica" {
+    $replica MULTI
+    assert {[$replica HGET h a] eq {QUEUED}}
+    assert {[$replica HGET h b] eq {QUEUED}}
+    assert {[$replica HGET h c] eq {QUEUED}}
+    assert {[$replica EXEC] eq {1 2 3}}
+}
+
+test "MULTI-EXEC with write operations is MOVED" {
+    $replica MULTI
+    catch {$replica HSET h b 4} err
+    assert {[string range $err 0 4] eq {MOVED}}
+    catch {$replica exec} err
+    assert {[string range $err 0 8] eq {EXECABORT}}
+}
+
+test "read-only blocking operations from replica" {
+    set rd [redis_deferring_client redis 1]
+    $rd readonly
+    $rd read
+    $rd XREAD BLOCK 0 STREAMS k 0
+
+    wait_for_condition 1000 50 {
+        [RI 1 blocked_clients] eq {1}
+    } else {
+        fail "client wasn't blocked"
+    }
+
+    $primary XADD k * foo bar
+    set res [$rd read]
+    set res [lindex [lindex [lindex [lindex $res 0] 1] 0] 1]
+    assert {$res eq {foo bar}}
+    $rd close
+}

+ 86 - 0
tests/cluster/tests/17-diskless-load-swapdb.tcl

@@ -0,0 +1,86 @@
+# Check replica can restore database backup correctly if fail to diskless load.
+
+source "../tests/includes/init-tests.tcl"
+
+test "Create a primary with a replica" {
+    create_cluster 1 1
+}
+
+test "Cluster should start ok" {
+    assert_cluster_state ok
+}
+
+test "Cluster is writable" {
+    cluster_write_test 0
+}
+
+test "Right to restore backups when fail to diskless load " {
+    set master [Rn 0]
+    set replica [Rn 1]
+    set master_id 0
+    set replica_id 1
+
+    $replica READONLY
+    $replica config set repl-diskless-load swapdb
+    $replica config set appendonly no
+    $replica config set save ""
+    $replica config rewrite
+    $master config set repl-backlog-size 1024
+    $master config set repl-diskless-sync yes
+    $master config set repl-diskless-sync-delay 0
+    $master config set rdb-key-save-delay 10000
+    $master config set rdbcompression no
+    $master config set appendonly no
+    $master config set save ""
+
+    # Write a key that belongs to slot 0
+    set slot0_key "06S"
+    $master set $slot0_key 1
+    wait_for_ofs_sync $master $replica
+    assert_equal {1} [$replica get $slot0_key]
+    assert_equal $slot0_key [$replica CLUSTER GETKEYSINSLOT 0 1]
+
+    # Save an RDB and kill the replica
+    $replica save
+    kill_instance redis $replica_id
+
+    # Delete the key from master
+    $master del $slot0_key
+
+    # Replica must full sync with master when start because replication
+    # backlog size is very small, and dumping rdb will cost several seconds.
+    set num 10000
+    set value [string repeat A 1024]
+    set rd [redis_deferring_client redis $master_id]
+    for {set j 0} {$j < $num} {incr j} {
+        $rd set $j $value
+    }
+    for {set j 0} {$j < $num} {incr j} {
+        $rd read
+    }
+
+    # Start the replica again
+    restart_instance redis $replica_id
+    $replica READONLY
+
+    # Start full sync, wait till after db is flushed (backed up)
+    wait_for_condition 500 10 {
+        [s $replica_id loading] eq 1
+    } else {
+        fail "Fail to full sync"
+    }
+
+    # Kill master, abort full sync
+    kill_instance redis $master_id
+
+    # Start full sync, wait till the replica detects the disconnection
+    wait_for_condition 500 10 {
+        [s $replica_id loading] eq 0
+    } else {
+        fail "Fail to full sync"
+    }
+
+    # Replica keys and keys to slots map still both are right
+    assert_equal {1} [$replica get $slot0_key]
+    assert_equal $slot0_key [$replica CLUSTER GETKEYSINSLOT 0 1]
+}

+ 45 - 0
tests/cluster/tests/18-info.tcl

@@ -0,0 +1,45 @@
+# Check cluster info stats
+
+source "../tests/includes/init-tests.tcl"
+
+test "Create a primary with a replica" {
+    create_cluster 2 0
+}
+
+test "Cluster should start ok" {
+    assert_cluster_state ok
+}
+
+set primary1 [Rn 0]
+set primary2 [Rn 1]
+
+proc cmdstat {instance cmd} {
+    return [cmdrstat $cmd $instance]
+}
+
+proc errorstat {instance cmd} {
+    return [errorrstat $cmd $instance]
+}
+
+test "errorstats: rejected call due to MOVED Redirection" {
+    $primary1 config resetstat
+    $primary2 config resetstat
+    assert_match {} [errorstat $primary1 MOVED]
+    assert_match {} [errorstat $primary2 MOVED]
+    # we know that one will have a MOVED reply and one will succeed
+    catch {$primary1 set key b} replyP1
+    catch {$primary2 set key b} replyP2
+    # sort servers so we know which one failed
+    if {$replyP1 eq {OK}} {
+        assert_match {MOVED*} $replyP2
+        set pok $primary1
+        set perr $primary2
+    } else {
+        assert_match {MOVED*} $replyP1
+        set pok $primary2
+        set perr $primary1
+    }
+    assert_match {} [errorstat $pok MOVED]
+    assert_match {*count=1*} [errorstat $perr MOVED]
+    assert_match {*calls=0,*,rejected_calls=1,failed_calls=0} [cmdstat $perr set]
+}

+ 71 - 0
tests/cluster/tests/19-cluster-nodes-slots.tcl

@@ -0,0 +1,71 @@
+# Optimize CLUSTER NODES command by generating all nodes slot topology firstly
+
+source "../tests/includes/init-tests.tcl"
+
+proc cluster_allocate_with_continuous_slots {n} {
+    set slot 16383
+    set avg [expr ($slot+1) / $n]
+    while {$slot >= 0} {
+        set node [expr $slot/$avg >= $n ? $n-1 : $slot/$avg]
+        lappend slots_$node $slot
+        incr slot -1
+    }
+    for {set j 0} {$j < $n} {incr j} {
+        R $j cluster addslots {*}[set slots_${j}]
+    }
+}
+
+proc cluster_create_with_continuous_slots {masters slaves} {
+    cluster_allocate_with_continuous_slots $masters
+    if {$slaves} {
+        cluster_allocate_slaves $masters $slaves
+    }
+    assert_cluster_state ok
+}
+
+test "Create a 2 nodes cluster" {
+    cluster_create_with_continuous_slots 2 2
+}
+
+test "Cluster should start ok" {
+    assert_cluster_state ok
+}
+
+set master1 [Rn 0]
+set master2 [Rn 1]
+
+test "Continuous slots distribution" {
+    assert_match "* 0-8191*" [$master1 CLUSTER NODES]
+    assert_match "* 8192-16383*" [$master2 CLUSTER NODES]
+    assert_match "*0 8191*" [$master1 CLUSTER SLOTS]
+    assert_match "*8192 16383*" [$master2 CLUSTER SLOTS]
+
+    $master1 CLUSTER DELSLOTS 4096
+    assert_match "* 0-4095 4097-8191*" [$master1 CLUSTER NODES]
+    assert_match "*0 4095*4097 8191*" [$master1 CLUSTER SLOTS]
+
+
+    $master2 CLUSTER DELSLOTS 12288
+    assert_match "* 8192-12287 12289-16383*" [$master2 CLUSTER NODES]
+    assert_match "*8192 12287*12289 16383*" [$master2 CLUSTER SLOTS]
+}
+
+test "Discontinuous slots distribution" {
+    # Remove middle slots
+    $master1 CLUSTER DELSLOTS 4092 4094
+    assert_match "* 0-4091 4093 4095 4097-8191*" [$master1 CLUSTER NODES]
+    assert_match "*0 4091*4093 4093*4095 4095*4097 8191*" [$master1 CLUSTER SLOTS]
+    $master2 CLUSTER DELSLOTS 12284 12286
+    assert_match "* 8192-12283 12285 12287 12289-16383*" [$master2 CLUSTER NODES]
+    assert_match "*8192 12283*12285 12285*12287 12287*12289 16383*" [$master2 CLUSTER SLOTS]
+
+    # Remove head slots
+    $master1 CLUSTER DELSLOTS 0 2
+    assert_match "* 1 3-4091 4093 4095 4097-8191*" [$master1 CLUSTER NODES]
+    assert_match "*1 1*3 4091*4093 4093*4095 4095*4097 8191*" [$master1 CLUSTER SLOTS]
+
+    # Remove tail slots
+    $master2 CLUSTER DELSLOTS 16380 16382 16383
+    assert_match "* 8192-12283 12285 12287 12289-16379 16381*" [$master2 CLUSTER NODES]
+    assert_match "*8192 12283*12285 12285*12287 12287*12289 16379*16381 16381*" [$master2 CLUSTER SLOTS]
+}

+ 98 - 0
tests/cluster/tests/20-half-migrated-slot.tcl

@@ -0,0 +1,98 @@
+# Tests for fixing migrating slot at all stages:
+# 1. when migration is half inited on "migrating" node
+# 2. when migration is half inited on "importing" node
+# 3. migration inited, but not finished
+# 4. migration is half finished on "migrating" node
+# 5. migration is half finished on "importing" node
+
+# TODO: Test is currently disabled until it is stabilized (fixing the test
+# itself or real issues in Redis).
+
+if {false} {
+source "../tests/includes/init-tests.tcl"
+source "../tests/includes/utils.tcl"
+
+test "Create a 2 nodes cluster" {
+    create_cluster 2 0
+    config_set_all_nodes cluster-allow-replica-migration no
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+set cluster [redis_cluster 127.0.0.1:[get_instance_attrib redis 0 port]]
+catch {unset nodefrom}
+catch {unset nodeto}
+
+proc reset_cluster {} {
+    uplevel 1 {
+        $cluster refresh_nodes_map
+        array set nodefrom [$cluster masternode_for_slot 609]
+        array set nodeto [$cluster masternode_notfor_slot 609]
+    }
+}
+
+reset_cluster
+
+$cluster set aga xyz
+
+test "Half init migration in 'migrating' is fixable" {
+    assert_equal {OK} [$nodefrom(link) cluster setslot 609 migrating $nodeto(id)]
+    fix_cluster $nodefrom(addr)
+    assert_equal "xyz" [$cluster get aga]
+}
+
+test "Half init migration in 'importing' is fixable" {
+    assert_equal {OK} [$nodeto(link) cluster setslot 609 importing $nodefrom(id)]
+    fix_cluster $nodefrom(addr)
+    assert_equal "xyz" [$cluster get aga]
+}
+
+test "Init migration and move key" {
+    assert_equal {OK} [$nodefrom(link) cluster setslot 609 migrating $nodeto(id)]
+    assert_equal {OK} [$nodeto(link) cluster setslot 609 importing $nodefrom(id)]
+    assert_equal {OK} [$nodefrom(link) migrate $nodeto(host) $nodeto(port) aga 0 10000]
+    wait_for_cluster_propagation
+    assert_equal "xyz" [$cluster get aga]
+    fix_cluster $nodefrom(addr)
+    assert_equal "xyz" [$cluster get aga]
+}
+
+reset_cluster
+
+test "Move key again" {
+    wait_for_cluster_propagation
+    assert_equal {OK} [$nodefrom(link) cluster setslot 609 migrating $nodeto(id)]
+    assert_equal {OK} [$nodeto(link) cluster setslot 609 importing $nodefrom(id)]
+    assert_equal {OK} [$nodefrom(link) migrate $nodeto(host) $nodeto(port) aga 0 10000]
+    wait_for_cluster_propagation
+    assert_equal "xyz" [$cluster get aga]
+}
+
+test "Half-finish migration" {
+    # half finish migration on 'migrating' node
+    assert_equal {OK} [$nodefrom(link) cluster setslot 609 node $nodeto(id)]
+    fix_cluster $nodefrom(addr)
+    assert_equal "xyz" [$cluster get aga]
+}
+
+reset_cluster
+
+test "Move key back" {
+    # 'aga' key is in 609 slot
+    assert_equal {OK} [$nodefrom(link) cluster setslot 609 migrating $nodeto(id)]
+    assert_equal {OK} [$nodeto(link) cluster setslot 609 importing $nodefrom(id)]
+    assert_equal {OK} [$nodefrom(link) migrate $nodeto(host) $nodeto(port) aga 0 10000]
+    assert_equal "xyz" [$cluster get aga]
+}
+
+test "Half-finish importing" {
+    # Now we half finish 'importing' node
+    assert_equal {OK} [$nodeto(link) cluster setslot 609 node $nodeto(id)]
+    fix_cluster $nodefrom(addr)
+    assert_equal "xyz" [$cluster get aga]
+}
+
+config_set_all_nodes cluster-allow-replica-migration yes
+}

+ 64 - 0
tests/cluster/tests/21-many-slot-migration.tcl

@@ -0,0 +1,64 @@
+# Tests for many simultaneous migrations.
+
+# TODO: Test is currently disabled until it is stabilized (fixing the test
+# itself or real issues in Redis).
+
+if {false} {
+
+source "../tests/includes/init-tests.tcl"
+source "../tests/includes/utils.tcl"
+
+# TODO: This test currently runs without replicas, as failovers (which may
+# happen on lower-end CI platforms) are still not handled properly by the
+# cluster during slot migration (related to #6339).
+
+test "Create a 10 nodes cluster" {
+    create_cluster 10 0
+    config_set_all_nodes cluster-allow-replica-migration no
+}
+
+test "Cluster is up" {
+    assert_cluster_state ok
+}
+
+set cluster [redis_cluster 127.0.0.1:[get_instance_attrib redis 0 port]]
+catch {unset nodefrom}
+catch {unset nodeto}
+
+$cluster refresh_nodes_map
+
+test "Set many keys" {
+    for {set i 0} {$i < 40000} {incr i} {
+        $cluster set key:$i val:$i
+    }
+}
+
+test "Keys are accessible" {
+    for {set i 0} {$i < 40000} {incr i} {
+        assert { [$cluster get key:$i] eq "val:$i" }
+    }
+}
+
+test "Init migration of many slots" {
+    for {set slot 0} {$slot < 1000} {incr slot} {
+        array set nodefrom [$cluster masternode_for_slot $slot]
+        array set nodeto [$cluster masternode_notfor_slot $slot]
+
+        $nodefrom(link) cluster setslot $slot migrating $nodeto(id)
+        $nodeto(link) cluster setslot $slot importing $nodefrom(id)
+    }
+}
+
+test "Fix cluster" {
+    wait_for_cluster_propagation
+    fix_cluster $nodefrom(addr)
+}
+
+test "Keys are accessible" {
+    for {set i 0} {$i < 40000} {incr i} {
+        assert { [$cluster get key:$i] eq "val:$i" }
+    }
+}
+
+config_set_all_nodes cluster-allow-replica-migration yes
+}

+ 16 - 0
tests/cluster/tests/helpers/onlydots.tcl

@@ -0,0 +1,16 @@
+# Read the standard input and only shows dots in the output, filtering out
+# all the other characters. Designed to avoid bufferization so that when
+# we get the output of redis-trib and want to show just the dots, we'll see
+# the dots as soon as redis-trib will output them.
+
+fconfigure stdin -buffering none
+
+while 1 {
+    set c [read stdin 1]
+    if {$c eq {}} {
+        exit 0; # EOF
+    } elseif {$c eq {.}} {
+        puts -nonewline .
+        flush stdout
+    }
+}

+ 70 - 0
tests/cluster/tests/includes/init-tests.tcl

@@ -0,0 +1,70 @@
+# Initialization tests -- most units will start including this.
+
+test "(init) Restart killed instances" {
+    foreach type {redis} {
+        foreach_${type}_id id {
+            if {[get_instance_attrib $type $id pid] == -1} {
+                puts -nonewline "$type/$id "
+                flush stdout
+                restart_instance $type $id
+            }
+        }
+    }
+}
+
+test "Cluster nodes are reachable" {
+    foreach_redis_id id {
+        # Every node should be reachable.
+        wait_for_condition 1000 50 {
+            ([catch {R $id ping} ping_reply] == 0) &&
+            ($ping_reply eq {PONG})
+        } else {
+            catch {R $id ping} err
+            fail "Node #$id keeps replying '$err' to PING."
+        }
+    }
+}
+
+test "Cluster nodes hard reset" {
+    foreach_redis_id id {
+        if {$::valgrind} {
+            set node_timeout 10000
+        } else {
+            set node_timeout 3000
+        }
+        catch {R $id flushall} ; # May fail for readonly slaves.
+        R $id MULTI
+        R $id cluster reset hard
+        R $id cluster set-config-epoch [expr {$id+1}]
+        R $id EXEC
+        R $id config set cluster-node-timeout $node_timeout
+        R $id config set cluster-slave-validity-factor 10
+        R $id config rewrite
+    }
+}
+
+test "Cluster Join and auto-discovery test" {
+    # Join node 0 with 1, 1 with 2, ... and so forth.
+    # If auto-discovery works all nodes will know every other node
+    # eventually.
+    set ids {}
+    foreach_redis_id id {lappend ids $id}
+    for {set j 0} {$j < [expr [llength $ids]-1]} {incr j} {
+        set a [lindex $ids $j]
+        set b [lindex $ids [expr $j+1]]
+        set b_port [get_instance_attrib redis $b port]
+        R $a cluster meet 127.0.0.1 $b_port
+    }
+
+    foreach_redis_id id {
+        wait_for_condition 1000 50 {
+            [llength [get_cluster_nodes $id]] == [llength $ids]
+        } else {
+            fail "Cluster failed to join into a full mesh."
+        }
+    }
+}
+
+test "Before slots allocation, all nodes report cluster failure" {
+    assert_cluster_state fail
+}

+ 25 - 0
tests/cluster/tests/includes/utils.tcl

@@ -0,0 +1,25 @@
+source "../../../tests/support/cli.tcl"
+
+proc config_set_all_nodes {keyword value} {
+    foreach_redis_id id {
+        R $id config set $keyword $value
+    }
+}
+
+proc fix_cluster {addr} {
+    set code [catch {
+        exec ../../../src/redis-cli {*}[rediscli_tls_config "../../../tests"] --cluster fix $addr << yes
+    } result]
+    if {$code != 0} {
+        puts "redis-cli --cluster fix returns non-zero exit code, output below:\n$result"
+    }
+    # Note: redis-cli --cluster fix may return a non-zero exit code if nodes don't agree,
+    # but we can ignore that and rely on the check below.
+    assert_cluster_state ok
+    wait_for_condition 100 100 {
+        [catch {exec ../../../src/redis-cli {*}[rediscli_tls_config "../../../tests"] --cluster check $addr} result] == 0
+    } else {
+        puts "redis-cli --cluster check returns non-zero exit code, output below:\n$result"
+        fail "Cluster could not settle with configuration"
+    }
+}

+ 2 - 0
tests/cluster/tmp/.gitignore

@@ -0,0 +1,2 @@
+redis_*
+sentinel_*

+ 55 - 0
tests/helpers/bg_block_op.tcl

@@ -0,0 +1,55 @@
+source tests/support/redis.tcl
+source tests/support/util.tcl
+
+set ::tlsdir "tests/tls"
+
+# This function sometimes writes sometimes blocking-reads from lists/sorted
+# sets. There are multiple processes like this executing at the same time
+# so that we have some chance to trap some corner condition if there is
+# a regression. For this to happen it is important that we narrow the key
+# space to just a few elements, and balance the operations so that it is
+# unlikely that lists and zsets just get more data without ever causing
+# blocking.
+proc bg_block_op {host port db ops tls} {
+    set r [redis $host $port 0 $tls]
+    $r client setname LOAD_HANDLER
+    $r select $db
+
+    for {set j 0} {$j < $ops} {incr j} {
+
+        # List side
+        set k list_[randomInt 10]
+        set k2 list_[randomInt 10]
+        set v [randomValue]
+
+        randpath {
+            randpath {
+                $r rpush $k $v
+            } {
+                $r lpush $k $v
+            }
+        } {
+            $r blpop $k 2
+        } {
+            $r blpop $k $k2 2
+        }
+
+        # Zset side
+        set k zset_[randomInt 10]
+        set k2 zset_[randomInt 10]
+        set v1 [randomValue]
+        set v2 [randomValue]
+
+        randpath {
+            $r zadd $k [randomInt 10000] $v
+        } {
+            $r zadd $k [randomInt 10000] $v [randomInt 10000] $v2
+        } {
+            $r bzpopmin $k 2
+        } {
+            $r bzpopmax $k 2
+        }
+    }
+}
+
+bg_block_op [lindex $argv 0] [lindex $argv 1] [lindex $argv 2] [lindex $argv 3] [lindex $argv 4]

+ 13 - 0
tests/helpers/bg_complex_data.tcl

@@ -0,0 +1,13 @@
+source tests/support/redis.tcl
+source tests/support/util.tcl
+
+set ::tlsdir "tests/tls"
+
+proc bg_complex_data {host port db ops tls} {
+    set r [redis $host $port 0 $tls]
+    $r client setname LOAD_HANDLER
+    $r select $db
+    createComplexDataset $r $ops
+}
+
+bg_complex_data [lindex $argv 0] [lindex $argv 1] [lindex $argv 2] [lindex $argv 3] [lindex $argv 4]

+ 58 - 0
tests/helpers/fake_redis_node.tcl

@@ -0,0 +1,58 @@
+# A fake Redis node for replaying predefined/expected traffic with a client.
+#
+# Usage: tclsh fake_redis_node.tcl PORT COMMAND REPLY [ COMMAND REPLY [ ... ] ]
+#
+# Commands are given as space-separated strings, e.g. "GET foo", and replies as
+# RESP-encoded replies minus the trailing \r\n, e.g. "+OK".
+
+set port [lindex $argv 0];
+set expected_traffic [lrange $argv 1 end];
+
+# Reads and parses a command from a socket and returns it as a space-separated
+# string, e.g. "set foo bar".
+proc read_command {sock} {
+    set char [read $sock 1]
+    switch $char {
+        * {
+            set numargs [gets $sock]
+            set result {}
+            for {set i 0} {$i<$numargs} {incr i} {
+                read $sock 1;       # dollar sign
+                set len [gets $sock]
+                set str [read $sock $len]
+                gets $sock;         # trailing \r\n
+                lappend result $str
+            }
+            return $result
+        }
+        {} {
+            # EOF
+            return {}
+        }
+        default {
+            # Non-RESP command
+            set rest [gets $sock]
+            return "$char$rest"
+        }
+    }
+}
+
+proc accept {sock host port} {
+    global expected_traffic
+    foreach {expect_cmd reply} $expected_traffic {
+        if {[eof $sock]} {break}
+        set cmd [read_command $sock]
+        if {[string equal -nocase $cmd $expect_cmd]} {
+            puts $sock $reply
+            flush $sock
+        } else {
+            puts $sock "-ERR unexpected command $cmd"
+            break
+        }
+    }
+    close $sock
+}
+
+socket -server accept $port
+after 5000 set done timeout
+vwait done

+ 18 - 0
tests/helpers/gen_write_load.tcl

@@ -0,0 +1,18 @@
+source tests/support/redis.tcl
+
+set ::tlsdir "tests/tls"
+
+proc gen_write_load {host port seconds tls} {
+    set start_time [clock seconds]
+    set r [redis $host $port 1 $tls]
+    $r client setname LOAD_HANDLER
+    $r select 9
+    while 1 {
+        $r set [expr rand()] [expr rand()]
+        if {[clock seconds]-$start_time > $seconds} {
+            exit 0
+        }
+    }
+}
+
+gen_write_load [lindex $argv 0] [lindex $argv 1] [lindex $argv 2] [lindex $argv 3]

+ 673 - 0
tests/instances.tcl

@@ -0,0 +1,673 @@
+# Multi-instance test framework.
+# This is used in order to test Sentinel and Redis Cluster, and provides
+# basic capabilities for spawning and handling N parallel Redis / Sentinel
+# instances.
+#
+# Copyright (C) 2014 Salvatore Sanfilippo antirez@gmail.com
+# This software is released under the BSD License. See the COPYING file for
+# more information.
+
+package require Tcl 8.5
+
+set tcl_precision 17
+source ../support/redis.tcl
+source ../support/util.tcl
+source ../support/server.tcl
+source ../support/test.tcl
+
+set ::verbose 0
+set ::valgrind 0
+set ::tls 0
+set ::pause_on_error 0
+set ::dont_clean 0
+set ::simulate_error 0
+set ::failed 0
+set ::sentinel_instances {}
+set ::redis_instances {}
+set ::global_config {}
+set ::sentinel_base_port 20000
+set ::redis_base_port 30000
+set ::redis_port_count 1024
+set ::host "127.0.0.1"
+set ::leaked_fds_file [file normalize "tmp/leaked_fds.txt"]
+set ::pids {} ; # We kill everything at exit
+set ::dirs {} ; # We remove all the temp dirs at exit
+set ::run_matching {} ; # If non empty, only tests matching pattern are run.
+
+if {[catch {cd tmp}]} {
+    puts "tmp directory not found."
+    puts "Please run this test from the Redis source root."
+    exit 1
+}
+
+# Execute the specified instance of the server specified by 'type', using
+# the provided configuration file. Returns the PID of the process.
+proc exec_instance {type dirname cfgfile} {
+    if {$type eq "redis"} {
+        set prgname redis-server
+    } elseif {$type eq "sentinel"} {
+        set prgname redis-sentinel
+    } else {
+        error "Unknown instance type."
+    }
+
+    set errfile [file join $dirname err.txt]
+    if {$::valgrind} {
+        set pid [exec valgrind --track-origins=yes --suppressions=../../../src/valgrind.sup --show-reachable=no --show-possibly-lost=no --leak-check=full ../../../src/${prgname} $cfgfile 2>> $errfile &]
+    } else {
+        set pid [exec ../../../src/${prgname} $cfgfile 2>> $errfile &]
+    }
+    return $pid
+}
+
+# Spawn a redis or sentinel instance, depending on 'type'.
+proc spawn_instance {type base_port count {conf {}} {base_conf_file ""}} {
+    for {set j 0} {$j < $count} {incr j} {
+        set port [find_available_port $base_port $::redis_port_count]
+        # plaintext port (only used for TLS cluster)
+        set pport 0
+        # Create a directory for this instance.
+        set dirname "${type}_${j}"
+        lappend ::dirs $dirname
+        catch {exec rm -rf $dirname}
+        file mkdir $dirname
+
+        # Write the instance config file.
+        set cfgfile [file join $dirname $type.conf]
+        if {$base_conf_file ne ""} {
+            file copy -- $base_conf_file $cfgfile
+            set cfg [open $cfgfile a+]
+        } else {
+            set cfg [open $cfgfile w]
+        }
+
+        if {$::tls} {
+            puts $cfg "tls-port $port"
+            puts $cfg "tls-replication yes"
+            puts $cfg "tls-cluster yes"
+            # plaintext port, only used by plaintext clients in a TLS cluster
+            set pport [find_available_port $base_port $::redis_port_count]
+            puts $cfg "port $pport"
+            puts $cfg [format "tls-cert-file %s/../../tls/server.crt" [pwd]]
+            puts $cfg [format "tls-key-file %s/../../tls/server.key" [pwd]]
+            puts $cfg [format "tls-client-cert-file %s/../../tls/client.crt" [pwd]]
+            puts $cfg [format "tls-client-key-file %s/../../tls/client.key" [pwd]]
+            puts $cfg [format "tls-dh-params-file %s/../../tls/redis.dh" [pwd]]
+            puts $cfg [format "tls-ca-cert-file %s/../../tls/ca.crt" [pwd]]
+            puts $cfg "loglevel debug"
+        } else {
+            puts $cfg "port $port"
+        }
+        puts $cfg "dir ./$dirname"
+        puts $cfg "logfile log.txt"
+        # Add additional config files
+        foreach directive $conf {
+            puts $cfg $directive
+        }
+        dict for {name val} $::global_config {
+            puts $cfg "$name $val"
+        }
+        close $cfg
+
+        # Finally exec it and remember the pid for later cleanup.
+        set retry 100
+        while {$retry} {
+            set pid [exec_instance $type $dirname $cfgfile]
+
+            # Check availability
+            if {[server_is_up 127.0.0.1 $port 100] == 0} {
+                puts "Starting $type #$j at port $port failed, try another"
+                incr retry -1
+                set port [find_available_port $base_port $::redis_port_count]
+                set cfg [open $cfgfile a+]
+                if {$::tls} {
+                    puts $cfg "tls-port $port"
+                    set pport [find_available_port $base_port $::redis_port_count]
+                    puts $cfg "port $pport"
+                } else {
+                    puts $cfg "port $port"
+                }
+                close $cfg
+            } else {
+                puts "Starting $type #$j at port $port"
+                lappend ::pids $pid
+                break
+            }
+        }
+
+        # Check availability finally
+        if {[server_is_up $::host $port 100] == 0} {
+            set logfile [file join $dirname log.txt]
+            puts [exec tail $logfile]
+            abort_sentinel_test "Problems starting $type #$j: ping timeout, maybe server start failed, check $logfile"
+        }
+
+        # Push the instance into the right list
+        set link [redis $::host $port 0 $::tls]
+        $link reconnect 1
+        lappend ::${type}_instances [list \
+            pid $pid \
+            host $::host \
+            port $port \
+            plaintext-port $pport \
+            link $link \
+        ]
+    }
+}
+
+proc log_crashes {} {
+    set start_pattern {*REDIS BUG REPORT START*}
+    set logs [glob */log.txt]
+    foreach log $logs {
+        set fd [open $log]
+        set found 0
+        while {[gets $fd line] >= 0} {
+            if {[string match $start_pattern $line]} {
+                puts "\n*** Crash report found in $log ***"
+                set found 1
+            }
+            if {$found} {
+                puts $line
+                incr ::failed
+            }
+        }
+    }
+
+    set logs [glob */err.txt]
+    foreach log $logs {
+        set res [find_valgrind_errors $log true]
+        if {$res != ""} {
+            puts $res
+            incr ::failed
+        }
+    }
+}
+
+proc is_alive pid {
+    if {[catch {exec ps -p $pid} err]} {
+        return 0
+    } else {
+        return 1
+    }
+}
+
+proc stop_instance pid {
+    catch {exec kill $pid}
+    # Node might have been stopped in the test
+    catch {exec kill -SIGCONT $pid}
+    if {$::valgrind} {
+        set max_wait 60000
+    } else {
+        set max_wait 10000
+    }
+    while {[is_alive $pid]} {
+        incr wait 10
+
+        if {$wait >= $max_wait} {
+            puts "Forcing process $pid to exit..."
+            catch {exec kill -KILL $pid}
+        } elseif {$wait % 1000 == 0} {
+            puts "Waiting for process $pid to exit..."
+        }
+        after 10
+    }
+}
+
+proc cleanup {} {
+    puts "Cleaning up..."
+    foreach pid $::pids {
+        puts "killing stale instance $pid"
+        stop_instance $pid
+    }
+    log_crashes
+    if {$::dont_clean} {
+        return
+    }
+    foreach dir $::dirs {
+        catch {exec rm -rf $dir}
+    }
+}
+
+proc abort_sentinel_test msg {
+    incr ::failed
+    puts "WARNING: Aborting the test."
+    puts ">>>>>>>> $msg"
+    if {$::pause_on_error} pause_on_error
+    cleanup
+    exit 1
+}
+
+proc parse_options {} {
+    for {set j 0} {$j < [llength $::argv]} {incr j} {
+        set opt [lindex $::argv $j]
+        set val [lindex $::argv [expr $j+1]]
+        if {$opt eq "--single"} {
+            incr j
+            set ::run_matching "*${val}*"
+        } elseif {$opt eq "--pause-on-error"} {
+            set ::pause_on_error 1
+        } elseif {$opt eq {--dont-clean}} {
+            set ::dont_clean 1
+        } elseif {$opt eq "--fail"} {
+            set ::simulate_error 1
+        } elseif {$opt eq {--valgrind}} {
+            set ::valgrind 1
+        } elseif {$opt eq {--host}} {
+            incr j
+            set ::host ${val}
+        } elseif {$opt eq {--tls}} {
+            package require tls 1.6
+            ::tls::init \
+                -cafile "$::tlsdir/ca.crt" \
+                -certfile "$::tlsdir/client.crt" \
+                -keyfile "$::tlsdir/client.key"
+            set ::tls 1
+        } elseif {$opt eq {--config}} {
+            set val2 [lindex $::argv [expr $j+2]]
+            dict set ::global_config $val $val2
+            incr j 2
+        } elseif {$opt eq "--help"} {
+            puts "--single <pattern>      Only runs tests specified by pattern."
+            puts "--dont-clean            Keep log files on exit."
+            puts "--pause-on-error        Pause for manual inspection on error."
+            puts "--fail                  Simulate a test failure."
+            puts "--valgrind              Run with valgrind."
+            puts "--tls                   Run tests in TLS mode."
+            puts "--host <host>           Use hostname instead of 127.0.0.1."
+            puts "--config <k> <v>        Extra config argument(s)."
+            puts "--help                  Shows this help."
+            exit 0
+        } else {
+            puts "Unknown option $opt"
+            exit 1
+        }
+    }
+}
+
+# If --pause-on-error option was passed at startup this function is called
+# on error in order to give the developer a chance to understand more about
+# the error condition while the instances are still running.
+proc pause_on_error {} {
+    puts ""
+    puts [colorstr yellow "*** Please inspect the error now ***"]
+    puts "\nType \"continue\" to resume the test, \"help\" for help screen.\n"
+    while 1 {
+        puts -nonewline "> "
+        flush stdout
+        set line [gets stdin]
+        set argv [split $line " "]
+        set cmd [lindex $argv 0]
+        if {$cmd eq {continue}} {
+            break
+        } elseif {$cmd eq {show-redis-logs}} {
+            set count 10
+            if {[lindex $argv 1] ne {}} {set count [lindex $argv 1]}
+            foreach_redis_id id {
+                puts "=== REDIS $id ===="
+                puts [exec tail -$count redis_$id/log.txt]
+                puts "---------------------\n"
+            }
+        } elseif {$cmd eq {show-sentinel-logs}} {
+            set count 10
+            if {[lindex $argv 1] ne {}} {set count [lindex $argv 1]}
+            foreach_sentinel_id id {
+                puts "=== SENTINEL $id ===="
+                puts [exec tail -$count sentinel_$id/log.txt]
+                puts "---------------------\n"
+            }
+        } elseif {$cmd eq {ls}} {
+            foreach_redis_id id {
+                puts -nonewline "Redis $id"
+                set errcode [catch {
+                    set str {}
+                    append str "@[RI $id tcp_port]: "
+                    append str "[RI $id role] "
+                    if {[RI $id role] eq {slave}} {
+                        append str "[RI $id master_host]:[RI $id master_port]"
+                    }
+                    set str
+                } retval]
+                if {$errcode} {
+                    puts " -- $retval"
+                } else {
+                    puts $retval
+                }
+            }
+            foreach_sentinel_id id {
+                puts -nonewline "Sentinel $id"
+                set errcode [catch {
+                    set str {}
+                    append str "@[SI $id tcp_port]: "
+                    append str "[join [S $id sentinel get-master-addr-by-name mymaster]]"
+                    set str
+                } retval]
+                if {$errcode} {
+                    puts " -- $retval"
+                } else {
+                    puts $retval
+                }
+            }
+        } elseif {$cmd eq {help}} {
+            puts "ls                     List Sentinel and Redis instances."
+            puts "show-sentinel-logs \[N\] Show latest N lines of logs."
+            puts "show-redis-logs \[N\]    Show latest N lines of logs."
+            puts "S <id> cmd ... arg     Call command in Sentinel <id>."
+            puts "R <id> cmd ... arg     Call command in Redis <id>."
+            puts "SI <id> <field>        Show Sentinel <id> INFO <field>."
+            puts "RI <id> <field>        Show Redis <id> INFO <field>."
+            puts "continue               Resume test."
+        } else {
+            set errcode [catch {eval $line} retval]
+            if {$retval ne {}} {puts "$retval"}
+        }
+    }
+}
+
+# We redefine 'test' as for Sentinel we don't use the server-client
+# architecture for the test, everything is sequential.
+proc test {descr code} {
+    set ts [clock format [clock seconds] -format %H:%M:%S]
+    puts -nonewline "$ts> $descr: "
+    flush stdout
+
+    if {[catch {set retval [uplevel 1 $code]} error]} {
+        incr ::failed
+        if {[string match "assertion:*" $error]} {
+            set msg [string range $error 10 end]
+            puts [colorstr red $msg]
+            if {$::pause_on_error} pause_on_error
+            puts "(Jumping to next unit after error)"
+            return -code continue
+        } else {
+            # Re-raise, let handler up the stack take care of this.
+            error $error $::errorInfo
+        }
+    } else {
+        puts [colorstr green OK]
+    }
+}
+
+# Check memory leaks when running on OSX using the "leaks" utility.
+proc check_leaks instance_types {
+    if {[string match {*Darwin*} [exec uname -a]]} {
+        puts -nonewline "Testing for memory leaks..."; flush stdout
+        foreach type $instance_types {
+            foreach_instance_id [set ::${type}_instances] id {
+                if {[instance_is_killed $type $id]} continue
+                set pid [get_instance_attrib $type $id pid]
+                set output {0 leaks}
+                catch {exec leaks $pid} output
+                if {[string match {*process does not exist*} $output] ||
+                    [string match {*cannot examine*} $output]} {
+                    # In a few tests we kill the server process.
+                    set output "0 leaks"
+                } else {
+                    puts -nonewline "$type/$pid "
+                    flush stdout
+                }
+                if {![string match {*0 leaks*} $output]} {
+                    puts [colorstr red "=== MEMORY LEAK DETECTED ==="]
+                    puts "Instance type $type, ID $id:"
+                    puts $output
+                    puts "==="
+                    incr ::failed
+                }
+            }
+        }
+        puts ""
+    }
+}
+
+# Execute all the units inside the 'tests' directory.
+proc run_tests {} {
+    set tests [lsort [glob ../tests/*]]
+    foreach test $tests {
+        # Remove leaked_fds file before starting
+        if {$::leaked_fds_file != "" && [file exists $::leaked_fds_file]} {
+            file delete $::leaked_fds_file
+        }
+
+        if {$::run_matching ne {} && [string match $::run_matching $test] == 0} {
+            continue
+        }
+        if {[file isdirectory $test]} continue
+        puts [colorstr yellow "Testing unit: [lindex [file split $test] end]"]
+        source $test
+        check_leaks {redis sentinel}
+
+        # Check if a leaked fds file was created and abort the test.
+        if {$::leaked_fds_file != "" && [file exists $::leaked_fds_file]} {
+            puts [colorstr red "ERROR: Sentinel has leaked fds to scripts:"]
+            puts [exec cat $::leaked_fds_file]
+            puts "----"
+            incr ::failed
+        }
+    }
+}
+
+# Print a message and exists with 0 / 1 according to zero or more failures.
+proc end_tests {} {
+    if {$::failed == 0 } {
+        puts "GOOD! No errors."
+        exit 0
+    } else {
+        puts "WARNING $::failed test(s) failed."
+        exit 1
+    }
+}
+
+# The "S" command is used to interact with the N-th Sentinel.
+# The general form is:
+#
+# S <sentinel-id> command arg arg arg ...
+#
+# Example to ping the Sentinel 0 (first instance): S 0 PING
+proc S {n args} {
+    set s [lindex $::sentinel_instances $n]
+    [dict get $s link] {*}$args
+}
+
+# Returns a Redis instance by index.
+# Example:
+#     [Rn 0] info
+proc Rn {n} {
+    return [dict get [lindex $::redis_instances $n] link]
+}
+
+# Like R but to chat with Redis instances.
+proc R {n args} {
+    [Rn $n] {*}$args
+}
+
+proc get_info_field {info field} {
+    set fl [string length $field]
+    append field :
+    foreach line [split $info "\n"] {
+        set line [string trim $line "\r\n "]
+        if {[string range $line 0 $fl] eq $field} {
+            return [string range $line [expr {$fl+1}] end]
+        }
+    }
+    return {}
+}
+
+proc SI {n field} {
+    get_info_field [S $n info] $field
+}
+
+proc RI {n field} {
+    get_info_field [R $n info] $field
+}
+
+proc RPort {n} {
+    if {$::tls} {
+        return [lindex [R $n config get tls-port] 1]
+    } else {
+        return [lindex [R $n config get port] 1]
+    }
+}
+
+# Iterate over IDs of sentinel or redis instances.
+proc foreach_instance_id {instances idvar code} {
+    upvar 1 $idvar id
+    for {set id 0} {$id < [llength $instances]} {incr id} {
+        set errcode [catch {uplevel 1 $code} result]
+        if {$errcode == 1} {
+            error $result $::errorInfo $::errorCode
+        } elseif {$errcode == 4} {
+            continue
+        } elseif {$errcode == 3} {
+            break
+        } elseif {$errcode != 0} {
+            return -code $errcode $result
+        }
+    }
+}
+
+proc foreach_sentinel_id {idvar code} {
+    set errcode [catch {uplevel 1 [list foreach_instance_id $::sentinel_instances $idvar $code]} result]
+    return -code $errcode $result
+}
+
+proc foreach_redis_id {idvar code} {
+    set errcode [catch {uplevel 1 [list foreach_instance_id $::redis_instances $idvar $code]} result]
+    return -code $errcode $result
+}
+
+# Get the specific attribute of the specified instance type, id.
+proc get_instance_attrib {type id attrib} {
+    dict get [lindex [set ::${type}_instances] $id] $attrib
+}
+
+# Set the specific attribute of the specified instance type, id.
+proc set_instance_attrib {type id attrib newval} {
+    set d [lindex [set ::${type}_instances] $id]
+    dict set d $attrib $newval
+    lset ::${type}_instances $id $d
+}
+
+# Create a master-slave cluster of the given number of total instances.
+# The first instance "0" is the master, all others are configured as
+# slaves.
+proc create_redis_master_slave_cluster n {
+    foreach_redis_id id {
+        if {$id == 0} {
+            # Our master.
+            R $id slaveof no one
+            R $id flushall
+        } elseif {$id < $n} {
+            R $id slaveof [get_instance_attrib redis 0 host] \
+                          [get_instance_attrib redis 0 port]
+        } else {
+            # Instances not part of the cluster.
+            R $id slaveof no one
+        }
+    }
+    # Wait for all the slaves to sync.
+    wait_for_condition 1000 50 {
+        [RI 0 connected_slaves] == ($n-1)
+    } else {
+        fail "Unable to create a master-slaves cluster."
+    }
+}
+
+proc get_instance_id_by_port {type port} {
+    foreach_${type}_id id {
+        if {[get_instance_attrib $type $id port] == $port} {
+            return $id
+        }
+    }
+    fail "Instance $type port $port not found."
+}
+
+# Kill an instance of the specified type/id with SIGKILL.
+# This function will mark the instance PID as -1 to remember that this instance
+# is no longer running and will remove its PID from the list of pids that
+# we kill at cleanup.
+#
+# The instance can be restarted with restart-instance.
+proc kill_instance {type id} {
+    set pid [get_instance_attrib $type $id pid]
+    set port [get_instance_attrib $type $id port]
+
+    if {$pid == -1} {
+        error "You tried to kill $type $id twice."
+    }
+
+    stop_instance $pid
+    set_instance_attrib $type $id pid -1
+    set_instance_attrib $type $id link you_tried_to_talk_with_killed_instance
+
+    # Remove the PID from the list of pids to kill at exit.
+    set ::pids [lsearch -all -inline -not -exact $::pids $pid]
+
+    # Wait for the port it was using to be available again, so that's not
+    # an issue to start a new server ASAP with the same port.
+    set retry 100
+    while {[incr retry -1]} {
+        set port_is_free [catch {set s [socket 127.0.0.1 $port]}]
+        if {$port_is_free} break
+        catch {close $s}
+        after 100
+    }
+    if {$retry == 0} {
+        error "Port $port does not return available after killing instance."
+    }
+}
+
+# Return true of the instance of the specified type/id is killed.
+proc instance_is_killed {type id} {
+    set pid [get_instance_attrib $type $id pid]
+    expr {$pid == -1}
+}
+
+# Restart an instance previously killed by kill_instance
+proc restart_instance {type id} {
+    set dirname "${type}_${id}"
+    set cfgfile [file join $dirname $type.conf]
+    set port [get_instance_attrib $type $id port]
+
+    # Execute the instance with its old setup and append the new pid
+    # file for cleanup.
+    set pid [exec_instance $type $dirname $cfgfile]
+    set_instance_attrib $type $id pid $pid
+    lappend ::pids $pid
+
+    # Check that the instance is running
+    if {[server_is_up 127.0.0.1 $port 100] == 0} {
+        set logfile [file join $dirname log.txt]
+        puts [exec tail $logfile]
+        abort_sentinel_test "Problems starting $type #$id: ping timeout, maybe server start failed, check $logfile"
+    }
+
+    # Connect with it with a fresh link
+    set link [redis 127.0.0.1 $port 0 $::tls]
+    $link reconnect 1
+    set_instance_attrib $type $id link $link
+
+    # Make sure the instance is not loading the dataset when this
+    # function returns.
+    while 1 {
+        catch {[$link ping]} retval
+        if {[string match {*LOADING*} $retval]} {
+            after 100
+            continue
+        } else {
+            break
+        }
+    }
+}
+
+proc redis_deferring_client {type id} {
+    set port [get_instance_attrib $type $id port]
+    set host [get_instance_attrib $type $id host]
+    set client [redis $host $port 1 $::tls]
+    return $client
+}
+
+proc redis_client {type id} {
+    set port [get_instance_attrib $type $id port]
+    set host [get_instance_attrib $type $id host]
+    set client [redis $host $port 0 $::tls]
+    return $client
+}

+ 36 - 0
tests/integration/aof-race.tcl

@@ -0,0 +1,36 @@
+set defaults { appendonly {yes} appendfilename {appendonly.aof} aof-use-rdb-preamble {no} }
+set server_path [tmpdir server.aof]
+set aof_path "$server_path/appendonly.aof"
+
+proc start_server_aof {overrides code} {
+    upvar defaults defaults srv srv server_path server_path
+    set config [concat $defaults $overrides]
+    start_server [list overrides $config] $code
+}
+
+tags {"aof"} {
+    # Specific test for a regression where internal buffers were not properly
+    # cleaned after a child responsible for an AOF rewrite exited. This buffer
+    # was subsequently appended to the new AOF, resulting in duplicate commands.
+    start_server_aof [list dir $server_path] {
+        set client [redis [srv host] [srv port] 0 $::tls]
+        set bench [open "|src/redis-benchmark -q -s [srv unixsocket] -c 20 -n 20000 incr foo" "r+"]
+
+        after 100
+
+        # Benchmark should be running by now: start background rewrite
+        $client bgrewriteaof
+
+        # Read until benchmark pipe reaches EOF
+        while {[string length [read $bench]] > 0} {}
+
+        # Check contents of foo
+        assert_equal 20000 [$client get foo]
+    }
+
+    # Restart server to replay AOF
+    start_server_aof [list dir $server_path] {
+        set client [redis [srv host] [srv port] 0 $::tls]
+        assert_equal 20000 [$client get foo]
+    }
+}

+ 323 - 0
tests/integration/aof.tcl

@@ -0,0 +1,323 @@
+set defaults { appendonly {yes} appendfilename {appendonly.aof} }
+set server_path [tmpdir server.aof]
+set aof_path "$server_path/appendonly.aof"
+
+proc append_to_aof {str} {
+    upvar fp fp
+    puts -nonewline $fp $str
+}
+
+proc create_aof {code} {
+    upvar fp fp aof_path aof_path
+    set fp [open $aof_path w+]
+    uplevel 1 $code
+    close $fp
+}
+
+proc start_server_aof {overrides code} {
+    upvar defaults defaults srv srv server_path server_path
+    set config [concat $defaults $overrides]
+    set srv [start_server [list overrides $config]]
+    uplevel 1 $code
+    kill_server $srv
+}
+
+tags {"aof external:skip"} {
+    ## Server can start when aof-load-truncated is set to yes and AOF
+    ## is truncated, with an incomplete MULTI block.
+    create_aof {
+        append_to_aof [formatCommand set foo hello]
+        append_to_aof [formatCommand multi]
+        append_to_aof [formatCommand set bar world]
+    }
+
+    start_server_aof [list dir $server_path aof-load-truncated yes] {
+        test "Unfinished MULTI: Server should start if load-truncated is yes" {
+            assert_equal 1 [is_alive $srv]
+        }
+    }
+
+    ## Should also start with truncated AOF without incomplete MULTI block.
+    create_aof {
+        append_to_aof [formatCommand incr foo]
+        append_to_aof [formatCommand incr foo]
+        append_to_aof [formatCommand incr foo]
+        append_to_aof [formatCommand incr foo]
+        append_to_aof [formatCommand incr foo]
+        append_to_aof [string range [formatCommand incr foo] 0 end-1]
+    }
+
+    start_server_aof [list dir $server_path aof-load-truncated yes] {
+        test "Short read: Server should start if load-truncated is yes" {
+            assert_equal 1 [is_alive $srv]
+        }
+
+        test "Truncated AOF loaded: we expect foo to be equal to 5" {
+            set client [redis [dict get $srv host] [dict get $srv port] 0 $::tls]
+            wait_done_loading $client
+            assert {[$client get foo] eq "5"}
+        }
+
+        test "Append a new command after loading an incomplete AOF" {
+            $client incr foo
+        }
+    }
+
+    # Now the AOF file is expected to be correct
+    start_server_aof [list dir $server_path aof-load-truncated yes] {
+        test "Short read + command: Server should start" {
+            assert_equal 1 [is_alive $srv]
+        }
+
+        test "Truncated AOF loaded: we expect foo to be equal to 6 now" {
+            set client [redis [dict get $srv host] [dict get $srv port] 0 $::tls]
+            wait_done_loading $client
+            assert {[$client get foo] eq "6"}
+        }
+    }
+
+    ## Test that the server exits when the AOF contains a format error
+    create_aof {
+        append_to_aof [formatCommand set foo hello]
+        append_to_aof "!!!"
+        append_to_aof [formatCommand set foo hello]
+    }
+
+    start_server_aof [list dir $server_path aof-load-truncated yes] {
+        test "Bad format: Server should have logged an error" {
+            set pattern "*Bad file format reading the append only file*"
+            set retry 10
+            while {$retry} {
+                set result [exec tail -1 < [dict get $srv stdout]]
+                if {[string match $pattern $result]} {
+                    break
+                }
+                incr retry -1
+                after 1000
+            }
+            if {$retry == 0} {
+                error "assertion:expected error not found on config file"
+            }
+        }
+    }
+
+    ## Test the server doesn't start when the AOF contains an unfinished MULTI
+    create_aof {
+        append_to_aof [formatCommand set foo hello]
+        append_to_aof [formatCommand multi]
+        append_to_aof [formatCommand set bar world]
+    }
+
+    start_server_aof [list dir $server_path aof-load-truncated no] {
+        test "Unfinished MULTI: Server should have logged an error" {
+            set pattern "*Unexpected end of file reading the append only file*"
+            set retry 10
+            while {$retry} {
+                set result [exec tail -1 < [dict get $srv stdout]]
+                if {[string match $pattern $result]} {
+                    break
+                }
+                incr retry -1
+                after 1000
+            }
+            if {$retry == 0} {
+                error "assertion:expected error not found on config file"
+            }
+        }
+    }
+
+    ## Test that the server exits when the AOF contains a short read
+    create_aof {
+        append_to_aof [formatCommand set foo hello]
+        append_to_aof [string range [formatCommand set bar world] 0 end-1]
+    }
+
+    start_server_aof [list dir $server_path aof-load-truncated no] {
+        test "Short read: Server should have logged an error" {
+            set pattern "*Unexpected end of file reading the append only file*"
+            set retry 10
+            while {$retry} {
+                set result [exec tail -1 < [dict get $srv stdout]]
+                if {[string match $pattern $result]} {
+                    break
+                }
+                incr retry -1
+                after 1000
+            }
+            if {$retry == 0} {
+                error "assertion:expected error not found on config file"
+            }
+        }
+    }
+
+    ## Test that redis-check-aof indeed sees this AOF is not valid
+    test "Short read: Utility should confirm the AOF is not valid" {
+        catch {
+            exec src/redis-check-aof $aof_path
+        } result
+        assert_match "*not valid*" $result
+    }
+
+    test "Short read: Utility should show the abnormal line num in AOF" {
+        create_aof {
+            append_to_aof [formatCommand set foo hello]
+            append_to_aof "!!!"
+        }
+
+        catch {
+            exec src/redis-check-aof $aof_path
+        } result
+        assert_match "*ok_up_to_line=8*" $result
+    }
+
+    test "Short read: Utility should be able to fix the AOF" {
+        set result [exec src/redis-check-aof --fix $aof_path << "y\n"]
+        assert_match "*Successfully truncated AOF*" $result
+    }
+
+    ## Test that the server can be started using the truncated AOF
+    start_server_aof [list dir $server_path aof-load-truncated no] {
+        test "Fixed AOF: Server should have been started" {
+            assert_equal 1 [is_alive $srv]
+        }
+
+        test "Fixed AOF: Keyspace should contain values that were parseable" {
+            set client [redis [dict get $srv host] [dict get $srv port] 0 $::tls]
+            wait_done_loading $client
+            assert_equal "hello" [$client get foo]
+            assert_equal "" [$client get bar]
+        }
+    }
+
+    ## Test that SPOP (that modifies the client's argc/argv) is correctly free'd
+    create_aof {
+        append_to_aof [formatCommand sadd set foo]
+        append_to_aof [formatCommand sadd set bar]
+        append_to_aof [formatCommand spop set]
+    }
+
+    start_server_aof [list dir $server_path aof-load-truncated no] {
+        test "AOF+SPOP: Server should have been started" {
+            assert_equal 1 [is_alive $srv]
+        }
+
+        test "AOF+SPOP: Set should have 1 member" {
+            set client [redis [dict get $srv host] [dict get $srv port] 0 $::tls]
+            wait_done_loading $client
+            assert_equal 1 [$client scard set]
+        }
+    }
+
+    ## Uses the alsoPropagate() API.
+    create_aof {
+        append_to_aof [formatCommand sadd set foo]
+        append_to_aof [formatCommand sadd set bar]
+        append_to_aof [formatCommand sadd set gah]
+        append_to_aof [formatCommand spop set 2]
+    }
+
+    start_server_aof [list dir $server_path] {
+        test "AOF+SPOP: Server should have been started" {
+            assert_equal 1 [is_alive $srv]
+        }
+
+        test "AOF+SPOP: Set should have 1 member" {
+            set client [redis [dict get $srv host] [dict get $srv port] 0 $::tls]
+            wait_done_loading $client
+            assert_equal 1 [$client scard set]
+        }
+    }
+
+    ## Test that PEXPIREAT is loaded correctly
+    create_aof {
+        append_to_aof [formatCommand rpush list foo]
+        append_to_aof [formatCommand pexpireat list 1000]
+        append_to_aof [formatCommand rpush list bar]
+    }
+
+    start_server_aof [list dir $server_path aof-load-truncated no] {
+        test "AOF+EXPIRE: Server should have been started" {
+            assert_equal 1 [is_alive $srv]
+        }
+
+        test "AOF+EXPIRE: List should be empty" {
+            set client [redis [dict get $srv host] [dict get $srv port] 0 $::tls]
+            wait_done_loading $client
+            assert_equal 0 [$client llen list]
+        }
+    }
+
+    start_server {overrides {appendonly {yes} appendfilename {appendonly.aof}}} {
+        test {Redis should not try to convert DEL into EXPIREAT for EXPIRE -1} {
+            r set x 10
+            r expire x -1
+        }
+    }
+
+    start_server {overrides {appendonly {yes} appendfilename {appendonly.aof} appendfsync always}} {
+        test {AOF fsync always barrier issue} {
+            set rd [redis_deferring_client]
+            # Set a sleep when aof is flushed, so that we have a chance to look
+            # at the aof size and detect if the response of an incr command
+            # arrives before the data was written (and hopefully fsynced)
+            # We create a big reply, which will hopefully not have room in the
+            # socket buffers, and will install a write handler, then we sleep
+            # a big and issue the incr command, hoping that the last portion of
+            # the output buffer write, and the processing of the incr will happen
+            # in the same event loop cycle.
+            # Since the socket buffers and timing are unpredictable, we fuzz this
+            # test with slightly different sizes and sleeps a few times.
+            for {set i 0} {$i < 10} {incr i} {
+                r debug aof-flush-sleep 0
+                r del x
+                r setrange x [expr {int(rand()*5000000)+10000000}] x
+                r debug aof-flush-sleep 500000
+                set aof [file join [lindex [r config get dir] 1] appendonly.aof]
+                set size1 [file size $aof]
+                $rd get x
+                after [expr {int(rand()*30)}]
+                $rd incr new_value
+                $rd read
+                $rd read
+                set size2 [file size $aof]
+                assert {$size1 != $size2}
+            }
+        }
+    }
+
+    start_server {overrides {appendonly {yes} appendfilename {appendonly.aof}}} {
+        test {GETEX should not append to AOF} {
+            set aof [file join [lindex [r config get dir] 1] appendonly.aof]
+            r set foo bar
+            set before [file size $aof]
+            r getex foo
+            set after [file size $aof]
+            assert_equal $before $after
+        }
+    }
+
+    ## Test that the server exits when the AOF contains a unknown command
+    create_aof {
+        append_to_aof [formatCommand set foo hello]
+        append_to_aof [formatCommand bla foo hello]
+        append_to_aof [formatCommand set foo hello]
+    }
+
+    start_server_aof [list dir $server_path aof-load-truncated yes] {
+        test "Unknown command: Server should have logged an error" {
+            set pattern "*Unknown command 'bla' reading the append only file*"
+            set retry 10
+            while {$retry} {
+                set result [exec tail -1 < [dict get $srv stdout]]
+                if {[string match $pattern $result]} {
+                    break
+                }
+                incr retry -1
+                after 1000
+            }
+            if {$retry == 0} {
+                error "assertion:expected error not found on config file"
+            }
+        }
+    }
+}

+ 51 - 0
tests/integration/block-repl.tcl

@@ -0,0 +1,51 @@
+# Test replication of blocking lists and zset operations.
+# Unlike stream operations such operations are "pop" style, so they consume
+# the list or sorted set, and must be replicated correctly.
+
+proc start_bg_block_op {host port db ops tls} {
+    set tclsh [info nameofexecutable]
+    exec $tclsh tests/helpers/bg_block_op.tcl $host $port $db $ops $tls &
+}
+
+proc stop_bg_block_op {handle} {
+    catch {exec /bin/kill -9 $handle}
+}
+
+start_server {tags {"repl" "external:skip"}} {
+    start_server {} {
+        set master [srv -1 client]
+        set master_host [srv -1 host]
+        set master_port [srv -1 port]
+        set slave [srv 0 client]
+
+        set load_handle0 [start_bg_block_op $master_host $master_port 9 100000 $::tls]
+        set load_handle1 [start_bg_block_op $master_host $master_port 9 100000 $::tls]
+        set load_handle2 [start_bg_block_op $master_host $master_port 9 100000 $::tls]
+
+        test {First server should have role slave after SLAVEOF} {
+            $slave slaveof $master_host $master_port
+            after 1000
+            s 0 role
+        } {slave}
+
+        test {Test replication with blocking lists and sorted sets operations} {
+            after 25000
+            stop_bg_block_op $load_handle0
+            stop_bg_block_op $load_handle1
+            stop_bg_block_op $load_handle2
+            wait_for_condition 100 100 {
+                [$master debug digest] == [$slave debug digest]
+            } else {
+                set csv1 [csvdump r]
+                set csv2 [csvdump {r -1}]
+                set fd [open /tmp/repldump1.txt w]
+                puts -nonewline $fd $csv1
+                close $fd
+                set fd [open /tmp/repldump2.txt w]
+                puts -nonewline $fd $csv2
+                close $fd
+                fail "Master - Replica inconsistency, Run diff -u against /tmp/repldump*.txt for more info"
+            }
+        }
+    }
+}

+ 28 - 0
tests/integration/convert-ziplist-hash-on-load.tcl

@@ -0,0 +1,28 @@
+tags {"external:skip"} {
+
+# Copy RDB with ziplist encoded hash to server path
+set server_path [tmpdir "server.convert-ziplist-hash-on-load"]
+
+exec cp -f tests/assets/hash-ziplist.rdb $server_path
+start_server [list overrides [list "dir" $server_path "dbfilename" "hash-ziplist.rdb"]] {
+    test "RDB load ziplist hash: converts to listpack when RDB loading" {
+        r select 0
+
+        assert_encoding listpack hash
+        assert_equal 2 [r hlen hash]
+        assert_match {v1 v2} [r hmget hash f1 f2]
+    }
+}
+
+exec cp -f tests/assets/hash-ziplist.rdb $server_path
+start_server [list overrides [list "dir" $server_path "dbfilename" "hash-ziplist.rdb" "hash-max-ziplist-entries" 1]] {
+    test "RDB load ziplist hash: converts to hash table when hash-max-ziplist-entries is exceeded" {
+        r select 0
+
+        assert_encoding hashtable hash
+        assert_equal 2 [r hlen hash]
+        assert_match {v1 v2} [r hmget hash f1 f2]
+    }
+}
+
+}

+ 39 - 0
tests/integration/convert-zipmap-hash-on-load.tcl

@@ -0,0 +1,39 @@
+tags {"external:skip"} {
+
+# Copy RDB with zipmap encoded hash to server path
+set server_path [tmpdir "server.convert-zipmap-hash-on-load"]
+
+exec cp -f tests/assets/hash-zipmap.rdb $server_path
+start_server [list overrides [list "dir" $server_path "dbfilename" "hash-zipmap.rdb"]] {
+  test "RDB load zipmap hash: converts to listpack" {
+    r select 0
+
+    assert_match "*listpack*" [r debug object hash]
+    assert_equal 2 [r hlen hash]
+    assert_match {v1 v2} [r hmget hash f1 f2]
+  }
+}
+
+exec cp -f tests/assets/hash-zipmap.rdb $server_path
+start_server [list overrides [list "dir" $server_path "dbfilename" "hash-zipmap.rdb" "hash-max-ziplist-entries" 1]] {
+  test "RDB load zipmap hash: converts to hash table when hash-max-ziplist-entries is exceeded" {
+    r select 0
+
+    assert_match "*hashtable*" [r debug object hash]
+    assert_equal 2 [r hlen hash]
+    assert_match {v1 v2} [r hmget hash f1 f2]
+  }
+}
+
+exec cp -f tests/assets/hash-zipmap.rdb $server_path
+start_server [list overrides [list "dir" $server_path "dbfilename" "hash-zipmap.rdb" "hash-max-ziplist-value" 1]] {
+  test "RDB load zipmap hash: converts to hash table when hash-max-ziplist-value is exceeded" {
+    r select 0
+
+    assert_match "*hashtable*" [r debug object hash]
+    assert_equal 2 [r hlen hash]
+    assert_match {v1 v2} [r hmget hash f1 f2]
+  }
+}
+
+}

+ 217 - 0
tests/integration/corrupt-dump-fuzzer.tcl

@@ -0,0 +1,217 @@
+# tests of corrupt ziplist payload with valid CRC
+
+tags {"dump" "corruption" "external:skip"} {
+
+# catch sigterm so that in case one of the random command hangs the test,
+# usually due to redis not putting a response in the output buffers,
+# we'll know which command it was
+if { ! [ catch {
+    package require Tclx
+} err ] } {
+    signal error SIGTERM
+}
+
+proc generate_collections {suffix elements} {
+    set rd [redis_deferring_client]
+    for {set j 0} {$j < $elements} {incr j} {
+        # add both string values and integers
+        if {$j % 2 == 0} {set val $j} else {set val "_$j"}
+        $rd hset hash$suffix $j $val
+        $rd lpush list$suffix $val
+        $rd zadd zset$suffix $j $val
+        $rd sadd set$suffix $val
+        $rd xadd stream$suffix * item 1 value $val
+    }
+    for {set j 0} {$j < $elements * 5} {incr j} {
+        $rd read ; # Discard replies
+    }
+    $rd close
+}
+
+# generate keys with various types and encodings
+proc generate_types {} {
+    r config set list-max-ziplist-size 5
+    r config set hash-max-ziplist-entries 5
+    r config set zset-max-ziplist-entries 5
+    r config set stream-node-max-entries 5
+
+    # create small (ziplist / listpack encoded) objects with 3 items
+    generate_collections "" 3
+
+    # add some metadata to the stream
+    r xgroup create stream mygroup 0
+    set records [r xreadgroup GROUP mygroup Alice COUNT 2 STREAMS stream >]
+    r xdel stream [lindex [lindex [lindex [lindex $records 0] 1] 1] 0]
+    r xack stream mygroup [lindex [lindex [lindex [lindex $records 0] 1] 0] 0]
+
+    # create other non-collection types
+    r incr int
+    r set string str
+
+    # create bigger objects with 10 items (more than a single ziplist / listpack)
+    generate_collections big 10
+
+    # make sure our big stream also has a listpack record that has different
+    # field names than the master recorded
+    r xadd streambig * item 1 value 1
+    r xadd streambig * item 1 unique value
+}
+
+proc corrupt_payload {payload} {
+    set len [string length $payload]
+    set count 1 ;# usually corrupt only one byte
+    if {rand() > 0.9} { set count 2 }
+    while { $count > 0 } {
+        set idx [expr {int(rand() * $len)}]
+        set ch [binary format c [expr {int(rand()*255)}]]
+        set payload [string replace $payload $idx $idx $ch]
+        incr count -1
+    }
+    return $payload
+}
+
+# fuzzy tester for corrupt RESTORE payloads
+# valgrind will make sure there were no leaks in the rdb loader error handling code
+foreach sanitize_dump {no yes} {
+    if {$::accurate} {
+        set min_duration [expr {60 * 10}] ;# run at least 10 minutes
+        set min_cycles 1000 ;# run at least 1k cycles (max 16 minutes)
+    } else {
+        set min_duration 10 ; # run at least 10 seconds
+        set min_cycles 10 ; # run at least 10 cycles
+    }
+
+    # Don't execute this on FreeBSD due to a yet-undiscovered memory issue
+    # which causes tclsh to bloat.
+    if {[exec uname] == "FreeBSD"} {
+        set min_cycles 1
+        set min_duration 1
+    }
+
+    test "Fuzzer corrupt restore payloads - sanitize_dump: $sanitize_dump" {
+        if {$min_duration * 2 > $::timeout} {
+            fail "insufficient timeout"
+        }
+        # start a server, fill with data and save an RDB file once (avoid re-save)
+        start_server [list overrides [list "save" "" use-exit-on-panic yes crash-memcheck-enabled no loglevel verbose] ] {
+            set stdout [srv 0 stdout]
+            r config set sanitize-dump-payload $sanitize_dump
+            r debug set-skip-checksum-validation 1
+            set start_time [clock seconds]
+            generate_types
+            set dbsize [r dbsize]
+            r save
+            set cycle 0
+            set stat_terminated_in_restore 0
+            set stat_terminated_in_traffic 0
+            set stat_terminated_by_signal 0
+            set stat_successful_restore 0
+            set stat_rejected_restore 0
+            set stat_traffic_commands_sent 0
+            # repeatedly DUMP a random key, corrupt it and try RESTORE into a new key
+            while true {
+                set k [r randomkey]
+                set dump [r dump $k]
+                set dump [corrupt_payload $dump]
+                set printable_dump [string2printable $dump]
+                set restore_failed false
+                set report_and_restart false
+                set sent {}
+                # RESTORE can fail, but hopefully not terminate
+                if { [catch { r restore "_$k" 0 $dump REPLACE } err] } {
+                    set restore_failed true
+                    # skip if return failed with an error response.
+                    if {[string match "ERR*" $err]} {
+                        incr stat_rejected_restore
+                    } else {
+                        set report_and_restart true
+                        incr stat_terminated_in_restore
+                        write_log_line 0 "corrupt payload: $printable_dump"
+                        if {$sanitize_dump == yes} {
+                            puts "Server crashed in RESTORE with payload: $printable_dump"
+                        }
+                    }
+                } else {
+                    r ping ;# an attempt to check if the server didn't terminate (this will throw an error that will terminate the tests)
+                }
+
+                set print_commands false
+                if {!$restore_failed} {
+                    # if RESTORE didn't fail or terminate, run some random traffic on the new key
+                    incr stat_successful_restore
+                    if { [ catch {
+                        set sent [generate_fuzzy_traffic_on_key "_$k" 1] ;# traffic for 1 second
+                        incr stat_traffic_commands_sent [llength $sent]
+                        r del "_$k" ;# in case the server terminated, here's where we'll detect it.
+                        if {$dbsize != [r dbsize]} {
+                            puts "unexpected keys"
+                            puts "keys: [r keys *]"
+                            puts $sent
+                            exit 1
+                        }
+                    } err ] } {
+                        # if the server terminated update stats and restart it
+                        set report_and_restart true
+                        incr stat_terminated_in_traffic
+                        set by_signal [count_log_message 0 "crashed by signal"]
+                        incr stat_terminated_by_signal $by_signal
+
+                        if {$by_signal != 0 || $sanitize_dump == yes} {
+                            puts "Server crashed (by signal: $by_signal), with payload: $printable_dump"
+                            set print_commands true
+                        }
+                    }
+                }
+
+                # check valgrind report for invalid reads after each RESTORE
+                # payload so that we have a report that is easier to reproduce
+                set valgrind_errors [find_valgrind_errors [srv 0 stderr] false]
+                if {$valgrind_errors != ""} {
+                    puts "valgrind found an issue for payload: $printable_dump"
+                    set report_and_restart true
+                    set print_commands true
+                }
+
+                if {$report_and_restart} {
+                    if {$print_commands} {
+                        puts "violating commands:"
+                        foreach cmd $sent {
+                            foreach arg $cmd {
+                                puts -nonewline "[string2printable $arg] "
+                            }
+                            puts ""
+                        }
+                    }
+
+                    # restart the server and re-apply debug configuration
+                    write_log_line 0 "corrupt payload: $printable_dump"
+                    restart_server 0 true true
+                    r config set sanitize-dump-payload $sanitize_dump
+                    r debug set-skip-checksum-validation 1
+                }
+
+                incr cycle
+                if { ([clock seconds]-$start_time) >= $min_duration && $cycle >= $min_cycles} {
+                    break
+                }
+            }
+            if {$::verbose} {
+                puts "Done $cycle cycles in [expr {[clock seconds]-$start_time}] seconds."
+                puts "RESTORE: successful: $stat_successful_restore, rejected: $stat_rejected_restore"
+                puts "Total commands sent in traffic: $stat_traffic_commands_sent, crashes during traffic: $stat_terminated_in_traffic ($stat_terminated_by_signal by signal)."
+            }
+        }
+        # if we run sanitization we never expect the server to crash at runtime
+        if {$sanitize_dump == yes} {
+            assert_equal $stat_terminated_in_restore 0
+            assert_equal $stat_terminated_in_traffic 0
+        }
+        # make sure all terminations where due to assertion and not a SIGSEGV
+        assert_equal $stat_terminated_by_signal 0
+    }
+}
+
+
+
+} ;# tags
+

+ 730 - 0
tests/integration/corrupt-dump.tcl

@@ -0,0 +1,730 @@
+# tests of corrupt ziplist payload with valid CRC
+# * setting crash-memcheck-enabled to no to avoid issues with valgrind
+# * setting use-exit-on-panic to yes so that valgrind can search for leaks
+# * setting debug set-skip-checksum-validation to 1 on some tests for which we
+#   didn't bother to fake a valid checksum
+# * some tests set sanitize-dump-payload to no and some to yet, depending on
+#   what we want to test
+
+tags {"dump" "corruption" "external:skip"} {
+
+# We only run OOM related tests on x86_64 and aarch64, as jemalloc on other
+# platforms (notably s390x) may actually succeed very large allocations. As
+# a result the test may hang for a very long time at the cleanup phase,
+# iterating as many as 2^61 hash table slots.
+
+set arch_name [exec uname -m]
+set run_oom_tests [expr {$arch_name == "x86_64" || $arch_name == "aarch64"}]
+
+set corrupt_payload_7445 "\x0E\x01\x1D\x1D\x00\x00\x00\x16\x00\x00\x00\x03\x00\x00\x04\x43\x43\x43\x43\x06\x04\x42\x42\x42\x42\x06\x3F\x41\x41\x41\x41\xFF\x09\x00\x88\xA5\xCA\xA8\xC5\x41\xF4\x35"
+
+test {corrupt payload: #7445 - with sanitize} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        catch {
+            r restore key 0 $corrupt_payload_7445
+        } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*integrity check failed*" 0
+    }
+}
+
+test {corrupt payload: #7445 - without sanitize - 1} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r restore key 0 $corrupt_payload_7445
+        catch {r lindex key 2}
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: #7445 - without sanitize - 2} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r restore key 0 $corrupt_payload_7445
+        catch {r lset key 2 "BEEF"}
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: hash with valid zip list header, invalid entry len} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        catch {
+            r restore key 0 "\x0D\x1B\x1B\x00\x00\x00\x16\x00\x00\x00\x04\x00\x00\x02\x61\x00\x04\x02\x62\x00\x04\x14\x63\x00\x04\x02\x64\x00\xFF\x09\x00\xD9\x10\x54\x92\x15\xF5\x5F\x52"
+        } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*integrity check failed*" 0
+    }
+}
+
+test {corrupt payload: invalid zlbytes header} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        catch {
+            r restore key 0 "\x0D\x1B\x25\x00\x00\x00\x16\x00\x00\x00\x04\x00\x00\x02\x61\x00\x04\x02\x62\x00\x04\x02\x63\x00\x04\x02\x64\x00\xFF\x09\x00\xB7\xF7\x6E\x9F\x43\x43\x14\xC6"
+        } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*integrity check failed*" 0
+    }
+}
+
+test {corrupt payload: valid zipped hash header, dup records} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        catch {
+            r restore key 0 "\x0D\x1B\x1B\x00\x00\x00\x16\x00\x00\x00\x04\x00\x00\x02\x61\x00\x04\x02\x62\x00\x04\x02\x61\x00\x04\x02\x64\x00\xFF\x09\x00\xA1\x98\x36\x78\xCC\x8E\x93\x2E"
+        } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*integrity check failed*" 0
+    }
+}
+
+test {corrupt payload: quicklist big ziplist prev len} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r restore key 0 "\x0E\x01\x13\x13\x00\x00\x00\x0E\x00\x00\x00\x02\x00\x00\x02\x61\x00\x0E\x02\x62\x00\xFF\x09\x00\x49\x97\x30\xB2\x0D\xA1\xED\xAA"
+        catch {r lindex key -2}
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: quicklist small ziplist prev len} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        catch {
+            r restore key 0 "\x0E\x01\x13\x13\x00\x00\x00\x0E\x00\x00\x00\x02\x00\x00\x02\x61\x00\x02\x02\x62\x00\xFF\x09\x00\xC7\x71\x03\x97\x07\x75\xB0\x63"
+        } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*integrity check failed*" 0
+    }
+}
+
+test {corrupt payload: quicklist ziplist wrong count} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r restore key 0 "\x0E\x01\x13\x13\x00\x00\x00\x0E\x00\x00\x00\x03\x00\x00\x02\x61\x00\x04\x02\x62\x00\xFF\x09\x00\x4D\xE2\x0A\x2F\x08\x25\xDF\x91"
+        # we'll be able to push, but iterating on the list will assert
+        r lpush key header
+        r rpush key footer
+        catch { [r lrange key -1 -1] }
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: #3080 - quicklist} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        catch {
+            r RESTORE key 0 "\x0E\x01\x80\x00\x00\x00\x10\x41\x41\x41\x41\x41\x41\x41\x41\x02\x00\x00\x80\x41\x41\x41\x41\x07\x00\x03\xC7\x1D\xEF\x54\x68\xCC\xF3"
+            r DUMP key ;# DUMP was used in the original issue, but now even with shallow sanitization restore safely fails, so this is dead code
+        } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*integrity check failed*" 0
+    }
+}
+
+test {corrupt payload: quicklist with empty ziplist} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        catch {r restore key 0 "\x0E\x01\x0B\x0B\x00\x00\x00\x0A\x00\x00\x00\x00\x00\xFF\x09\x00\xC2\x69\x37\x83\x3C\x7F\xFE\x6F" replace} err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+test {corrupt payload: #3080 - ziplist} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        # shallow sanitization is enough for restore to safely reject the payload with wrong size
+        r config set sanitize-dump-payload no
+        catch {
+            r RESTORE key 0 "\x0A\x80\x00\x00\x00\x10\x41\x41\x41\x41\x41\x41\x41\x41\x02\x00\x00\x80\x41\x41\x41\x41\x07\x00\x39\x5B\x49\xE0\xC1\xC6\xDD\x76"
+        } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*integrity check failed*" 0
+    }
+}
+
+test {corrupt payload: load corrupted rdb with no CRC - #3505} {
+    set server_path [tmpdir "server.rdb-corruption-test"]
+    exec cp tests/assets/corrupt_ziplist.rdb $server_path
+    set srv [start_server [list overrides [list "dir" $server_path "dbfilename" "corrupt_ziplist.rdb" loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no sanitize-dump-payload no]]]
+
+    # wait for termination
+    wait_for_condition 100 50 {
+        ! [is_alive $srv]
+    } else {
+        fail "rdb loading didn't fail"
+    }
+
+    set stdout [dict get $srv stdout]
+    assert_equal [count_message_lines $stdout "Terminating server after rdb file reading failure."]  1
+    assert_lessthan 1 [count_message_lines $stdout "integrity check failed"]
+    kill_server $srv ;# let valgrind look for issues
+}
+
+foreach sanitize_dump {no yes} {
+    test {corrupt payload: load corrupted rdb with empty keys} {
+        set server_path [tmpdir "server.rdb-corruption-empty-keys-test"]
+        exec cp tests/assets/corrupt_empty_keys.rdb $server_path
+        start_server [list overrides [list "dir" $server_path "dbfilename" "corrupt_empty_keys.rdb" "sanitize-dump-payload" $sanitize_dump]] {
+            r select 0
+            assert_equal [r dbsize] 0
+
+            verify_log_message 0 "*skipping empty key: set*" 0
+            verify_log_message 0 "*skipping empty key: list_quicklist*" 0
+            verify_log_message 0 "*skipping empty key: list_quicklist_empty_ziplist*" 0
+            verify_log_message 0 "*skipping empty key: list_ziplist*" 0
+            verify_log_message 0 "*skipping empty key: hash*" 0
+            verify_log_message 0 "*skipping empty key: hash_ziplist*" 0
+            verify_log_message 0 "*skipping empty key: zset*" 0
+            verify_log_message 0 "*skipping empty key: zset_ziplist*" 0
+            verify_log_message 0 "*empty keys skipped: 8*" 0
+        }
+    }
+}
+
+test {corrupt payload: listpack invalid size header} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        catch {
+            r restore key 0 "\x0F\x01\x10\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x02\x40\x55\x5F\x00\x00\x00\x0F\x00\x01\x01\x00\x01\x02\x01\x88\x31\x00\x00\x00\x00\x00\x00\x00\x09\x88\x32\x00\x00\x00\x00\x00\x00\x00\x09\x00\x01\x00\x01\x00\x01\x00\x01\x02\x02\x88\x31\x00\x00\x00\x00\x00\x00\x00\x09\x88\x61\x00\x00\x00\x00\x00\x00\x00\x09\x88\x32\x00\x00\x00\x00\x00\x00\x00\x09\x88\x62\x00\x00\x00\x00\x00\x00\x00\x09\x08\x01\xFF\x0A\x01\x00\x00\x09\x00\x45\x91\x0A\x87\x2F\xA5\xF9\x2E"
+        } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*Stream listpack integrity check failed*" 0
+    }
+}
+
+test {corrupt payload: listpack too long entry len} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r restore key 0 "\x0F\x01\x10\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x02\x40\x55\x55\x00\x00\x00\x0F\x00\x01\x01\x00\x01\x02\x01\x88\x31\x00\x00\x00\x00\x00\x00\x00\x09\x88\x32\x00\x00\x00\x00\x00\x00\x00\x09\x00\x01\x00\x01\x00\x01\x00\x01\x02\x02\x89\x31\x00\x00\x00\x00\x00\x00\x00\x09\x88\x61\x00\x00\x00\x00\x00\x00\x00\x09\x88\x32\x00\x00\x00\x00\x00\x00\x00\x09\x88\x62\x00\x00\x00\x00\x00\x00\x00\x09\x08\x01\xFF\x0A\x01\x00\x00\x09\x00\x40\x63\xC9\x37\x03\xA2\xE5\x68"
+        catch {
+            r xinfo stream key full
+        } err
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: listpack very long entry len} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r restore key 0 "\x0F\x01\x10\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x02\x40\x55\x55\x00\x00\x00\x0F\x00\x01\x01\x00\x01\x02\x01\x88\x31\x00\x00\x00\x00\x00\x00\x00\x09\x88\x32\x00\x00\x00\x00\x00\x00\x00\x09\x00\x01\x00\x01\x00\x01\x00\x01\x02\x02\x88\x31\x00\x00\x00\x00\x00\x00\x00\x09\x88\x61\x00\x00\x00\x00\x00\x00\x00\x09\x88\x32\x00\x00\x00\x00\x00\x00\x00\x09\x9C\x62\x00\x00\x00\x00\x00\x00\x00\x09\x08\x01\xFF\x0A\x01\x00\x00\x09\x00\x63\x6F\x42\x8E\x7C\xB5\xA2\x9D"
+        catch {
+            r xinfo stream key full
+        } err
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: listpack too long entry prev len} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        catch {
+            r restore key 0 "\x0F\x01\x10\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x02\x40\x55\x55\x00\x00\x00\x0F\x00\x01\x01\x00\x15\x02\x01\x88\x31\x00\x00\x00\x00\x00\x00\x00\x09\x88\x32\x00\x00\x00\x00\x00\x00\x00\x09\x00\x01\x00\x01\x00\x01\x00\x01\x02\x02\x88\x31\x00\x00\x00\x00\x00\x00\x00\x09\x88\x61\x00\x00\x00\x00\x00\x00\x00\x09\x88\x32\x00\x00\x00\x00\x00\x00\x00\x09\x88\x62\x00\x00\x00\x00\x00\x00\x00\x09\x08\x01\xFF\x0A\x01\x00\x00\x09\x00\x06\xFB\x44\x24\x0A\x8E\x75\xEA"
+        } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*Stream listpack integrity check failed*" 0
+    }
+}
+
+test {corrupt payload: hash ziplist with duplicate records} {
+    # when we do perform full sanitization, we expect duplicate records to fail the restore
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch { r RESTORE _hash 0 "\x0D\x3D\x3D\x00\x00\x00\x3A\x00\x00\x00\x14\x13\x00\xF5\x02\xF5\x02\xF2\x02\x53\x5F\x31\x04\xF3\x02\xF3\x02\xF7\x02\xF7\x02\xF8\x02\x02\x5F\x37\x04\xF1\x02\xF1\x02\xF6\x02\x02\x5F\x35\x04\xF4\x02\x02\x5F\x33\x04\xFA\x02\x02\x5F\x39\x04\xF9\x02\xF9\xFF\x09\x00\xB5\x48\xDE\x62\x31\xD0\xE5\x63" } err
+        assert_match "*Bad data format*" $err
+    }
+}
+
+test {corrupt payload: hash listpack with duplicate records} {
+    # when we do perform full sanitization, we expect duplicate records to fail the restore
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch { r RESTORE _hash 0 "\x10\x17\x17\x00\x00\x00\x04\x00\x82a\x00\x03\x82b\x00\x03\x82a\x00\x03\x82d\x00\x03\xff\n\x00\xc0\xcf\xa6\x87\xe5\xa7\xc5\xbe" } err
+        assert_match "*Bad data format*" $err
+    }
+}
+
+test {corrupt payload: hash ziplist uneven record count} {
+    # when we do perform full sanitization, we expect duplicate records to fail the restore
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch { r RESTORE _hash 0 "\r\x1b\x1b\x00\x00\x00\x16\x00\x00\x00\x04\x00\x00\x02a\x00\x04\x02b\x00\x04\x02a\x00\x04\x02d\x00\xff\t\x00\xa1\x98\x36x\xcc\x8e\x93\x2e" } err
+        assert_match "*Bad data format*" $err
+    }
+}
+
+test {corrupt payload: hash duplicate records} {
+    # when we do perform full sanitization, we expect duplicate records to fail the restore
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch { r RESTORE _hash 0 "\x04\x02\x01a\x01b\x01a\x01d\t\x00\xc6\x9c\xab\xbc\bk\x0c\x06" } err
+        assert_match "*Bad data format*" $err
+    }
+}
+
+test {corrupt payload: hash empty zipmap} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        catch { r RESTORE _hash 0 "\x09\x02\x00\xFF\x09\x00\xC0\xF1\xB8\x67\x4C\x16\xAC\xE3" } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*Zipmap integrity check failed*" 0
+    }
+}
+
+test {corrupt payload: fuzzer findings - NPD in streamIteratorGetID} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        catch {
+            r RESTORE key 0 "\x0F\x01\x10\x00\x00\x01\x73\xBD\x68\x48\x71\x00\x00\x00\x00\x00\x00\x00\x00\x40\x42\x42\x00\x00\x00\x18\x00\x03\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x00\x01\x02\x01\x00\x01\x00\x01\x01\x01\x00\x01\x05\x01\x02\x01\x00\x01\x01\x01\x01\x01\x82\x5F\x31\x03\x05\x01\x02\x01\x00\x01\x02\x01\x01\x01\x02\x01\x48\x01\xFF\x03\x81\x00\x00\x01\x73\xBD\x68\x48\x71\x02\x01\x07\x6D\x79\x67\x72\x6F\x75\x70\x81\x00\x00\x01\x73\xBD\x68\x48\x71\x00\x01\x00\x00\x01\x73\xBD\x68\x48\x71\x00\x00\x00\x00\x00\x00\x00\x00\x72\x48\x68\xBD\x73\x01\x00\x00\x01\x01\x05\x41\x6C\x69\x63\x65\x72\x48\x68\xBD\x73\x01\x00\x00\x01\x00\x00\x01\x73\xBD\x68\x48\x71\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x80\xCD\xB0\xD5\x1A\xCE\xFF\x10"
+            r XREVRANGE key 725 233
+        }
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: fuzzer findings - listpack NPD on invalid stream} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        catch {
+            r RESTORE _stream 0 "\x0F\x01\x10\x00\x00\x01\x73\xDC\xB6\x6B\xF1\x00\x00\x00\x00\x00\x00\x00\x00\x40\x42\x42\x00\x00\x00\x18\x00\x03\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x00\x01\x02\x01\x00\x01\x00\x01\x01\x01\x00\x01\x05\x01\x02\x01\x1F\x01\x00\x01\x01\x01\x6D\x5F\x31\x03\x05\x01\x02\x01\x29\x01\x00\x01\x01\x01\x02\x01\x05\x01\xFF\x03\x81\x00\x00\x01\x73\xDC\xB6\x6C\x1A\x00\x01\x07\x6D\x79\x67\x72\x6F\x75\x70\x81\x00\x00\x01\x73\xDC\xB6\x6B\xF1\x00\x01\x00\x00\x01\x73\xDC\xB6\x6B\xF1\x00\x00\x00\x00\x00\x00\x00\x00\x4B\x6C\xB6\xDC\x73\x01\x00\x00\x01\x01\x05\x41\x6C\x69\x63\x65\x3D\x6C\xB6\xDC\x73\x01\x00\x00\x01\x00\x00\x01\x73\xDC\xB6\x6B\xF1\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\xC7\x7D\x1C\xD7\x04\xFF\xE6\x9D"
+            r XREAD STREAMS _stream 519389898758
+        }
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: fuzzer findings - NPD in quicklistIndex} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        catch {
+            r RESTORE key 0 "\x0E\x01\x13\x13\x00\x00\x00\x10\x00\x00\x00\x03\x12\x00\xF3\x02\x02\x5F\x31\x04\xF1\xFF\x09\x00\xC9\x4B\x31\xFE\x61\xC0\x96\xFE"
+            r LSET key 290 290
+        }
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: fuzzer findings - encoded entry header reach outside the allocation} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r debug set-skip-checksum-validation 1
+        catch {
+            r RESTORE key 0 "\x0D\x19\x19\x00\x00\x00\x16\x00\x00\x00\x06\x00\x00\xF1\x02\xF1\x02\xF2\x02\x02\x5F\x31\x04\x99\x02\xF3\xFF\x09\x00\xC5\xB8\x10\xC0\x8A\xF9\x16\xDF"
+        } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*integrity check failed*" 0
+    }
+}
+
+
+test {corrupt payload: fuzzer findings - invalid ziplist encoding} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch {
+            r RESTORE _listbig 0 "\x0E\x02\x1B\x1B\x00\x00\x00\x16\x00\x00\x00\x05\x00\x00\x02\x5F\x39\x04\xF9\x02\x86\x5F\x37\x04\xF7\x02\x02\x5F\x35\xFF\x19\x19\x00\x00\x00\x16\x00\x00\x00\x05\x00\x00\xF5\x02\x02\x5F\x33\x04\xF3\x02\x02\x5F\x31\x04\xF1\xFF\x09\x00\x0C\xFC\x99\x2C\x23\x45\x15\x60"
+        } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*integrity check failed*" 0
+    }
+}
+
+test {corrupt payload: fuzzer findings - hash crash} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        r RESTORE _hash 0 "\x0D\x19\x19\x00\x00\x00\x16\x00\x00\x00\x06\x00\x00\xF1\x02\xF1\x02\xF2\x02\x02\x5F\x31\x04\xF3\x02\xF3\xFF\x09\x00\x38\xB8\x10\xC0\x8A\xF9\x16\xDF"
+        r HSET _hash 394891450 1635910264
+        r HMGET _hash 887312884855
+    }
+}
+
+test {corrupt payload: fuzzer findings - uneven entry count in hash} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r debug set-skip-checksum-validation 1
+        catch {
+            r RESTORE _hashbig 0 "\x0D\x3D\x3D\x00\x00\x00\x38\x00\x00\x00\x14\x00\x00\xF2\x02\x02\x5F\x31\x04\x1C\x02\xF7\x02\xF1\x02\xF1\x02\xF5\x02\xF5\x02\xF4\x02\x02\x5F\x33\x04\xF6\x02\x02\x5F\x35\x04\xF8\x02\x02\x5F\x37\x04\xF9\x02\xF9\x02\xF3\x02\xF3\x02\xFA\x02\x02\x5F\x39\xFF\x09\x00\x73\xB7\x68\xC8\x97\x24\x8E\x88"
+        } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*integrity check failed*" 0
+    }
+}
+
+test {corrupt payload: fuzzer findings - invalid read in lzf_decompress} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        catch { r RESTORE _setbig 0 "\x02\x03\x02\x5F\x31\xC0\x02\xC3\x00\x09\x00\xE6\xDC\x76\x44\xFF\xEB\x3D\xFE" } err
+        assert_match "*Bad data format*" $err
+    }
+}
+
+test {corrupt payload: fuzzer findings - leak in rdbloading due to dup entry in set} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        catch { r RESTORE _setbig 0 "\x02\x0A\x02\x5F\x39\xC0\x06\x02\x5F\x31\xC0\x00\xC0\x04\x02\x5F\x35\xC0\x02\xC0\x08\x02\x5F\x31\x02\x5F\x33\x09\x00\x7A\x5A\xFB\x90\x3A\xE9\x3C\xBE" } err
+        assert_match "*Bad data format*" $err
+    }
+}
+
+test {corrupt payload: fuzzer findings - empty intset} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        catch {r RESTORE _setbig 0 "\x02\xC0\xC0\x06\x02\x5F\x39\xC0\x02\x02\x5F\x33\xC0\x00\x02\x5F\x31\xC0\x04\xC0\x08\x02\x5F\x37\x02\x5F\x35\x09\x00\xC5\xD4\x6D\xBA\xAD\x14\xB7\xE7"} err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+test {corrupt payload: fuzzer findings - valgrind ziplist - crash report prints freed memory} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        r RESTORE _zsetbig 0 "\x0C\x3D\x3D\x00\x00\x00\x3A\x00\x00\x00\x14\x00\x00\xF1\x02\xF1\x02\x02\x5F\x31\x04\xF2\x02\xF3\x02\xF3\x02\x02\x5F\x33\x04\xF4\x02\xEE\x02\xF5\x02\x02\x5F\x35\x04\xF6\x02\xF7\x02\xF7\x02\x02\x5F\x37\x04\xF8\x02\xF9\x02\xF9\x02\x02\x5F\x39\x04\xFA\xFF\x09\x00\xAE\xF9\x77\x2A\x47\x24\x33\xF6"
+        catch { r ZREMRANGEBYSCORE _zsetbig -1050966020 724 }
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: fuzzer findings - valgrind ziplist prevlen reaches outside the ziplist} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        r RESTORE _listbig 0 "\x0E\x02\x1B\x1B\x00\x00\x00\x16\x00\x00\x00\x05\x00\x00\x02\x5F\x39\x04\xF9\x02\x02\x5F\x37\x04\xF7\x02\x02\x5F\x35\xFF\x19\x19\x00\x00\x00\x16\x00\x00\x00\x05\x00\x00\xF5\x02\x02\x5F\x33\x04\xF3\x95\x02\x5F\x31\x04\xF1\xFF\x09\x00\x0C\xFC\x99\x2C\x23\x45\x15\x60"
+        catch { r RPOP _listbig }
+        catch { r RPOP _listbig }
+        catch { r RPUSH _listbig 949682325 }
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: fuzzer findings - valgrind - bad rdbLoadDoubleValue} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        catch { r RESTORE _list 0 "\x03\x01\x11\x11\x00\x00\x00\x0A\x00\x00\x00\x01\x00\x00\xD0\x07\x1A\xE9\x02\xFF\x09\x00\x1A\x06\x07\x32\x41\x28\x3A\x46" } err
+        assert_match "*Bad data format*" $err
+    }
+}
+
+test {corrupt payload: fuzzer findings - valgrind ziplist prev too big} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        r RESTORE _list 0 "\x0E\x01\x13\x13\x00\x00\x00\x10\x00\x00\x00\x03\x00\x00\xF3\x02\x02\x5F\x31\xC1\xF1\xFF\x09\x00\xC9\x4B\x31\xFE\x61\xC0\x96\xFE"
+        catch { r RPUSHX _list -45 }
+        catch { r LREM _list -748 -840}
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: fuzzer findings - lzf decompression fails, avoid valgrind invalid read} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        catch {r RESTORE _stream 0 "\x0F\x02\x10\x00\x00\x01\x73\xDD\xAA\x2A\xB9\x00\x00\x00\x00\x00\x00\x00\x00\xC3\x40\x4B\x40\x5C\x18\x5C\x00\x00\x00\x24\x00\x05\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x40\x10\x00\x00\x20\x01\x00\x01\x20\x03\x00\x05\x20\x1C\x40\x07\x05\x01\x01\x82\x5F\x31\x03\x80\x0D\x40\x00\x00\x02\x60\x19\x40\x27\x40\x19\x00\x33\x60\x19\x40\x29\x02\x01\x01\x04\x20\x19\x00\xFF\x10\x00\x00\x01\x73\xDD\xAA\x2A\xBC\x00\x00\x00\x00\x00\x00\x00\x00\xC3\x40\x4D\x40\x5E\x18\x5E\x00\x00\x00\x24\x00\x05\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x40\x10\x00\x00\x20\x01\x06\x01\x01\x82\x5F\x35\x03\x05\x20\x1E\x17\x0B\x03\x01\x01\x06\x01\x40\x0B\x00\x01\x60\x0D\x02\x82\x5F\x37\x60\x19\x80\x00\x00\x08\x60\x19\x80\x27\x02\x82\x5F\x39\x20\x19\x00\xFF\x0A\x81\x00\x00\x01\x73\xDD\xAA\x2A\xBE\x00\x00\x09\x00\x21\x85\x77\x43\x71\x7B\x17\x88"} err
+        assert_match "*Bad data format*" $err
+    }
+}
+
+test {corrupt payload: fuzzer findings - stream bad lp_count} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch { r RESTORE _stream 0 "\x0F\x01\x10\x00\x00\x01\x73\xDE\xDF\x7D\x9B\x00\x00\x00\x00\x00\x00\x00\x00\x40\x42\x42\x00\x00\x00\x18\x00\x03\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x00\x01\x02\x01\x00\x01\x00\x01\x01\x01\x00\x01\x56\x01\x02\x01\x22\x01\x00\x01\x01\x01\x82\x5F\x31\x03\x05\x01\x02\x01\x2C\x01\x00\x01\x01\x01\x02\x01\x05\x01\xFF\x03\x81\x00\x00\x01\x73\xDE\xDF\x7D\xC7\x00\x01\x07\x6D\x79\x67\x72\x6F\x75\x70\x81\x00\x00\x01\x73\xDE\xDF\x7D\x9B\x00\x01\x00\x00\x01\x73\xDE\xDF\x7D\x9B\x00\x00\x00\x00\x00\x00\x00\x00\xF9\x7D\xDF\xDE\x73\x01\x00\x00\x01\x01\x05\x41\x6C\x69\x63\x65\xEB\x7D\xDF\xDE\x73\x01\x00\x00\x01\x00\x00\x01\x73\xDE\xDF\x7D\x9B\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\xB2\xA8\xA7\x5F\x1B\x61\x72\xD5"} err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+test {corrupt payload: fuzzer findings - stream bad lp_count - unsanitized} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        r RESTORE _stream 0 "\x0F\x01\x10\x00\x00\x01\x73\xDE\xDF\x7D\x9B\x00\x00\x00\x00\x00\x00\x00\x00\x40\x42\x42\x00\x00\x00\x18\x00\x03\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x00\x01\x02\x01\x00\x01\x00\x01\x01\x01\x00\x01\x56\x01\x02\x01\x22\x01\x00\x01\x01\x01\x82\x5F\x31\x03\x05\x01\x02\x01\x2C\x01\x00\x01\x01\x01\x02\x01\x05\x01\xFF\x03\x81\x00\x00\x01\x73\xDE\xDF\x7D\xC7\x00\x01\x07\x6D\x79\x67\x72\x6F\x75\x70\x81\x00\x00\x01\x73\xDE\xDF\x7D\x9B\x00\x01\x00\x00\x01\x73\xDE\xDF\x7D\x9B\x00\x00\x00\x00\x00\x00\x00\x00\xF9\x7D\xDF\xDE\x73\x01\x00\x00\x01\x01\x05\x41\x6C\x69\x63\x65\xEB\x7D\xDF\xDE\x73\x01\x00\x00\x01\x00\x00\x01\x73\xDE\xDF\x7D\x9B\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\xB2\xA8\xA7\x5F\x1B\x61\x72\xD5"
+        catch { r XREVRANGE _stream 638932639 738}
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: fuzzer findings - stream integrity check issue} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch { r RESTORE _stream 0 "\x0F\x02\x10\x00\x00\x01\x75\x2D\xA2\x90\x67\x00\x00\x00\x00\x00\x00\x00\x00\xC3\x40\x4F\x40\x5C\x18\x5C\x00\x00\x00\x24\x00\x05\x01\x00\x01\x4A\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x40\x10\x00\x00\x20\x01\x00\x01\x20\x03\x00\x05\x20\x1C\x40\x09\x05\x01\x01\x82\x5F\x31\x03\x80\x0D\x00\x02\x20\x0D\x00\x02\xA0\x19\x00\x03\x20\x0B\x02\x82\x5F\x33\xA0\x19\x00\x04\x20\x0D\x00\x04\x20\x19\x00\xFF\x10\x00\x00\x01\x75\x2D\xA2\x90\x67\x00\x00\x00\x00\x00\x00\x00\x05\xC3\x40\x56\x40\x60\x18\x60\x00\x00\x00\x24\x00\x05\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x40\x10\x00\x00\x20\x01\x06\x01\x01\x82\x5F\x35\x03\x05\x20\x1E\x40\x0B\x03\x01\x01\x06\x01\x80\x0B\x00\x02\x20\x0B\x02\x82\x5F\x37\x60\x19\x03\x01\x01\xDF\xFB\x20\x05\x00\x08\x60\x1A\x20\x0C\x00\xFC\x20\x05\x02\x82\x5F\x39\x20\x1B\x00\xFF\x0A\x81\x00\x00\x01\x75\x2D\xA2\x90\x68\x01\x00\x09\x00\x1D\x6F\xC0\x69\x8A\xDE\xF7\x92" } err
+        assert_match "*Bad data format*" $err
+    }
+}
+
+test {corrupt payload: fuzzer findings - infinite loop} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        r RESTORE _stream 0 "\x0F\x01\x10\x00\x00\x01\x75\x3A\xA6\xD0\x93\x00\x00\x00\x00\x00\x00\x00\x00\x40\x42\x42\x00\x00\x00\x18\x00\x03\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x00\x01\x02\x01\x00\x01\x00\x01\x01\x01\x00\x01\x05\x01\x02\x01\x00\x01\x01\x01\x01\x01\x82\x5F\x31\x03\xFD\x01\x02\x01\x00\x01\x02\x01\x01\x01\x02\x01\x05\x01\xFF\x03\x81\x00\x00\x01\x75\x3A\xA6\xD0\x93\x02\x01\x07\x6D\x79\x67\x72\x6F\x75\x70\x81\x00\x00\x01\x75\x3A\xA6\xD0\x93\x00\x01\x00\x00\x01\x75\x3A\xA6\xD0\x93\x00\x00\x00\x00\x00\x00\x00\x00\x94\xD0\xA6\x3A\x75\x01\x00\x00\x01\x01\x05\x41\x6C\x69\x63\x65\x94\xD0\xA6\x3A\x75\x01\x00\x00\x01\x00\x00\x01\x75\x3A\xA6\xD0\x93\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\xC4\x09\xAD\x69\x7E\xEE\xA6\x2F"
+        catch { r XREVRANGE _stream 288270516 971031845 }
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: fuzzer findings - hash ziplist too long entry len} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r debug set-skip-checksum-validation 1
+        catch {
+            r RESTORE _hash 0 "\x0D\x3D\x3D\x00\x00\x00\x3A\x00\x00\x00\x14\x13\x00\xF5\x02\xF5\x02\xF2\x02\x53\x5F\x31\x04\xF3\x02\xF3\x02\xF7\x02\xF7\x02\xF8\x02\x02\x5F\x37\x04\xF1\x02\xF1\x02\xF6\x02\x02\x5F\x35\x04\xF4\x02\x02\x5F\x33\x04\xFA\x02\x02\x5F\x39\x04\xF9\x02\xF9\xFF\x09\x00\xB5\x48\xDE\x62\x31\xD0\xE5\x63"
+        } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*integrity check failed*" 0
+    }
+}
+
+if {$run_oom_tests} {
+
+test {corrupt payload: OOM in rdbGenericLoadStringObject} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        catch { r RESTORE x 0 "\x0A\x81\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x13\x00\x00\x00\x0E\x00\x00\x00\x02\x00\x00\x02\x61\x00\x04\x02\x62\x00\xFF\x09\x00\x57\x04\xE5\xCD\xD4\x37\x6C\x57" } err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+test {corrupt payload: fuzzer findings - OOM in dictExpand} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        catch { r RESTORE x 0 "\x02\x81\x02\x5F\x31\xC0\x00\xC0\x02\x09\x00\xCD\x84\x2C\xB7\xE8\xA4\x49\x57" } err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+}
+
+test {corrupt payload: fuzzer findings - invalid tail offset after removal} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        r RESTORE _zset 0 "\x0C\x19\x19\x00\x00\x00\x02\x00\x00\x00\x06\x00\x00\xF1\x02\xF1\x02\x02\x5F\x31\x04\xF2\x02\xF3\x02\xF3\xFF\x09\x00\x4D\x72\x7B\x97\xCD\x9A\x70\xC1"
+        catch {r ZPOPMIN _zset}
+        catch {r ZPOPMAX _zset}
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: fuzzer findings - negative reply length} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        r RESTORE _stream 0 "\x0F\x01\x10\x00\x00\x01\x75\xCF\xA1\x16\xA7\x00\x00\x00\x00\x00\x00\x00\x00\x40\x42\x42\x00\x00\x00\x18\x00\x03\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x00\x01\x02\x01\x00\x01\x00\x01\x01\x01\x00\x01\x05\x01\x02\x01\x00\x01\x01\x01\x01\x01\x14\x5F\x31\x03\x05\x01\x02\x01\x00\x01\x02\x01\x01\x01\x02\x01\x05\x01\xFF\x03\x81\x00\x00\x01\x75\xCF\xA1\x16\xA7\x02\x01\x07\x6D\x79\x67\x72\x6F\x75\x70\x81\x00\x00\x01\x75\xCF\xA1\x16\xA7\x01\x01\x00\x00\x01\x75\xCF\xA1\x16\xA7\x00\x00\x00\x00\x00\x00\x00\x01\xA7\x16\xA1\xCF\x75\x01\x00\x00\x01\x01\x05\x41\x6C\x69\x63\x65\xA7\x16\xA1\xCF\x75\x01\x00\x00\x01\x00\x00\x01\x75\xCF\xA1\x16\xA7\x00\x00\x00\x00\x00\x00\x00\x01\x09\x00\x1B\x42\x52\xB8\xDD\x5C\xE5\x4E"
+        catch {r XADD _stream * -956 -2601503852}
+        catch {r XINFO STREAM _stream FULL}
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: fuzzer findings - valgrind negative malloc} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch {r RESTORE _key 0 "\x0E\x01\x81\xD6\xD6\x00\x00\x00\x0A\x00\x00\x00\x01\x00\x00\x40\xC8\x6F\x2F\x36\xE2\xDF\xE3\x2E\x26\x64\x8B\x87\xD1\x7A\xBD\xFF\xEF\xEF\x63\x65\xF6\xF8\x8C\x4E\xEC\x96\x89\x56\x88\xF8\x3D\x96\x5A\x32\xBD\xD1\x36\xD8\x02\xE6\x66\x37\xCB\x34\x34\xC4\x52\xA7\x2A\xD5\x6F\x2F\x7E\xEE\xA2\x94\xD9\xEB\xA9\x09\x38\x3B\xE1\xA9\x60\xB6\x4E\x09\x44\x1F\x70\x24\xAA\x47\xA8\x6E\x30\xE1\x13\x49\x4E\xA1\x92\xC4\x6C\xF0\x35\x83\xD9\x4F\xD9\x9C\x0A\x0D\x7A\xE7\xB1\x61\xF5\xC1\x2D\xDC\xC3\x0E\x87\xA6\x80\x15\x18\xBA\x7F\x72\xDD\x14\x75\x46\x44\x0B\xCA\x9C\x8F\x1C\x3C\xD7\xDA\x06\x62\x18\x7E\x15\x17\x24\xAB\x45\x21\x27\xC2\xBC\xBB\x86\x6E\xD8\xBD\x8E\x50\xE0\xE0\x88\xA4\x9B\x9D\x15\x2A\x98\xFF\x5E\x78\x6C\x81\xFC\xA8\xC9\xC8\xE6\x61\xC8\xD1\x4A\x7F\x81\xD6\xA6\x1A\xAD\x4C\xC1\xA2\x1C\x90\x68\x15\x2A\x8A\x36\xC0\x58\xC3\xCC\xA6\x54\x19\x12\x0F\xEB\x46\xFF\x6E\xE3\xA7\x92\xF8\xFF\x09\x00\xD0\x71\xF7\x9F\xF7\x6A\xD6\x2E"} err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+test {corrupt payload: fuzzer findings - valgrind invalid read} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch {r RESTORE _key 0 "\x05\x0A\x02\x5F\x39\x00\x00\x00\x00\x00\x00\x22\x40\xC0\x08\x00\x00\x00\x00\x00\x00\x20\x40\x02\x5F\x37\x00\x00\x00\x00\x00\x00\x1C\x40\xC0\x06\x00\x00\x00\x00\x00\x00\x18\x40\x02\x5F\x33\x00\x00\x00\x00\x00\x00\x14\x40\xC0\x04\x00\x00\x00\x00\x00\x00\x10\x40\x02\x5F\x33\x00\x00\x00\x00\x00\x00\x08\x40\xC0\x02\x00\x00\x00\x00\x00\x00\x00\x40\x02\x5F\x31\x00\x00\x00\x00\x00\x00\xF0\x3F\xC0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x3C\x66\xD7\x14\xA9\xDA\x3C\x69"} err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+test {corrupt payload: fuzzer findings - empty hash ziplist} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch {r RESTORE _int 0 "\x04\xC0\x01\x09\x00\xF6\x8A\xB6\x7A\x85\x87\x72\x4D"} err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+test {corrupt payload: fuzzer findings - stream with no records} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        r restore _stream 0 "\x0F\x01\x10\x00\x00\x01\x78\x4D\x55\x68\x09\x00\x00\x00\x00\x00\x00\x00\x00\x40\x42\x42\x00\x00\x00\x18\x00\x02\x01\x01\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x00\x01\x02\x01\x00\x01\x00\x01\x01\x01\x00\x01\x05\x01\x03\x01\x3E\x01\x00\x01\x01\x01\x82\x5F\x31\x03\x05\x01\x02\x01\x50\x01\x00\x01\x01\x01\x02\x01\x05\x23\xFF\x02\x81\x00\x00\x01\x78\x4D\x55\x68\x59\x00\x01\x07\x6D\x79\x67\x72\x6F\x75\x70\x81\x00\x00\x01\x78\x4D\x55\x68\x47\x00\x01\x00\x00\x01\x78\x4D\x55\x68\x47\x00\x00\x00\x00\x00\x00\x00\x00\x9F\x68\x55\x4D\x78\x01\x00\x00\x01\x01\x05\x41\x6C\x69\x63\x65\x85\x68\x55\x4D\x78\x01\x00\x00\x01\x00\x00\x01\x78\x4D\x55\x68\x47\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\xF1\xC0\x72\x70\x39\x40\x1E\xA9" replace
+        catch {r XREAD STREAMS _stream $}
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "Guru Meditation"] 1
+    }
+}
+
+test {corrupt payload: fuzzer findings - quicklist ziplist tail followed by extra data which start with 0xff} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch {
+            r restore key 0 "\x0E\x01\x11\x11\x00\x00\x00\x0A\x00\x00\x00\x01\x00\x00\xF6\xFF\xB0\x6C\x9C\xFF\x09\x00\x9C\x37\x47\x49\x4D\xDE\x94\xF5" replace
+        } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*integrity check failed*" 0
+    }
+}
+
+test {corrupt payload: fuzzer findings - dict init to huge size} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        catch {r restore key 0 "\x02\x81\xC0\x00\x02\x5F\x31\xC0\x02\x09\x00\xB2\x1B\xE5\x17\x2E\x15\xF4\x6C" replace} err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+test {corrupt payload: fuzzer findings - huge string} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch {r restore key 0 "\x00\x81\x01\x09\x00\xF6\x2B\xB6\x7A\x85\x87\x72\x4D"} err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+test {corrupt payload: fuzzer findings - stream PEL without consumer} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch {r restore _stream 0 "\x0F\x01\x10\x00\x00\x01\x7B\x08\xF0\xB2\x34\x00\x00\x00\x00\x00\x00\x00\x00\xC3\x3B\x40\x42\x19\x42\x00\x00\x00\x18\x00\x02\x01\x01\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x00\x20\x10\x00\x00\x20\x01\x00\x01\x20\x03\x02\x05\x01\x03\x20\x05\x40\x00\x04\x82\x5F\x31\x03\x05\x60\x19\x80\x32\x02\x05\x01\xFF\x02\x81\x00\x00\x01\x7B\x08\xF0\xB2\x34\x02\x01\x07\x6D\x79\x67\x72\x6F\x75\x70\x81\x00\x00\x01\x7B\x08\xF0\xB2\x34\x01\x01\x00\x00\x01\x7B\x08\xF0\xB2\x34\x00\x00\x00\x00\x00\x00\x00\x01\x35\xB2\xF0\x08\x7B\x01\x00\x00\x01\x01\x13\x41\x6C\x69\x63\x65\x35\xB2\xF0\x08\x7B\x01\x00\x00\x01\x00\x00\x01\x7B\x08\xF0\xB2\x34\x00\x00\x00\x00\x00\x00\x00\x01\x09\x00\x28\x2F\xE0\xC5\x04\xBB\xA7\x31"} err
+        assert_match "*Bad data format*" $err
+        #catch {r XINFO STREAM _stream FULL }
+        r ping
+    }
+}
+
+test {corrupt payload: fuzzer findings - stream listpack valgrind issue} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        r restore _stream 0 "\x0F\x01\x10\x00\x00\x01\x7B\x09\x5E\x94\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x40\x42\x42\x00\x00\x00\x18\x00\x02\x01\x01\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x00\x01\x02\x01\x00\x01\x00\x01\x01\x01\x00\x01\x05\x01\x03\x01\x25\x01\x00\x01\x01\x01\x82\x5F\x31\x03\x05\x01\x02\x01\x32\x01\x00\x01\x01\x01\x02\x01\xF0\x01\xFF\x02\x81\x00\x00\x01\x7B\x09\x5E\x95\x31\x00\x01\x07\x6D\x79\x67\x72\x6F\x75\x70\x81\x00\x00\x01\x7B\x09\x5E\x95\x24\x00\x01\x00\x00\x01\x7B\x09\x5E\x95\x24\x00\x00\x00\x00\x00\x00\x00\x00\x5C\x95\x5E\x09\x7B\x01\x00\x00\x01\x01\x05\x41\x6C\x69\x63\x65\x4B\x95\x5E\x09\x7B\x01\x00\x00\x01\x00\x00\x01\x7B\x09\x5E\x95\x24\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x19\x29\x94\xDF\x76\xF8\x1A\xC6"
+        catch {r XINFO STREAM _stream FULL }
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: fuzzer findings - stream with bad lpFirst} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch {r restore _stream 0 "\x0F\x01\x10\x00\x00\x01\x7B\x0E\x52\xD2\xEC\x00\x00\x00\x00\x00\x00\x00\x00\x40\x42\x42\x00\x00\x00\x18\x00\x02\xF7\x01\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x00\x01\x02\x01\x00\x01\x00\x01\x01\x01\x00\x01\x05\x01\x03\x01\x01\x01\x00\x01\x01\x01\x82\x5F\x31\x03\x05\x01\x02\x01\x01\x01\x01\x01\x01\x01\x02\x01\x05\x01\xFF\x02\x81\x00\x00\x01\x7B\x0E\x52\xD2\xED\x01\x01\x07\x6D\x79\x67\x72\x6F\x75\x70\x81\x00\x00\x01\x7B\x0E\x52\xD2\xED\x00\x01\x00\x00\x01\x7B\x0E\x52\xD2\xED\x00\x00\x00\x00\x00\x00\x00\x00\xED\xD2\x52\x0E\x7B\x01\x00\x00\x01\x01\x05\x41\x6C\x69\x63\x65\xED\xD2\x52\x0E\x7B\x01\x00\x00\x01\x00\x00\x01\x7B\x0E\x52\xD2\xED\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\xAC\x05\xC9\x97\x5D\x45\x80\xB3"} err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+test {corrupt payload: fuzzer findings - stream listpack lpPrev valgrind issue} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload no
+        r debug set-skip-checksum-validation 1
+        r restore _stream 0  "\x0F\x01\x10\x00\x00\x01\x7B\x0E\xAE\x66\x36\x00\x00\x00\x00\x00\x00\x00\x00\x40\x42\x42\x00\x00\x00\x18\x00\x02\x01\x01\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x00\x01\x02\x01\x00\x01\x00\x01\x01\x01\x00\x01\x1D\x01\x03\x01\x24\x01\x00\x01\x01\x69\x82\x5F\x31\x03\x05\x01\x02\x01\x33\x01\x00\x01\x01\x01\x02\x01\x05\x01\xFF\x02\x81\x00\x00\x01\x7B\x0E\xAE\x66\x69\x00\x01\x07\x6D\x79\x67\x72\x6F\x75\x70\x81\x00\x00\x01\x7B\x0E\xAE\x66\x5A\x00\x01\x00\x00\x01\x7B\x0E\xAE\x66\x5A\x00\x00\x00\x00\x00\x00\x00\x00\x94\x66\xAE\x0E\x7B\x01\x00\x00\x01\x01\x05\x41\x6C\x69\x63\x65\x83\x66\xAE\x0E\x7B\x01\x00\x00\x01\x00\x00\x01\x7B\x0E\xAE\x66\x5A\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\xD5\xD7\xA5\x5C\x63\x1C\x09\x40"
+        catch {r XREVRANGE _stream 1618622681 606195012389}
+        assert_equal [count_log_message 0 "crashed by signal"] 0
+        assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
+    }
+}
+
+test {corrupt payload: fuzzer findings - stream with non-integer entry id} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch {r restore _streambig 0 "\x0F\x03\x10\x00\x00\x01\x7B\x13\x34\xC3\xB2\x00\x00\x00\x00\x00\x00\x00\x00\xC3\x40\x4F\x40\x5C\x18\x5C\x00\x00\x00\x24\x00\x05\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x40\x10\x00\x80\x20\x01\x00\x01\x20\x03\x00\x05\x20\x1C\x40\x09\x05\x01\x01\x82\x5F\x31\x03\x80\x0D\x00\x02\x20\x0D\x00\x02\xA0\x19\x00\x03\x20\x0B\x02\x82\x5F\x33\xA0\x19\x00\x04\x20\x0D\x00\x04\x20\x19\x00\xFF\x10\x00\x00\x01\x7B\x13\x34\xC3\xB2\x00\x00\x00\x00\x00\x00\x00\x05\xC3\x40\x56\x40\x61\x18\x61\x00\x00\x00\x24\x00\x05\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x40\x10\x00\x00\x20\x01\x06\x01\x01\x82\x5F\x35\x03\x05\x20\x1E\x40\x0B\x03\x01\x01\x06\x01\x40\x0B\x03\x01\x01\xDF\xFB\x20\x05\x02\x82\x5F\x37\x60\x1A\x20\x0E\x00\xFC\x20\x05\x00\x08\xC0\x1B\x00\xFD\x20\x0C\x02\x82\x5F\x39\x20\x1B\x00\xFF\x10\x00\x00\x01\x7B\x13\x34\xC3\xB3\x00\x00\x00\x00\x00\x00\x00\x03\xC3\x3D\x40\x4A\x18\x4A\x00\x00\x00\x15\x00\x02\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x40\x10\x00\x00\x20\x01\x40\x00\x00\x05\x60\x07\x02\xDF\xFD\x02\xC0\x23\x09\x01\x01\x86\x75\x6E\x69\x71\x75\x65\x07\xA0\x2D\x02\x08\x01\xFF\x0C\x81\x00\x00\x01\x7B\x13\x34\xC3\xB4\x00\x00\x09\x00\x9D\xBD\xD5\xB9\x33\xC4\xC5\xFF"} err
+        #catch {r XINFO STREAM _streambig FULL }
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+test {corrupt payload: fuzzer findings - empty quicklist} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch {
+            r restore key 0 "\x0E\xC0\x2B\x15\x00\x00\x00\x0A\x00\x00\x00\x01\x00\x00\xE0\x62\x58\xEA\xDF\x22\x00\x00\x00\xFF\x09\x00\xDF\x35\xD2\x67\xDC\x0E\x89\xAB" replace
+        } err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+test {corrupt payload: fuzzer findings - empty zset} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch {r restore key 0 "\x05\xC0\x01\x09\x00\xF6\x8A\xB6\x7A\x85\x87\x72\x4D"} err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+test {corrupt payload: fuzzer findings - hash with len of 0} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r config set sanitize-dump-payload yes
+        r debug set-skip-checksum-validation 1
+        catch {r restore key 0 "\x04\xC0\x21\x09\x00\xF6\x8A\xB6\x7A\x85\x87\x72\x4D"} err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+test {corrupt payload: fuzzer findings - hash listpack first element too long entry len} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r debug set-skip-checksum-validation 1
+        r config set sanitize-dump-payload yes
+        catch { r restore _hash 0 "\x10\x15\x15\x00\x00\x00\x06\x00\xF0\x01\x00\x01\x01\x01\x82\x5F\x31\x03\x02\x01\x02\x01\xFF\x0A\x00\x94\x21\x0A\xFA\x06\x52\x9F\x44" replace } err
+        assert_match "*Bad data format*" $err
+        verify_log_message 0 "*integrity check failed*" 0
+    }
+}
+
+test {corrupt payload: fuzzer findings - stream double free listpack when insert dup node to rax returns 0} {
+    start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
+        r debug set-skip-checksum-validation 1
+        r config set sanitize-dump-payload yes
+        catch { r restore _stream 0 "\x0F\x03\x10\x00\x00\x01\x7B\x60\x5A\x23\x79\x00\x00\x00\x00\x00\x00\x00\x00\xC3\x40\x4F\x40\x5C\x18\x5C\x00\x00\x00\x24\x00\x05\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x40\x10\x00\x00\x20\x01\x00\x01\x20\x03\x00\x05\x20\x1C\x40\x09\x05\x01\x01\x82\x5F\x31\x03\x80\x0D\x00\x02\x20\x0D\x00\x02\xA0\x19\x00\x03\x20\x0B\x02\x82\x5F\x33\xA0\x19\x00\x04\x20\x0D\x00\x04\x20\x19\x00\xFF\x10\x00\x00\x01\x7B\x60\x5A\x23\x79\x00\x00\x00\x00\x00\x00\x00\x05\xC3\x40\x51\x40\x5E\x18\x5E\x00\x00\x00\x24\x00\x05\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x40\x10\x00\x00\x20\x01\x06\x01\x01\x82\x5F\x35\x03\x05\x20\x1E\x40\x0B\x03\x01\x01\x06\x01\x80\x0B\x00\x02\x20\x0B\x02\x82\x5F\x37\xA0\x19\x00\x03\x20\x0D\x00\x08\xA0\x19\x00\x04\x20\x0B\x02\x82\x5F\x39\x20\x19\x00\xFF\x10\x00\x00\x01\x7B\x60\x5A\x23\x79\x00\x00\x00\x00\x00\x00\x00\x00\xC3\x3B\x40\x49\x18\x49\x00\x00\x00\x15\x00\x02\x01\x00\x01\x02\x01\x84\x69\x74\x65\x6D\x05\x85\x76\x61\x6C\x75\x65\x06\x40\x10\x00\x00\x20\x01\x40\x00\x00\x05\x20\x07\x40\x09\xC0\x22\x09\x01\x01\x86\x75\x6E\x69\x71\x75\x65\x07\xA0\x2C\x02\x08\x01\xFF\x0C\x81\x00\x00\x01\x7B\x60\x5A\x23\x7A\x01\x00\x0A\x00\x9C\x8F\x1E\xBF\x2E\x05\x59\x09" replace } err
+        assert_match "*Bad data format*" $err
+        r ping
+    }
+}
+
+} ;# tags
+

+ 101 - 0
tests/integration/dismiss-mem.tcl

@@ -0,0 +1,101 @@
+# The tests of this file aim to get coverage on all the "dismiss" methods
+# that dismiss all data-types memory in the fork child. like client query
+# buffer, client output buffer and replication backlog.
+# Actually, we may not have many asserts in the test, since we just check for
+# crashes and the dump file inconsistencies.
+
+start_server {tags {"dismiss external:skip"}} {
+    # In other tests, although we test child process dumping RDB file, but
+    # memory allocations of key/values are usually small, they couldn't cover
+    # the "dismiss" object methods, in this test, we create big size key/values
+    # to satisfy the conditions for release memory pages, especially, we assume
+    # the page size of OS is 4KB in some cases.
+    test {dismiss all data types memory} {
+        set bigstr [string repeat A 8192]
+        set 64bytes [string repeat A 64]
+
+        # string
+        populate 100 bigstring 8192
+
+        # list
+        r lpush biglist1 $bigstr            ; # uncompressed ziplist node
+        r config set list-compress-depth 1  ; # compressed ziplist nodes
+        for {set i 0} {$i < 16} {incr i} {
+            r lpush biglist2 $bigstr
+        }
+
+        # set
+        r sadd bigset1 $bigstr              ; # hash encoding
+        set biginteger [string repeat 1 19]
+        for {set i 0} {$i < 512} {incr i} {
+            r sadd bigset2 $biginteger      ; # intset encoding
+        }
+
+        # zset
+        r zadd bigzset1 1.0 $bigstr         ; # skiplist encoding
+        for {set i 0} {$i < 128} {incr i} {
+            r zadd bigzset2 1.0 $64bytes    ; # ziplist encoding
+        }
+
+        # hash
+        r hset bighash1 field1 $bigstr      ; # hash encoding
+        for {set i 0} {$i < 128} {incr i} {
+            r hset bighash2 $i $64bytes     ; # ziplist encoding
+        }
+
+        # stream
+        r xadd bigstream * entry1 $bigstr entry2 $bigstr
+
+        set digest [r debug digest]
+        r config set aof-use-rdb-preamble no
+        r bgrewriteaof
+        waitForBgrewriteaof r
+        r debug loadaof
+        set newdigest [r debug digest]
+        assert {$digest eq $newdigest}
+    }
+
+    test {dismiss client output buffer} {
+        # Big output buffer
+        set item [string repeat "x" 100000]
+        for {set i 0} {$i < 100} {incr i} {
+            r lpush mylist $item
+        }
+        set rd [redis_deferring_client]
+        $rd lrange mylist 0 -1
+        $rd flush
+        after 100
+
+        r bgsave
+        waitForBgsave r
+        assert_equal $item [r lpop mylist]
+    }
+
+    test {dismiss client query buffer} {
+        # Big pending query buffer
+        set bigstr [string repeat A 8192]
+        set rd [redis_deferring_client]
+        $rd write "*2\r\n\$8192\r\n"
+        $rd write $bigstr\r\n
+        $rd flush
+        after 100
+
+        r bgsave
+        waitForBgsave r
+    }
+
+    test {dismiss replication backlog} {
+        set master [srv 0 client]
+        start_server {} {
+            r slaveof [srv -1 host] [srv -1 port]
+            wait_for_sync r
+
+            set bigstr [string repeat A 8192]
+            for {set i 0} {$i < 20} {incr i} {
+                $master set $i $bigstr
+            }
+            $master bgsave
+            waitForBgsave $master
+        }
+    }
+}

+ 294 - 0
tests/integration/failover.tcl

@@ -0,0 +1,294 @@
+start_server {tags {"failover external:skip"}} {
+start_server {} {
+start_server {} {
+    set node_0 [srv 0 client]
+    set node_0_host [srv 0 host]
+    set node_0_port [srv 0 port]
+    set node_0_pid [srv 0 pid]
+
+    set node_1 [srv -1 client]
+    set node_1_host [srv -1 host]
+    set node_1_port [srv -1 port]
+    set node_1_pid [srv -1 pid]
+
+    set node_2 [srv -2 client]
+    set node_2_host [srv -2 host]
+    set node_2_port [srv -2 port]
+    set node_2_pid [srv -2 pid]
+
+    proc assert_digests_match {n1 n2 n3} {
+        assert_equal [$n1 debug digest] [$n2 debug digest]
+        assert_equal [$n2 debug digest] [$n3 debug digest]
+    }
+
+    test {failover command fails without connected replica} {
+        catch { $node_0 failover to $node_1_host $node_1_port } err
+        if {! [string match "ERR*" $err]} {
+            fail "failover command succeeded when replica not connected"
+        }
+    }
+
+    test {setup replication for following tests} {
+        $node_1 replicaof $node_0_host $node_0_port
+        $node_2 replicaof $node_0_host $node_0_port
+        wait_for_sync $node_1
+        wait_for_sync $node_2
+    }
+
+    test {failover command fails with invalid host} {
+        catch { $node_0 failover to invalidhost $node_1_port } err
+        assert_match "ERR*" $err
+    }
+
+    test {failover command fails with invalid port} {
+        catch { $node_0 failover to $node_1_host invalidport } err
+        assert_match "ERR*" $err
+    }
+
+    test {failover command fails with just force and timeout} {
+        catch { $node_0 FAILOVER FORCE TIMEOUT 100} err
+        assert_match "ERR*" $err
+    }
+
+    test {failover command fails when sent to a replica} {
+        catch { $node_1 failover to $node_1_host $node_1_port } err
+        assert_match "ERR*" $err
+    }
+
+    test {failover command fails with force without timeout} {
+        catch { $node_0 failover to $node_1_host $node_1_port FORCE } err
+        assert_match "ERR*" $err
+    }
+
+    test {failover command to specific replica works} {
+        set initial_psyncs [s -1 sync_partial_ok]
+        set initial_syncs [s -1 sync_full]
+
+        # Generate a delta between primary and replica
+        set load_handler [start_write_load $node_0_host $node_0_port 5]
+        exec kill -SIGSTOP [srv -1 pid]
+        wait_for_condition 50 100 {
+            [s 0 total_commands_processed] > 100
+        } else {
+            fail "Node 0 did not accept writes"
+        }
+        exec kill -SIGCONT [srv -1 pid]
+
+        # Execute the failover
+        $node_0 failover to $node_1_host $node_1_port
+
+        # Wait for failover to end
+        wait_for_condition 50 100 {
+            [s 0 master_failover_state] == "no-failover"
+        } else {
+            fail "Failover from node 0 to node 1 did not finish"
+        }
+
+        # stop the write load and make sure no more commands processed
+        stop_write_load $load_handler
+        wait_load_handlers_disconnected
+
+        $node_2 replicaof $node_1_host $node_1_port
+        wait_for_sync $node_0
+        wait_for_sync $node_2
+
+        assert_match *slave* [$node_0 role]
+        assert_match *master* [$node_1 role]
+        assert_match *slave* [$node_2 role]
+
+        # We should accept psyncs from both nodes
+        assert_equal [expr [s -1 sync_partial_ok] - $initial_psyncs] 2
+        assert_equal [expr [s -1 sync_full] - $initial_psyncs] 0
+        assert_digests_match $node_0 $node_1 $node_2
+    }
+
+    test {failover command to any replica works} {
+        set initial_psyncs [s -2 sync_partial_ok]
+        set initial_syncs [s -2 sync_full]
+
+        wait_for_ofs_sync $node_1 $node_2
+        # We stop node 0 to and make sure node 2 is selected
+        exec kill -SIGSTOP $node_0_pid
+        $node_1 set CASE 1
+        $node_1 FAILOVER
+
+        # Wait for failover to end
+        wait_for_condition 50 100 {
+            [s -1 master_failover_state] == "no-failover"
+        } else {
+            fail "Failover from node 1 to node 2 did not finish"
+        }
+        exec kill -SIGCONT $node_0_pid
+        $node_0 replicaof $node_2_host $node_2_port
+
+        wait_for_sync $node_0
+        wait_for_sync $node_1
+
+        assert_match *slave* [$node_0 role]
+        assert_match *slave* [$node_1 role]
+        assert_match *master* [$node_2 role]
+
+        # We should accept Psyncs from both nodes
+        assert_equal [expr [s -2 sync_partial_ok] - $initial_psyncs] 2
+        assert_equal [expr [s -1 sync_full] - $initial_psyncs] 0
+        assert_digests_match $node_0 $node_1 $node_2
+    }
+
+    test {failover to a replica with force works} {
+        set initial_psyncs [s 0 sync_partial_ok]
+        set initial_syncs [s 0 sync_full]
+
+        exec kill -SIGSTOP $node_0_pid
+        # node 0 will never acknowledge this write
+        $node_2 set case 2
+        $node_2 failover to $node_0_host $node_0_port TIMEOUT 100 FORCE
+
+        # Wait for node 0 to give up on sync attempt and start failover
+        wait_for_condition 50 100 {
+            [s -2 master_failover_state] == "failover-in-progress"
+        } else {
+            fail "Failover from node 2 to node 0 did not timeout"
+        }
+
+        # Quick check that everyone is a replica, we never want a 
+        # state where there are two masters.
+        assert_match *slave* [$node_1 role]
+        assert_match *slave* [$node_2 role]
+
+        exec kill -SIGCONT $node_0_pid
+
+        # Wait for failover to end
+        wait_for_condition 50 100 {
+            [s -2 master_failover_state] == "no-failover"
+        } else {
+            fail "Failover from node 2 to node 0 did not finish"
+        }
+        $node_1 replicaof $node_0_host $node_0_port
+
+        wait_for_sync $node_1
+        wait_for_sync $node_2
+
+        assert_match *master* [$node_0 role]
+        assert_match *slave* [$node_1 role]
+        assert_match *slave* [$node_2 role]
+
+        assert_equal [count_log_message -2 "time out exceeded, failing over."] 1
+
+        # We should accept both psyncs, although this is the condition we might not
+        # since we didn't catch up.
+        assert_equal [expr [s 0 sync_partial_ok] - $initial_psyncs] 2
+        assert_equal [expr [s 0 sync_full] - $initial_syncs] 0
+        assert_digests_match $node_0 $node_1 $node_2
+    }
+
+    test {failover with timeout aborts if replica never catches up} {
+        set initial_psyncs [s 0 sync_partial_ok]
+        set initial_syncs [s 0 sync_full]
+
+        # Stop replica so it never catches up
+        exec kill -SIGSTOP [srv -1 pid]
+        $node_0 SET CASE 1
+        
+        $node_0 failover to [srv -1 host] [srv -1 port] TIMEOUT 500
+        # Wait for failover to end
+        wait_for_condition 50 20 {
+            [s 0 master_failover_state] == "no-failover"
+        } else {
+            fail "Failover from node_0 to replica did not finish"
+        }
+
+        exec kill -SIGCONT [srv -1 pid]
+
+        # We need to make sure the nodes actually sync back up
+        wait_for_ofs_sync $node_0 $node_1
+        wait_for_ofs_sync $node_0 $node_2
+
+        assert_match *master* [$node_0 role]
+        assert_match *slave* [$node_1 role]
+        assert_match *slave* [$node_2 role]
+
+        # Since we never caught up, there should be no syncs
+        assert_equal [expr [s 0 sync_partial_ok] - $initial_psyncs] 0
+        assert_equal [expr [s 0 sync_full] - $initial_syncs] 0
+        assert_digests_match $node_0 $node_1 $node_2
+    }
+
+    test {failovers can be aborted} {
+        set initial_psyncs [s 0 sync_partial_ok]
+        set initial_syncs [s 0 sync_full]
+    
+        # Stop replica so it never catches up
+        exec kill -SIGSTOP [srv -1 pid]
+        $node_0 SET CASE 2
+        
+        $node_0 failover to [srv -1 host] [srv -1 port] TIMEOUT 60000
+        assert_match [s 0 master_failover_state] "waiting-for-sync"
+
+        # Sanity check that read commands are still accepted
+        $node_0 GET CASE
+
+        $node_0 failover abort
+        assert_match [s 0 master_failover_state] "no-failover"
+
+        exec kill -SIGCONT [srv -1 pid]
+
+        # Just make sure everything is still synced
+        wait_for_ofs_sync $node_0 $node_1
+        wait_for_ofs_sync $node_0 $node_2
+
+        assert_match *master* [$node_0 role]
+        assert_match *slave* [$node_1 role]
+        assert_match *slave* [$node_2 role]
+
+        # Since we never caught up, there should be no syncs
+        assert_equal [expr [s 0 sync_partial_ok] - $initial_psyncs] 0
+        assert_equal [expr [s 0 sync_full] - $initial_syncs] 0
+        assert_digests_match $node_0 $node_1 $node_2
+    }
+
+    test {failover aborts if target rejects sync request} {
+        set initial_psyncs [s 0 sync_partial_ok]
+        set initial_syncs [s 0 sync_full]
+
+        # We block psync, so the failover will fail
+        $node_1 acl setuser default -psync
+
+        # We pause the target long enough to send a write command
+        # during the pause. This write will not be interrupted.
+        exec kill -SIGSTOP [srv -1 pid]
+        set rd [redis_deferring_client]
+        $rd SET FOO BAR
+        $node_0 failover to $node_1_host $node_1_port
+        exec kill -SIGCONT [srv -1 pid]
+
+        # Wait for failover to end
+        wait_for_condition 50 100 {
+            [s 0 master_failover_state] == "no-failover"
+        } else {
+            fail "Failover from node_0 to replica did not finish"
+        }
+
+        assert_equal [$rd read] "OK"
+        $rd close
+
+        # restore access to psync
+        $node_1 acl setuser default +psync
+
+        # We need to make sure the nodes actually sync back up
+        wait_for_sync $node_1
+        wait_for_sync $node_2
+
+        assert_match *master* [$node_0 role]
+        assert_match *slave* [$node_1 role]
+        assert_match *slave* [$node_2 role]
+
+        # We will cycle all of our replicas here and force a psync.
+        assert_equal [expr [s 0 sync_partial_ok] - $initial_psyncs] 2
+        assert_equal [expr [s 0 sync_full] - $initial_syncs] 0
+
+        assert_equal [count_log_message 0 "Failover target rejected psync request"] 1
+        assert_digests_match $node_0 $node_1 $node_2
+    }
+}
+}
+}

+ 55 - 0
tests/integration/logging.tcl

@@ -0,0 +1,55 @@
+tags {"external:skip"} {
+
+set system_name [string tolower [exec uname -s]]
+set system_supported 0
+
+# We only support darwin or Linux with glibc
+if {$system_name eq {darwin}} {
+    set system_supported 1
+} elseif {$system_name eq {linux}} {
+    # Avoid the test on libmusl, which does not support backtrace
+    set ldd [exec ldd src/redis-server]
+    if {![string match {*libc.musl*} $ldd]} {
+        set system_supported 1
+    }
+}
+
+if {$system_supported} {
+    set server_path [tmpdir server.log]
+    start_server [list overrides [list dir $server_path]] {
+        test "Server is able to generate a stack trace on selected systems" {
+            r config set watchdog-period 200
+            r debug sleep 1
+            set pattern "*debugCommand*"
+            set retry 10
+            while {$retry} {
+                set result [exec tail -100 < [srv 0 stdout]]
+                if {[string match $pattern $result]} {
+                    break
+                }
+                incr retry -1
+                after 1000
+            }
+            if {$retry == 0} {
+                error "assertion:expected stack trace not found into log file"
+            }
+        }
+    }
+
+    # Valgrind will complain that the process terminated by a signal, skip it.
+    if {!$::valgrind} {
+        set server_path [tmpdir server1.log]
+        start_server [list overrides [list dir $server_path]] {
+            test "Crash report generated on SIGABRT" {
+                set pid [s process_id]
+                exec kill -SIGABRT $pid
+                set pattern "*STACK TRACE*"
+                set result [exec tail -1000 < [srv 0 stdout]]
+                assert {[string match $pattern $result]}
+            }
+        }
+    }
+
+}
+
+}

+ 244 - 0
tests/integration/psync2-pingoff.tcl

@@ -0,0 +1,244 @@
+# These tests were added together with the meaningful offset implementation
+# in redis 6.0.0, which was later abandoned in 6.0.4, they used to test that
+# servers are able to PSYNC with replicas even if the replication stream has
+# PINGs at the end which present in one sever and missing on another.
+# We keep these tests just because they reproduce edge cases in the replication
+# logic in hope they'll be able to spot some problem in the future.
+
+start_server {tags {"psync2 external:skip"}} {
+start_server {} {
+    # Config
+    set debug_msg 0                 ; # Enable additional debug messages
+
+    for {set j 0} {$j < 2} {incr j} {
+        set R($j) [srv [expr 0-$j] client]
+        set R_host($j) [srv [expr 0-$j] host]
+        set R_port($j) [srv [expr 0-$j] port]
+        $R($j) CONFIG SET repl-ping-replica-period 1
+        if {$debug_msg} {puts "Log file: [srv [expr 0-$j] stdout]"}
+    }
+
+    # Setup replication
+    test "PSYNC2 pingoff: setup" {
+        $R(1) replicaof $R_host(0) $R_port(0)
+        $R(0) set foo bar
+        wait_for_condition 50 1000 {
+            [status $R(1) master_link_status] == "up" &&
+            [$R(0) dbsize] == 1 && [$R(1) dbsize] == 1
+        } else {
+            fail "Replicas not replicating from master"
+        }
+    }
+
+    test "PSYNC2 pingoff: write and wait replication" {
+        $R(0) INCR counter
+        $R(0) INCR counter
+        $R(0) INCR counter
+        wait_for_condition 50 1000 {
+            [$R(0) GET counter] eq [$R(1) GET counter]
+        } else {
+            fail "Master and replica don't agree about counter"
+        }
+    }
+
+    # In this test we'll make sure the replica will get stuck, but with
+    # an active connection: this way the master will continue to send PINGs
+    # every second (we modified the PING period earlier)
+    test "PSYNC2 pingoff: pause replica and promote it" {
+        $R(1) MULTI
+        $R(1) DEBUG SLEEP 5
+        $R(1) SLAVEOF NO ONE
+        $R(1) EXEC
+        $R(1) ping ; # Wait for it to return back available
+    }
+
+    test "Make the old master a replica of the new one and check conditions" {
+        assert_equal [status $R(1) sync_full] 0
+        $R(0) REPLICAOF $R_host(1) $R_port(1)
+        wait_for_condition 50 1000 {
+            [status $R(1) sync_full] == 1
+        } else {
+            fail "The new master was not able to sync"
+        }
+
+        # make sure replication is still alive and kicking
+        $R(1) incr x
+        wait_for_condition 50 1000 {
+            [status $R(0) loading] == 0 &&
+            [$R(0) get x] == 1
+        } else {
+            fail "replica didn't get incr"
+        }
+        assert_equal [status $R(0) master_repl_offset] [status $R(1) master_repl_offset]
+    }
+}}
+
+
+start_server {tags {"psync2 external:skip"}} {
+start_server {} {
+start_server {} {
+start_server {} {
+start_server {} {
+    test {test various edge cases of repl topology changes with missing pings at the end} {
+        set master [srv -4 client]
+        set master_host [srv -4 host]
+        set master_port [srv -4 port]
+        set replica1 [srv -3 client]
+        set replica2 [srv -2 client]
+        set replica3 [srv -1 client]
+        set replica4 [srv -0 client]
+
+        $replica1 replicaof $master_host $master_port
+        $replica2 replicaof $master_host $master_port
+        $replica3 replicaof $master_host $master_port
+        $replica4 replicaof $master_host $master_port
+        wait_for_condition 50 1000 {
+            [status $master connected_slaves] == 4
+        } else {
+            fail "replicas didn't connect"
+        }
+
+        $master incr x
+        wait_for_condition 50 1000 {
+            [$replica1 get x] == 1 && [$replica2 get x] == 1 &&
+            [$replica3 get x] == 1 && [$replica4 get x] == 1
+        } else {
+            fail "replicas didn't get incr"
+        }
+
+        # disconnect replica1 and replica2
+        # and wait for the master to send a ping to replica3 and replica4
+        $replica1 replicaof no one
+        $replica2 replicaof 127.0.0.1 1 ;# we can't promote it to master since that will cycle the replication id
+        $master config set repl-ping-replica-period 1
+        set replofs [status $master master_repl_offset]
+        wait_for_condition 50 100 {
+            [status $replica3 master_repl_offset] > $replofs &&
+            [status $replica4 master_repl_offset] > $replofs
+        } else {
+            fail "replica didn't sync in time"
+        }
+
+        # make everyone sync from the replica1 that didn't get the last ping from the old master
+        # replica4 will keep syncing from the old master which now syncs from replica1
+        # and replica2 will re-connect to the old master (which went back in time)
+        set new_master_host [srv -3 host]
+        set new_master_port [srv -3 port]
+        $replica3 replicaof $new_master_host $new_master_port
+        $master replicaof $new_master_host $new_master_port
+        $replica2 replicaof $master_host $master_port
+        wait_for_condition 50 1000 {
+            [status $replica2 master_link_status] == "up" &&
+            [status $replica3 master_link_status] == "up" &&
+            [status $replica4 master_link_status] == "up" &&
+            [status $master master_link_status] == "up"
+        } else {
+            fail "replicas didn't connect"
+        }
+
+        # make sure replication is still alive and kicking
+        $replica1 incr x
+        wait_for_condition 50 1000 {
+            [$replica2 get x] == 2 &&
+            [$replica3 get x] == 2 &&
+            [$replica4 get x] == 2 &&
+            [$master get x] == 2
+        } else {
+            fail "replicas didn't get incr"
+        }
+
+        # make sure we have the right amount of full syncs
+        assert_equal [status $master sync_full] 6
+        assert_equal [status $replica1 sync_full] 2
+        assert_equal [status $replica2 sync_full] 0
+        assert_equal [status $replica3 sync_full] 0
+        assert_equal [status $replica4 sync_full] 0
+
+        # force psync
+        $master client kill type master
+        $replica2 client kill type master
+        $replica3 client kill type master
+        $replica4 client kill type master
+
+        # make sure replication is still alive and kicking
+        $replica1 incr x
+        wait_for_condition 50 1000 {
+            [$replica2 get x] == 3 &&
+            [$replica3 get x] == 3 &&
+            [$replica4 get x] == 3 &&
+            [$master get x] == 3
+        } else {
+            fail "replicas didn't get incr"
+        }
+
+        # make sure we have the right amount of full syncs
+        assert_equal [status $master sync_full] 6
+        assert_equal [status $replica1 sync_full] 2
+        assert_equal [status $replica2 sync_full] 0
+        assert_equal [status $replica3 sync_full] 0
+        assert_equal [status $replica4 sync_full] 0
+}
+}}}}}
+
+start_server {tags {"psync2 external:skip"}} {
+start_server {} {
+start_server {} {
+
+    for {set j 0} {$j < 3} {incr j} {
+        set R($j) [srv [expr 0-$j] client]
+        set R_host($j) [srv [expr 0-$j] host]
+        set R_port($j) [srv [expr 0-$j] port]
+        $R($j) CONFIG SET repl-ping-replica-period 1
+    }
+
+    test "Chained replicas disconnect when replica re-connect with the same master" {
+        # Add a second replica as a chained replica of the current replica
+        $R(1) replicaof $R_host(0) $R_port(0)
+        $R(2) replicaof $R_host(1) $R_port(1)
+        wait_for_condition 50 1000 {
+            [status $R(2) master_link_status] == "up"
+        } else {
+            fail "Chained replica not replicating from its master"
+        }
+
+        # Do a write on the master, and wait for the master to
+        # send some PINGs to its replica
+        $R(0) INCR counter2
+        set replofs [status $R(0) master_repl_offset]
+        wait_for_condition 50 100 {
+            [status $R(1) master_repl_offset] > $replofs &&
+            [status $R(2) master_repl_offset] > $replofs
+        } else {
+            fail "replica didn't sync in time"
+        }
+        set sync_partial_master [status $R(0) sync_partial_ok]
+        set sync_partial_replica [status $R(1) sync_partial_ok]
+        $R(0) CONFIG SET repl-ping-replica-period 100
+
+        # Disconnect the master's direct replica
+        $R(0) client kill type replica
+        wait_for_condition 50 1000 {
+            [status $R(1) master_link_status] == "up" && 
+            [status $R(2) master_link_status] == "up" &&
+            [status $R(0) sync_partial_ok] == $sync_partial_master + 1 &&
+            [status $R(1) sync_partial_ok] == $sync_partial_replica
+        } else {
+            fail "Disconnected replica failed to PSYNC with master"
+        }
+
+        # Verify that the replica and its replica's meaningful and real
+        # offsets match with the master
+        assert_equal [status $R(0) master_repl_offset] [status $R(1) master_repl_offset]
+        assert_equal [status $R(0) master_repl_offset] [status $R(2) master_repl_offset]
+
+        # make sure replication is still alive and kicking
+        $R(0) incr counter2
+        wait_for_condition 50 1000 {
+            [$R(1) get counter2] == 2 && [$R(2) get counter2] == 2
+        } else {
+            fail "replicas didn't get incr"
+        }
+        assert_equal [status $R(0) master_repl_offset] [status $R(1) master_repl_offset]
+        assert_equal [status $R(0) master_repl_offset] [status $R(2) master_repl_offset]
+    }
+}}}

+ 82 - 0
tests/integration/psync2-reg.tcl

@@ -0,0 +1,82 @@
+# Issue 3899 regression test.
+# We create a chain of three instances: master -> slave -> slave2
+# and continuously break the link while traffic is generated by
+# redis-benchmark. At the end we check that the data is the same
+# everywhere.
+
+start_server {tags {"psync2 external:skip"}} {
+start_server {} {
+start_server {} {
+    # Config
+    set debug_msg 0                 ; # Enable additional debug messages
+
+    set no_exit 0                   ; # Do not exit at end of the test
+
+    set duration 20                 ; # Total test seconds
+
+    for {set j 0} {$j < 3} {incr j} {
+        set R($j) [srv [expr 0-$j] client]
+        set R_host($j) [srv [expr 0-$j] host]
+        set R_port($j) [srv [expr 0-$j] port]
+        set R_unixsocket($j) [srv [expr 0-$j] unixsocket]
+        if {$debug_msg} {puts "Log file: [srv [expr 0-$j] stdout]"}
+    }
+
+    # Setup the replication and backlog parameters
+    test "PSYNC2 #3899 regression: setup" {
+        $R(1) slaveof $R_host(0) $R_port(0)
+        $R(2) slaveof $R_host(0) $R_port(0)
+        $R(0) set foo bar
+        wait_for_condition 50 1000 {
+            [status $R(1) master_link_status] == "up" &&
+            [status $R(2) master_link_status] == "up" &&
+            [$R(1) dbsize] == 1 &&
+            [$R(2) dbsize] == 1
+        } else {
+            fail "Replicas not replicating from master"
+        }
+        $R(0) config set repl-backlog-size 10mb
+        $R(1) config set repl-backlog-size 10mb
+    }
+
+    set cycle_start_time [clock milliseconds]
+    set bench_pid [exec src/redis-benchmark -s $R_unixsocket(0) -n 10000000 -r 1000 incr __rand_int__ > /dev/null &]
+    while 1 {
+        set elapsed [expr {[clock milliseconds]-$cycle_start_time}]
+        if {$elapsed > $duration*1000} break
+        if {rand() < .05} {
+            test "PSYNC2 #3899 regression: kill first replica" {
+                $R(1) client kill type master
+            }
+        }
+        if {rand() < .05} {
+            test "PSYNC2 #3899 regression: kill chained replica" {
+                $R(2) client kill type master
+            }
+        }
+        after 100
+    }
+    exec kill -9 $bench_pid
+
+    if {$debug_msg} {
+        for {set j 0} {$j < 100} {incr j} {
+            if {
+                [$R(0) debug digest] == [$R(1) debug digest] &&
+                [$R(1) debug digest] == [$R(2) debug digest]
+            } break
+            puts [$R(0) debug digest]
+            puts [$R(1) debug digest]
+            puts [$R(2) debug digest]
+            after 1000
+        }
+    }
+
+    test "PSYNC2 #3899 regression: verify consistency" {
+        wait_for_condition 50 1000 {
+            ([$R(0) debug digest] eq [$R(1) debug digest]) &&
+            ([$R(1) debug digest] eq [$R(2) debug digest])
+        } else {
+            fail "The three instances have different data sets"
+        }
+    }
+}}}

+ 445 - 0
tests/integration/psync2.tcl

@@ -0,0 +1,445 @@
+
+proc show_cluster_status {} {
+    uplevel 1 {
+        # The following is the regexp we use to match the log line
+        # time info. Logs are in the following form:
+        #
+        # 11296:M 25 May 2020 17:37:14.652 # Server initialized
+        set log_regexp {^[0-9]+:[A-Z] [0-9]+ [A-z]+ [0-9]+ ([0-9:.]+) .*}
+        set repl_regexp {(master|repl|sync|backlog|meaningful|offset)}
+
+        puts "Master ID is $master_id"
+        for {set j 0} {$j < 5} {incr j} {
+            puts "$j: sync_full: [status $R($j) sync_full]"
+            puts "$j: id1      : [status $R($j) master_replid]:[status $R($j) master_repl_offset]"
+            puts "$j: id2      : [status $R($j) master_replid2]:[status $R($j) second_repl_offset]"
+            puts "$j: backlog  : firstbyte=[status $R($j) repl_backlog_first_byte_offset] len=[status $R($j) repl_backlog_histlen]"
+            puts "$j: x var is : [$R($j) GET x]"
+            puts "---"
+        }
+
+        # Show the replication logs of every instance, interleaving
+        # them by the log date.
+        #
+        # First: load the lines as lists for each instance.
+        array set log {}
+        for {set j 0} {$j < 5} {incr j} {
+            set fd [open $R_log($j)]
+            while {[gets $fd l] >= 0} {
+                if {[regexp $log_regexp $l] &&
+                    [regexp -nocase $repl_regexp $l]} {
+                    lappend log($j) $l
+                }
+            }
+            close $fd
+        }
+
+        # To interleave the lines, at every step consume the element of
+        # the list with the lowest time and remove it. Do it until
+        # all the lists are empty.
+        #
+        # regexp {^[0-9]+:[A-Z] [0-9]+ [A-z]+ [0-9]+ ([0-9:.]+) .*} $l - logdate
+        while 1 {
+            # Find the log with smallest time.
+            set empty 0
+            set best 0
+            set bestdate {}
+            for {set j 0} {$j < 5} {incr j} {
+                if {[llength $log($j)] == 0} {
+                    incr empty
+                    continue
+                }
+                regexp $log_regexp [lindex $log($j) 0] - date
+                if {$bestdate eq {}} {
+                    set best $j
+                    set bestdate $date
+                } else {
+                    if {[string compare $bestdate $date] > 0} {
+                        set best $j
+                        set bestdate $date
+                    }
+                }
+            }
+            if {$empty == 5} break ; # Our exit condition: no more logs
+
+            # Emit the one with the smallest time (that is the first
+            # event in the time line).
+            puts "\[$best port $R_port($best)\] [lindex $log($best) 0]"
+            set log($best) [lrange $log($best) 1 end]
+        }
+    }
+}
+
+start_server {tags {"psync2 external:skip"}} {
+start_server {} {
+start_server {} {
+start_server {} {
+start_server {} {
+    set master_id 0                 ; # Current master
+    set start_time [clock seconds]  ; # Test start time
+    set counter_value 0             ; # Current value of the Redis counter "x"
+
+    # Config
+    set debug_msg 0                 ; # Enable additional debug messages
+
+    set no_exit 0                   ; # Do not exit at end of the test
+
+    set duration 40                 ; # Total test seconds
+
+    set genload 1                   ; # Load master with writes at every cycle
+
+    set genload_time 5000           ; # Writes duration time in ms
+
+    set disconnect 1                ; # Break replication link between random
+                                      # master and slave instances while the
+                                      # master is loaded with writes.
+
+    set disconnect_period 1000      ; # Disconnect repl link every N ms.
+
+    for {set j 0} {$j < 5} {incr j} {
+        set R($j) [srv [expr 0-$j] client]
+        set R_host($j) [srv [expr 0-$j] host]
+        set R_port($j) [srv [expr 0-$j] port]
+        set R_id_from_port($R_port($j)) $j ;# To get a replica index by port
+        set R_log($j) [srv [expr 0-$j] stdout]
+        if {$debug_msg} {puts "Log file: [srv [expr 0-$j] stdout]"}
+    }
+
+    set cycle 0
+    while {([clock seconds]-$start_time) < $duration} {
+        incr cycle
+        test "PSYNC2: --- CYCLE $cycle ---" {}
+
+        # Create a random replication layout.
+        # Start with switching master (this simulates a failover).
+
+        # 1) Select the new master.
+        set master_id [randomInt 5]
+        set used [list $master_id]
+        test "PSYNC2: \[NEW LAYOUT\] Set #$master_id as master" {
+            $R($master_id) slaveof no one
+            $R($master_id) config set repl-ping-replica-period 1 ;# increase the chance that random ping will cause issues
+            if {$counter_value == 0} {
+                $R($master_id) set x $counter_value
+            }
+        }
+
+        # Build a lookup with the root master of each replica (head of the chain).
+        array set root_master {}
+        for {set j 0} {$j < 5} {incr j} {
+            set r $j
+            while {1} {
+                set r_master_port [status $R($r) master_port]
+                if {$r_master_port == ""} {
+                    set root_master($j) $r
+                    break
+                }
+                set r_master_id $R_id_from_port($r_master_port)
+                set r $r_master_id
+            }
+        }
+
+        # Wait for the newly detached master-replica chain (new master and existing replicas that were
+        # already connected to it, to get updated on the new replication id.
+        # This is needed to avoid a race that can result in a full sync when a replica that already
+        # got an updated repl id, tries to psync from one that's not yet aware of it.
+        wait_for_condition 50 1000 {
+            ([status $R(0) master_replid] == [status $R($root_master(0)) master_replid]) &&
+            ([status $R(1) master_replid] == [status $R($root_master(1)) master_replid]) &&
+            ([status $R(2) master_replid] == [status $R($root_master(2)) master_replid]) &&
+            ([status $R(3) master_replid] == [status $R($root_master(3)) master_replid]) &&
+            ([status $R(4) master_replid] == [status $R($root_master(4)) master_replid])
+        } else {
+            show_cluster_status
+            fail "Replica did not inherit the new replid."
+        }
+
+        # Build a lookup with the direct connection master of each replica.
+        # First loop that uses random to decide who replicates from who.
+        array set slave_to_master {}
+        while {[llength $used] != 5} {
+            while 1 {
+                set slave_id [randomInt 5]
+                if {[lsearch -exact $used $slave_id] == -1} break
+            }
+            set rand [randomInt [llength $used]]
+            set mid [lindex $used $rand]
+            set slave_to_master($slave_id) $mid
+            lappend used $slave_id
+        }
+
+        # 2) Attach all the slaves to a random instance
+        # Second loop that does the actual SLAVEOF command and make sure execute it in the right order.
+        while {[array size slave_to_master] > 0} {
+            foreach slave_id [array names slave_to_master] {
+                set mid $slave_to_master($slave_id)
+
+                # We only attach the replica to a random instance that already in the old/new chain.
+                if {$root_master($mid) == $root_master($master_id)} {
+                    # Find a replica that can be attached to the new chain already attached to the new master.
+                    # My new master is in the new chain.
+                } elseif {$root_master($mid) == $root_master($slave_id)} {
+                    # My new master and I are in the old chain.
+                } else {
+                    # In cycle 1, we do not care about the order.
+                    if {$cycle != 1} {
+                        # skipping this replica for now to avoid attaching in a bad order
+                        # this is done to avoid an unexpected full sync, when we take a
+                        # replica that already reconnected to the new chain and got a new replid
+                        # and is then set to connect to a master that's still not aware of that new replid
+                        continue
+                    }
+                }
+
+                set master_host $R_host($master_id)
+                set master_port $R_port($master_id)
+
+                test "PSYNC2: Set #$slave_id to replicate from #$mid" {
+                    $R($slave_id) slaveof $master_host $master_port
+                }
+
+                # Wait for replica to be connected before we proceed.
+                wait_for_condition 50 1000 {
+                    [status $R($slave_id) master_link_status] == "up"
+                } else {
+                    show_cluster_status
+                    fail "Replica not reconnecting."
+                }
+
+                set root_master($slave_id) $root_master($mid)
+                unset slave_to_master($slave_id)
+                break
+            }
+        }
+
+        # Wait for replicas to sync. so next loop won't get -LOADING error
+        wait_for_condition 50 1000 {
+            [status $R([expr {($master_id+1)%5}]) master_link_status] == "up" &&
+            [status $R([expr {($master_id+2)%5}]) master_link_status] == "up" &&
+            [status $R([expr {($master_id+3)%5}]) master_link_status] == "up" &&
+            [status $R([expr {($master_id+4)%5}]) master_link_status] == "up"
+        } else {
+            show_cluster_status
+            fail "Replica not reconnecting"
+        }
+
+        # 3) Increment the counter and wait for all the instances
+        # to converge.
+        test "PSYNC2: cluster is consistent after failover" {
+            $R($master_id) incr x; incr counter_value
+            for {set j 0} {$j < 5} {incr j} {
+                wait_for_condition 50 1000 {
+                    [$R($j) get x] == $counter_value
+                } else {
+                    show_cluster_status
+                    fail "Instance #$j x variable is inconsistent"
+                }
+            }
+        }
+
+        # 4) Generate load while breaking the connection of random
+        # slave-master pairs.
+        test "PSYNC2: generate load while killing replication links" {
+            set t [clock milliseconds]
+            set next_break [expr {$t+$disconnect_period}]
+            while {[clock milliseconds]-$t < $genload_time} {
+                if {$genload} {
+                    $R($master_id) incr x; incr counter_value
+                }
+                if {[clock milliseconds] == $next_break} {
+                    set next_break \
+                        [expr {[clock milliseconds]+$disconnect_period}]
+                    set slave_id [randomInt 5]
+                    if {$disconnect} {
+                        $R($slave_id) client kill type master
+                        if {$debug_msg} {
+                            puts "+++ Breaking link for replica #$slave_id"
+                        }
+                    }
+                }
+            }
+        }
+
+        # 5) Increment the counter and wait for all the instances
+        set x [$R($master_id) get x]
+        test "PSYNC2: cluster is consistent after load (x = $x)" {
+            for {set j 0} {$j < 5} {incr j} {
+                wait_for_condition 50 1000 {
+                    [$R($j) get x] == $counter_value
+                } else {
+                    show_cluster_status
+                    fail "Instance #$j x variable is inconsistent"
+                }
+            }
+        }
+
+        # wait for all the slaves to be in sync.
+        set masteroff [status $R($master_id) master_repl_offset]
+        wait_for_condition 500 100 {
+            [status $R(0) master_repl_offset] >= $masteroff &&
+            [status $R(1) master_repl_offset] >= $masteroff &&
+            [status $R(2) master_repl_offset] >= $masteroff &&
+            [status $R(3) master_repl_offset] >= $masteroff &&
+            [status $R(4) master_repl_offset] >= $masteroff
+        } else {
+            show_cluster_status
+            fail "Replicas offsets didn't catch up with the master after too long time."
+        }
+
+        if {$debug_msg} {
+            show_cluster_status
+        }
+
+        test "PSYNC2: total sum of full synchronizations is exactly 4" {
+            set sum 0
+            for {set j 0} {$j < 5} {incr j} {
+                incr sum [status $R($j) sync_full]
+            }
+            if {$sum != 4} {
+                show_cluster_status
+                assert {$sum == 4}
+            }
+        }
+
+        # In absence of pings, are the instances really able to have
+        # the exact same offset?
+        $R($master_id) config set repl-ping-replica-period 3600
+        wait_for_condition 500 100 {
+            [status $R($master_id) master_repl_offset] == [status $R(0) master_repl_offset] &&
+            [status $R($master_id) master_repl_offset] == [status $R(1) master_repl_offset] &&
+            [status $R($master_id) master_repl_offset] == [status $R(2) master_repl_offset] &&
+            [status $R($master_id) master_repl_offset] == [status $R(3) master_repl_offset] &&
+            [status $R($master_id) master_repl_offset] == [status $R(4) master_repl_offset]
+        } else {
+            show_cluster_status
+            fail "Replicas and master offsets were unable to match *exactly*."
+        }
+
+        # Limit anyway the maximum number of cycles. This is useful when the
+        # test is skipped via --only option of the test suite. In that case
+        # we don't want to see many seconds of this test being just skipped.
+        if {$cycle > 50} break
+    }
+
+    test "PSYNC2: Bring the master back again for next test" {
+        $R($master_id) slaveof no one
+        set master_host $R_host($master_id)
+        set master_port $R_port($master_id)
+        for {set j 0} {$j < 5} {incr j} {
+            if {$j == $master_id} continue
+            $R($j) slaveof $master_host $master_port
+        }
+
+        # Wait for replicas to sync. it is not enough to just wait for connected_slaves==4
+        # since we might do the check before the master realized that they're disconnected
+        wait_for_condition 50 1000 {
+            [status $R($master_id) connected_slaves] == 4 &&
+            [status $R([expr {($master_id+1)%5}]) master_link_status] == "up" &&
+            [status $R([expr {($master_id+2)%5}]) master_link_status] == "up" &&
+            [status $R([expr {($master_id+3)%5}]) master_link_status] == "up" &&
+            [status $R([expr {($master_id+4)%5}]) master_link_status] == "up"
+        } else {
+            show_cluster_status
+            fail "Replica not reconnecting"
+        }
+    }
+
+    test "PSYNC2: Partial resync after restart using RDB aux fields" {
+        # Pick a random slave
+        set slave_id [expr {($master_id+1)%5}]
+        set sync_count [status $R($master_id) sync_full]
+        set sync_partial [status $R($master_id) sync_partial_ok]
+        set sync_partial_err [status $R($master_id) sync_partial_err]
+        catch {
+            $R($slave_id) config rewrite
+            restart_server [expr {0-$slave_id}] true false
+            set R($slave_id) [srv [expr {0-$slave_id}] client]
+        }
+        # note: just waiting for connected_slaves==4 has a race condition since
+        # we might do the check before the master realized that the slave disconnected
+        wait_for_condition 50 1000 {
+            [status $R($master_id) sync_partial_ok] == $sync_partial + 1
+        } else {
+            puts "prev sync_full: $sync_count"
+            puts "prev sync_partial_ok: $sync_partial"
+            puts "prev sync_partial_err: $sync_partial_err"
+            puts [$R($master_id) info stats]
+            show_cluster_status
+            fail "Replica didn't partial sync"
+        }
+        set new_sync_count [status $R($master_id) sync_full]
+        assert {$sync_count == $new_sync_count}
+    }
+
+    test "PSYNC2: Replica RDB restart with EVALSHA in backlog issue #4483" {
+        # Pick a random slave
+        set slave_id [expr {($master_id+1)%5}]
+        set sync_count [status $R($master_id) sync_full]
+
+        # Make sure to replicate the first EVAL while the salve is online
+        # so that it's part of the scripts the master believes it's safe
+        # to propagate as EVALSHA.
+        $R($master_id) EVAL {return redis.call("incr","__mycounter")} 0
+        $R($master_id) EVALSHA e6e0b547500efcec21eddb619ac3724081afee89 0
+
+        # Wait for the two to sync
+        wait_for_condition 50 1000 {
+            [$R($master_id) debug digest] == [$R($slave_id) debug digest]
+        } else {
+            show_cluster_status
+            fail "Replica not reconnecting"
+        }
+
+        # Prevent the slave from receiving master updates, and at
+        # the same time send a new script several times to the
+        # master, so that we'll end with EVALSHA into the backlog.
+        $R($slave_id) slaveof 127.0.0.1 0
+
+        $R($master_id) EVALSHA e6e0b547500efcec21eddb619ac3724081afee89 0
+        $R($master_id) EVALSHA e6e0b547500efcec21eddb619ac3724081afee89 0
+        $R($master_id) EVALSHA e6e0b547500efcec21eddb619ac3724081afee89 0
+
+        catch {
+            $R($slave_id) config rewrite
+            restart_server [expr {0-$slave_id}] true false
+            set R($slave_id) [srv [expr {0-$slave_id}] client]
+        }
+
+        # Reconfigure the slave correctly again, when it's back online.
+        set retry 50
+        while {$retry} {
+            if {[catch {
+                $R($slave_id) slaveof $master_host $master_port
+            }]} {
+                after 1000
+            } else {
+                break
+            }
+            incr retry -1
+        }
+
+        # The master should be back at 4 slaves eventually
+        wait_for_condition 50 1000 {
+            [status $R($master_id) connected_slaves] == 4
+        } else {
+            show_cluster_status
+            fail "Replica not reconnecting"
+        }
+        set new_sync_count [status $R($master_id) sync_full]
+        assert {$sync_count == $new_sync_count}
+
+        # However if the slave started with the full state of the
+        # scripting engine, we should now have the same digest.
+        wait_for_condition 50 1000 {
+            [$R($master_id) debug digest] == [$R($slave_id) debug digest]
+        } else {
+            show_cluster_status
+            fail "Debug digest mismatch between master and replica in post-restart handshake"
+        }
+    }
+
+    if {$no_exit} {
+        while 1 { puts -nonewline .; flush stdout; after 1000}
+    }
+
+}}}}}

+ 314 - 0
tests/integration/rdb.tcl

@@ -0,0 +1,314 @@
+tags {"rdb external:skip"} {
+
+set server_path [tmpdir "server.rdb-encoding-test"]
+
+# Copy RDB with different encodings in server path
+exec cp tests/assets/encodings.rdb $server_path
+
+start_server [list overrides [list "dir" $server_path "dbfilename" "encodings.rdb"]] {
+  test "RDB encoding loading test" {
+    r select 0
+    csvdump r
+  } {"0","compressible","string","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+"0","hash","hash","a","1","aa","10","aaa","100","b","2","bb","20","bbb","200","c","3","cc","30","ccc","300","ddd","400","eee","5000000000",
+"0","hash_zipped","hash","a","1","b","2","c","3",
+"0","list","list","1","2","3","a","b","c","100000","6000000000","1","2","3","a","b","c","100000","6000000000","1","2","3","a","b","c","100000","6000000000",
+"0","list_zipped","list","1","2","3","a","b","c","100000","6000000000",
+"0","number","string","10"
+"0","set","set","1","100000","2","3","6000000000","a","b","c",
+"0","set_zipped_1","set","1","2","3","4",
+"0","set_zipped_2","set","100000","200000","300000","400000",
+"0","set_zipped_3","set","1000000000","2000000000","3000000000","4000000000","5000000000","6000000000",
+"0","string","string","Hello World"
+"0","zset","zset","a","1","b","2","c","3","aa","10","bb","20","cc","30","aaa","100","bbb","200","ccc","300","aaaa","1000","cccc","123456789","bbbb","5000000000",
+"0","zset_zipped","zset","a","1","b","2","c","3",
+}
+}
+
+set server_path [tmpdir "server.rdb-startup-test"]
+
+start_server [list overrides [list "dir" $server_path] keep_persistence true] {
+    test {Server started empty with non-existing RDB file} {
+        r debug digest
+    } {0000000000000000000000000000000000000000}
+    # Save an RDB file, needed for the next test.
+    r save
+}
+
+start_server [list overrides [list "dir" $server_path] keep_persistence true] {
+    test {Server started empty with empty RDB file} {
+        r debug digest
+    } {0000000000000000000000000000000000000000}
+}
+
+start_server [list overrides [list "dir" $server_path] keep_persistence true] {
+    test {Test RDB stream encoding} {
+        for {set j 0} {$j < 1000} {incr j} {
+            if {rand() < 0.9} {
+                r xadd stream * foo $j
+            } else {
+                r xadd stream * bar $j
+            }
+        }
+        r xgroup create stream mygroup 0
+        set records [r xreadgroup GROUP mygroup Alice COUNT 2 STREAMS stream >]
+        r xdel stream [lindex [lindex [lindex [lindex $records 0] 1] 1] 0]
+        r xack stream mygroup [lindex [lindex [lindex [lindex $records 0] 1] 0] 0]
+        set digest [r debug digest]
+        r config set sanitize-dump-payload no
+        r debug reload
+        set newdigest [r debug digest]
+        assert {$digest eq $newdigest}
+    }
+    test {Test RDB stream encoding - sanitize dump} {
+        r config set sanitize-dump-payload yes
+        r debug reload
+        set newdigest [r debug digest]
+        assert {$digest eq $newdigest}
+    }
+    # delete the stream, maybe valgrind will find something
+    r del stream
+}
+
+# Helper function to start a server and kill it, just to check the error
+# logged.
+set defaults {}
+proc start_server_and_kill_it {overrides code} {
+    upvar defaults defaults srv srv server_path server_path
+    set config [concat $defaults $overrides]
+    set srv [start_server [list overrides $config keep_persistence true]]
+    uplevel 1 $code
+    kill_server $srv
+}
+
+# Make the RDB file unreadable
+file attributes [file join $server_path dump.rdb] -permissions 0222
+
+# Detect root account (it is able to read the file even with 002 perm)
+set isroot 0
+catch {
+    open [file join $server_path dump.rdb]
+    set isroot 1
+}
+
+# Now make sure the server aborted with an error
+if {!$isroot} {
+    start_server_and_kill_it [list "dir" $server_path] {
+        test {Server should not start if RDB file can't be open} {
+            wait_for_condition 50 100 {
+                [string match {*Fatal error loading*} \
+                    [exec tail -1 < [dict get $srv stdout]]]
+            } else {
+                fail "Server started even if RDB was unreadable!"
+            }
+        }
+    }
+}
+
+# Fix permissions of the RDB file.
+file attributes [file join $server_path dump.rdb] -permissions 0666
+
+# Corrupt its CRC64 checksum.
+set filesize [file size [file join $server_path dump.rdb]]
+set fd [open [file join $server_path dump.rdb] r+]
+fconfigure $fd -translation binary
+seek $fd -8 end
+puts -nonewline $fd "foobar00"; # Corrupt the checksum
+close $fd
+
+# Now make sure the server aborted with an error
+start_server_and_kill_it [list "dir" $server_path] {
+    test {Server should not start if RDB is corrupted} {
+        wait_for_condition 50 100 {
+            [string match {*CRC error*} \
+                [exec tail -10 < [dict get $srv stdout]]]
+        } else {
+            fail "Server started even if RDB was corrupted!"
+        }
+    }
+}
+
+start_server {} {
+    test {Test FLUSHALL aborts bgsave} {
+        # 1000 keys with 1ms sleep per key should take 1 second
+        r config set rdb-key-save-delay 1000
+        r debug populate 1000
+        r bgsave
+        assert_equal [s rdb_bgsave_in_progress] 1
+        r flushall
+        # wait half a second max
+        wait_for_condition 5 100 {
+            [s rdb_bgsave_in_progress] == 0
+        } else {
+            fail "bgsave not aborted"
+        }
+        # veirfy that bgsave failed, by checking that the change counter is still high
+        assert_lessthan 999 [s rdb_changes_since_last_save]
+        # make sure the server is still writable
+        r set x xx
+    }
+
+    test {bgsave resets the change counter} {
+        r config set rdb-key-save-delay 0
+        r bgsave
+        wait_for_condition 50 100 {
+            [s rdb_bgsave_in_progress] == 0
+        } else {
+            fail "bgsave not done"
+        }
+        assert_equal [s rdb_changes_since_last_save] 0
+    }
+}
+
+test {client freed during loading} {
+    start_server [list overrides [list key-load-delay 50 rdbcompression no]] {
+        # create a big rdb that will take long to load. it is important
+        # for keys to be big since the server processes events only once in 2mb.
+        # 100mb of rdb, 100k keys will load in more than 5 seconds
+        r debug populate 100000 key 1000
+
+        restart_server 0 false false
+
+        # make sure it's still loading
+        assert_equal [s loading] 1
+
+        # connect and disconnect 5 clients
+        set clients {}
+        for {set j 0} {$j < 5} {incr j} {
+            lappend clients [redis_deferring_client]
+        }
+        foreach rd $clients {
+            $rd debug log bla
+        }
+        foreach rd $clients {
+            $rd read
+        }
+        foreach rd $clients {
+            $rd close
+        }
+
+        # make sure the server freed the clients
+        wait_for_condition 100 100 {
+            [s connected_clients] < 3
+        } else {
+            fail "clients didn't disconnect"
+        }
+
+        # make sure it's still loading
+        assert_equal [s loading] 1
+
+        # no need to keep waiting for loading to complete
+        exec kill [srv 0 pid]
+    }
+}
+
+# Our COW metrics (Private_Dirty) work only on Linux
+set system_name [string tolower [exec uname -s]]
+if {$system_name eq {linux}} {
+
+start_server {overrides {save ""}} {
+    test {Test child sending info} {
+        # make sure that rdb_last_cow_size and current_cow_size are zero (the test using new server),
+        # so that the comparisons during the test will be valid
+        assert {[s current_cow_size] == 0}
+        assert {[s current_save_keys_processed] == 0}
+        assert {[s current_save_keys_total] == 0}
+
+        assert {[s rdb_last_cow_size] == 0}
+
+        # using a 200us delay, the bgsave is empirically taking about 10 seconds.
+        # we need it to take more than some 5 seconds, since redis only report COW once a second.
+        r config set rdb-key-save-delay 200
+        r config set loglevel debug
+
+        # populate the db with 10k keys of 4k each
+        set rd [redis_deferring_client 0]
+        set size 4096
+        set cmd_count 10000
+        for {set k 0} {$k < $cmd_count} {incr k} {
+            $rd set key$k [string repeat A $size]
+        }
+
+        for {set k 0} {$k < $cmd_count} {incr k} {
+            catch { $rd read }
+        }
+
+        $rd close
+
+        # start background rdb save
+        r bgsave
+
+        set current_save_keys_total [s current_save_keys_total]
+        if {$::verbose} {
+            puts "Keys before bgsave start: current_save_keys_total"
+        }
+
+        # on each iteration, we will write some key to the server to trigger copy-on-write, and
+        # wait to see that it reflected in INFO.
+        set iteration 1
+        while 1 {
+            # take samples before writing new data to the server
+            set cow_size [s current_cow_size]
+            if {$::verbose} {
+                puts "COW info before copy-on-write: $cow_size"
+            }
+
+            set keys_processed [s current_save_keys_processed]
+            if {$::verbose} {
+                puts "current_save_keys_processed info : $keys_processed"
+            }
+
+            # trigger copy-on-write
+            r setrange key$iteration 0 [string repeat B $size]
+
+            # wait to see that current_cow_size value updated (as long as the child is in progress)
+            wait_for_condition 80 100 {
+                [s rdb_bgsave_in_progress] == 0 ||
+                [s current_cow_size] >= $cow_size + $size && 
+                [s current_save_keys_processed] > $keys_processed &&
+                [s current_fork_perc] > 0
+            } else {
+                if {$::verbose} {
+                    puts "COW info on fail: [s current_cow_size]"
+                    puts [exec tail -n 100 < [srv 0 stdout]]
+                }
+                fail "COW info wasn't reported"
+            }
+
+            # assert that $keys_processed is not greater than total keys.
+            assert_morethan_equal $current_save_keys_total $keys_processed
+
+            # for no accurate, stop after 2 iterations
+            if {!$::accurate && $iteration == 2} {
+                break
+            }
+
+            # stop iterating if the bgsave completed
+            if { [s rdb_bgsave_in_progress] == 0 } {
+                break
+            }
+
+            incr iteration 1
+        }
+
+        # make sure we saw report of current_cow_size
+        if {$iteration < 2 && $::verbose} {
+            puts [exec tail -n 100 < [srv 0 stdout]]
+        }
+        assert_morethan_equal $iteration 2
+
+        # if bgsave completed, check that rdb_last_cow_size (fork exit report)
+        # is at least 90% of last rdb_active_cow_size.
+        if { [s rdb_bgsave_in_progress] == 0 } {
+            set final_cow [s rdb_last_cow_size]
+            set cow_size [expr $cow_size * 0.9]
+            if {$final_cow < $cow_size && $::verbose} {
+                puts [exec tail -n 100 < [srv 0 stdout]]
+            }
+            assert_morethan_equal $final_cow $cow_size
+        }
+    }
+}
+} ;# system_name
+
+} ;# tags

+ 168 - 0
tests/integration/redis-benchmark.tcl

@@ -0,0 +1,168 @@
+source tests/support/benchmark.tcl
+
+
+proc cmdstat {cmd} {
+    return [cmdrstat $cmd r]
+}
+
+start_server {tags {"benchmark network external:skip"}} {
+    start_server {} {
+        set master_host [srv 0 host]
+        set master_port [srv 0 port]
+
+        test {benchmark: set,get} {
+            r config resetstat
+            r flushall
+            set cmd [redisbenchmark $master_host $master_port "-c 5 -n 10 -t set,get"]
+            if {[catch { exec {*}$cmd } error]} {
+                set first_line [lindex [split $error "\n"] 0]
+                puts [colorstr red "redis-benchmark non zero code. first line: $first_line"]
+                fail "redis-benchmark non zero code. first line: $first_line"
+            }
+            assert_match  {*calls=10,*} [cmdstat set]
+            assert_match  {*calls=10,*} [cmdstat get]
+            # assert one of the non benchmarked commands is not present
+            assert_match  {} [cmdstat lrange]
+        }
+
+        test {benchmark: full test suite} {
+            r config resetstat
+            set cmd [redisbenchmark $master_host $master_port "-c 10 -n 100"]
+            if {[catch { exec {*}$cmd } error]} {
+                set first_line [lindex [split $error "\n"] 0]
+                puts [colorstr red "redis-benchmark non zero code. first line: $first_line"]
+                fail "redis-benchmark non zero code. first line: $first_line"
+            }
+            # ping total calls are 2*issued commands per test due to PING_INLINE and PING_MBULK
+            assert_match  {*calls=200,*} [cmdstat ping]
+            assert_match  {*calls=100,*} [cmdstat set]
+            assert_match  {*calls=100,*} [cmdstat get]
+            assert_match  {*calls=100,*} [cmdstat incr]
+            # lpush total calls are 2*issued commands per test due to the lrange tests
+            assert_match  {*calls=200,*} [cmdstat lpush]
+            assert_match  {*calls=100,*} [cmdstat rpush]
+            assert_match  {*calls=100,*} [cmdstat lpop]
+            assert_match  {*calls=100,*} [cmdstat rpop]
+            assert_match  {*calls=100,*} [cmdstat sadd]
+            assert_match  {*calls=100,*} [cmdstat hset]
+            assert_match  {*calls=100,*} [cmdstat spop]
+            assert_match  {*calls=100,*} [cmdstat zadd]
+            assert_match  {*calls=100,*} [cmdstat zpopmin]
+            assert_match  {*calls=400,*} [cmdstat lrange]
+            assert_match  {*calls=100,*} [cmdstat mset]
+            # assert one of the non benchmarked commands is not present
+            assert_match {} [cmdstat rpoplpush]
+        }
+
+        test {benchmark: multi-thread set,get} {
+            r config resetstat
+            r flushall
+            set cmd [redisbenchmark $master_host $master_port "--threads 10 -c 5 -n 10 -t set,get"]
+            if {[catch { exec {*}$cmd } error]} {
+                set first_line [lindex [split $error "\n"] 0]
+                puts [colorstr red "redis-benchmark non zero code. first line: $first_line"]
+                fail "redis-benchmark non zero code. first line: $first_line"
+            }
+            assert_match  {*calls=10,*} [cmdstat set]
+            assert_match  {*calls=10,*} [cmdstat get]
+            # assert one of the non benchmarked commands is not present
+            assert_match  {} [cmdstat lrange]
+
+            # ensure only one key was populated
+            assert_match  {1} [scan [regexp -inline {keys\=([\d]*)} [r info keyspace]] keys=%d]
+        }
+
+        test {benchmark: pipelined full set,get} {
+            r config resetstat
+            r flushall
+            set cmd [redisbenchmark $master_host $master_port "-P 5 -c 10 -n 10010 -t set,get"]
+            if {[catch { exec {*}$cmd } error]} {
+                set first_line [lindex [split $error "\n"] 0]
+                puts [colorstr red "redis-benchmark non zero code. first line: $first_line"]
+                fail "redis-benchmark non zero code. first line: $first_line"
+            }
+            assert_match  {*calls=10010,*} [cmdstat set]
+            assert_match  {*calls=10010,*} [cmdstat get]
+            # assert one of the non benchmarked commands is not present
+            assert_match  {} [cmdstat lrange]
+
+            # ensure only one key was populated
+            assert_match  {1} [scan [regexp -inline {keys\=([\d]*)} [r info keyspace]] keys=%d]
+        }
+
+        test {benchmark: arbitrary command} {
+            r config resetstat
+            r flushall
+            set cmd [redisbenchmark $master_host $master_port "-c 5 -n 150 INCRBYFLOAT mykey 10.0"]
+            if {[catch { exec {*}$cmd } error]} {
+                set first_line [lindex [split $error "\n"] 0]
+                puts [colorstr red "redis-benchmark non zero code. first line: $first_line"]
+                fail "redis-benchmark non zero code. first line: $first_line"
+            }
+            assert_match  {*calls=150,*} [cmdstat incrbyfloat]
+            # assert one of the non benchmarked commands is not present
+            assert_match  {} [cmdstat get]
+
+            # ensure only one key was populated
+            assert_match  {1} [scan [regexp -inline {keys\=([\d]*)} [r info keyspace]] keys=%d]
+        }
+
+        test {benchmark: keyspace length} {
+            r flushall
+            r config resetstat
+            set cmd [redisbenchmark $master_host $master_port "-r 50 -t set -n 1000"]
+            if {[catch { exec {*}$cmd } error]} {
+                set first_line [lindex [split $error "\n"] 0]
+                puts [colorstr red "redis-benchmark non zero code. first line: $first_line"]
+                fail "redis-benchmark non zero code. first line: $first_line"
+            }
+            assert_match  {*calls=1000,*} [cmdstat set]
+            # assert one of the non benchmarked commands is not present
+            assert_match  {} [cmdstat get]
+
+            # ensure the keyspace has the desired size
+            assert_match  {50} [scan [regexp -inline {keys\=([\d]*)} [r info keyspace]] keys=%d]
+        }
+
+        # tls specific tests
+        if {$::tls} {
+            test {benchmark: specific tls-ciphers} {
+                r flushall
+                r config resetstat
+                set cmd [redisbenchmark $master_host $master_port "-r 50 -t set -n 1000 --tls-ciphers \"DEFAULT:-AES128-SHA256\""]
+                if {[catch { exec {*}$cmd } error]} {
+                    set first_line [lindex [split $error "\n"] 0]
+                    puts [colorstr red "redis-benchmark non zero code. first line: $first_line"]
+                    fail "redis-benchmark non zero code. first line: $first_line"
+                }
+                assert_match  {*calls=1000,*} [cmdstat set]
+                # assert one of the non benchmarked commands is not present
+                assert_match  {} [cmdstat get]
+            }
+
+            test {benchmark: specific tls-ciphersuites} {
+                r flushall
+                r config resetstat
+                set ciphersuites_supported 1
+                set cmd [redisbenchmark $master_host $master_port "-r 50 -t set -n 1000 --tls-ciphersuites \"TLS_AES_128_GCM_SHA256\""]
+                if {[catch { exec {*}$cmd } error]} {
+                    set first_line [lindex [split $error "\n"] 0]
+                    if {[string match "*Invalid option*" $first_line]} {
+                        set ciphersuites_supported 0
+                        if {$::verbose} {
+                            puts "Skipping test, TLSv1.3 not supported."
+                        }
+                    } else {
+                        puts [colorstr red "redis-benchmark non zero code. first line: $first_line"]
+                        fail "redis-benchmark non zero code. first line: $first_line"
+                    }
+                }
+                if {$ciphersuites_supported} {
+                    assert_match  {*calls=1000,*} [cmdstat set]
+                    # assert one of the non benchmarked commands is not present
+                    assert_match  {} [cmdstat get]
+                }
+            }
+        }
+    }
+}

+ 402 - 0
tests/integration/redis-cli.tcl

@@ -0,0 +1,402 @@
+source tests/support/cli.tcl
+
+if {$::singledb} {
+    set ::dbnum 0
+} else {
+    set ::dbnum 9
+}
+
+start_server {tags {"cli"}} {
+    proc open_cli {{opts ""} {infile ""}} {
+        if { $opts == "" } {
+            set opts "-n $::dbnum"
+        }
+        set ::env(TERM) dumb
+        set cmdline [rediscli [srv host] [srv port] $opts]
+        if {$infile ne ""} {
+            set cmdline "$cmdline < $infile"
+            set mode "r"
+        } else {
+            set mode "r+"
+        }
+        set fd [open "|$cmdline" $mode]
+        fconfigure $fd -buffering none
+        fconfigure $fd -blocking false
+        fconfigure $fd -translation binary
+        set _ $fd
+    }
+
+    proc close_cli {fd} {
+        close $fd
+    }
+
+    proc read_cli {fd} {
+        set ret [read $fd]
+        while {[string length $ret] == 0} {
+            after 10
+            set ret [read $fd]
+        }
+
+        # We may have a short read, try to read some more.
+        set empty_reads 0
+        while {$empty_reads < 5} {
+            set buf [read $fd]
+            if {[string length $buf] == 0} {
+                after 10
+                incr empty_reads
+            } else {
+                append ret $buf
+                set empty_reads 0
+            }
+        }
+        return $ret
+    }
+
+    proc write_cli {fd buf} {
+        puts $fd $buf
+        flush $fd
+    }
+
+    # Helpers to run tests in interactive mode
+
+    proc format_output {output} {
+        set _ [string trimright [regsub -all "\r" $output ""] "\n"]
+    }
+
+    proc run_command {fd cmd} {
+        write_cli $fd $cmd
+        set _ [format_output [read_cli $fd]]
+    }
+
+    proc test_interactive_cli {name code} {
+        set ::env(FAKETTY) 1
+        set fd [open_cli]
+        test "Interactive CLI: $name" $code
+        close_cli $fd
+        unset ::env(FAKETTY)
+    }
+
+    # Helpers to run tests where stdout is not a tty
+    proc write_tmpfile {contents} {
+        set tmp [tmpfile "cli"]
+        set tmpfd [open $tmp "w"]
+        puts -nonewline $tmpfd $contents
+        close $tmpfd
+        set _ $tmp
+    }
+
+    proc _run_cli {host port db opts args} {
+        set cmd [rediscli $host $port [list -n $db {*}$args]]
+        foreach {key value} $opts {
+            if {$key eq "pipe"} {
+                set cmd "sh -c \"$value | $cmd\""
+            }
+            if {$key eq "path"} {
+                set cmd "$cmd < $value"
+            }
+        }
+
+        set fd [open "|$cmd" "r"]
+        fconfigure $fd -buffering none
+        fconfigure $fd -translation binary
+        set resp [read $fd 1048576]
+        close $fd
+        set _ [format_output $resp]
+    }
+
+    proc run_cli {args} {
+        _run_cli [srv host] [srv port] $::dbnum {} {*}$args
+    }
+
+    proc run_cli_with_input_pipe {cmd args} {
+        _run_cli [srv host] [srv port] $::dbnum [list pipe $cmd] -x {*}$args
+    }
+
+    proc run_cli_with_input_file {path args} {
+        _run_cli [srv host] [srv port] $::dbnum [list path $path] -x {*}$args
+    }
+
+    proc run_cli_host_port_db {host port db args} {
+        _run_cli $host $port $db {} {*}$args
+    }
+
+    proc test_nontty_cli {name code} {
+        test "Non-interactive non-TTY CLI: $name" $code
+    }
+
+    # Helpers to run tests where stdout is a tty (fake it)
+    proc test_tty_cli {name code} {
+        set ::env(FAKETTY) 1
+        test "Non-interactive TTY CLI: $name" $code
+        unset ::env(FAKETTY)
+    }
+
+    test_interactive_cli "INFO response should be printed raw" {
+        set lines [split [run_command $fd info] "\n"]
+        foreach line $lines {
+            if {![regexp {^$|^#|^[^#:]+:} $line]} {
+                fail "Malformed info line: $line"
+            }
+        }
+    }
+
+    test_interactive_cli "Status reply" {
+        assert_equal "OK" [run_command $fd "set key foo"]
+    }
+
+    test_interactive_cli "Integer reply" {
+        assert_equal "(integer) 1" [run_command $fd "incr counter"]
+    }
+
+    test_interactive_cli "Bulk reply" {
+        r set key foo
+        assert_equal "\"foo\"" [run_command $fd "get key"]
+    }
+
+    test_interactive_cli "Multi-bulk reply" {
+        r rpush list foo
+        r rpush list bar
+        assert_equal "1) \"foo\"\n2) \"bar\"" [run_command $fd "lrange list 0 -1"]
+    }
+
+    test_interactive_cli "Parsing quotes" {
+        assert_equal "OK" [run_command $fd "set key \"bar\""]
+        assert_equal "bar" [r get key]
+        assert_equal "OK" [run_command $fd "set key \" bar \""]
+        assert_equal " bar " [r get key]
+        assert_equal "OK" [run_command $fd "set key \"\\\"bar\\\"\""]
+        assert_equal "\"bar\"" [r get key]
+        assert_equal "OK" [run_command $fd "set key \"\tbar\t\""]
+        assert_equal "\tbar\t" [r get key]
+
+        # invalid quotation
+        assert_equal "Invalid argument(s)" [run_command $fd "get \"\"key"]
+        assert_equal "Invalid argument(s)" [run_command $fd "get \"key\"x"]
+
+        # quotes after the argument are weird, but should be allowed
+        assert_equal "OK" [run_command $fd "set key\"\" bar"]
+        assert_equal "bar" [r get key]
+    }
+
+    test_tty_cli "Status reply" {
+        assert_equal "OK" [run_cli set key bar]
+        assert_equal "bar" [r get key]
+    }
+
+    test_tty_cli "Integer reply" {
+        r del counter
+        assert_equal "(integer) 1" [run_cli incr counter]
+    }
+
+    test_tty_cli "Bulk reply" {
+        r set key "tab\tnewline\n"
+        assert_equal "\"tab\\tnewline\\n\"" [run_cli get key]
+    }
+
+    test_tty_cli "Multi-bulk reply" {
+        r del list
+        r rpush list foo
+        r rpush list bar
+        assert_equal "1) \"foo\"\n2) \"bar\"" [run_cli lrange list 0 -1]
+    }
+
+    test_tty_cli "Read last argument from pipe" {
+        assert_equal "OK" [run_cli_with_input_pipe "echo foo" set key]
+        assert_equal "foo\n" [r get key]
+    }
+
+    test_tty_cli "Read last argument from file" {
+        set tmpfile [write_tmpfile "from file"]
+        assert_equal "OK" [run_cli_with_input_file $tmpfile set key]
+        assert_equal "from file" [r get key]
+        file delete $tmpfile
+    }
+
+    test_nontty_cli "Status reply" {
+        assert_equal "OK" [run_cli set key bar]
+        assert_equal "bar" [r get key]
+    }
+
+    test_nontty_cli "Integer reply" {
+        r del counter
+        assert_equal "1" [run_cli incr counter]
+    }
+
+    test_nontty_cli "Bulk reply" {
+        r set key "tab\tnewline\n"
+        assert_equal "tab\tnewline" [run_cli get key]
+    }
+
+    test_nontty_cli "Multi-bulk reply" {
+        r del list
+        r rpush list foo
+        r rpush list bar
+        assert_equal "foo\nbar" [run_cli lrange list 0 -1]
+    }
+
+if {!$::tls} { ;# fake_redis_node doesn't support TLS
+    test_nontty_cli "ASK redirect test" {
+        # Set up two fake Redis nodes.
+        set tclsh [info nameofexecutable]
+        set script "tests/helpers/fake_redis_node.tcl"
+        set port1 [find_available_port $::baseport $::portcount]
+        set port2 [find_available_port $::baseport $::portcount]
+        set p1 [exec $tclsh $script $port1 \
+                "SET foo bar" "-ASK 12182 127.0.0.1:$port2" &]
+        set p2 [exec $tclsh $script $port2 \
+                "ASKING" "+OK" \
+                "SET foo bar" "+OK" &]
+        # Make sure both fake nodes have started listening
+        wait_for_condition 50 50 {
+            [catch {close [socket "127.0.0.1" $port1]}] == 0 && \
+            [catch {close [socket "127.0.0.1" $port2]}] == 0
+        } else {
+            fail "Failed to start fake Redis nodes"
+        }
+        # Run the cli
+        assert_equal "OK" [run_cli_host_port_db "127.0.0.1" $port1 0 -c SET foo bar]
+    }
+}
+
+    test_nontty_cli "Quoted input arguments" {
+        r set "\x00\x00" "value"
+        assert_equal "value" [run_cli --quoted-input get {"\x00\x00"}]
+    }
+
+    test_nontty_cli "No accidental unquoting of input arguments" {
+        run_cli --quoted-input set {"\x41\x41"} quoted-val
+        run_cli set {"\x41\x41"} unquoted-val
+        assert_equal "quoted-val" [r get AA]
+        assert_equal "unquoted-val" [r get {"\x41\x41"}]
+    }
+
+    test_nontty_cli "Invalid quoted input arguments" {
+        catch {run_cli --quoted-input set {"Unterminated}} err
+        assert_match {*exited abnormally*} $err
+
+        # A single arg that unquotes to two arguments is also not expected
+        catch {run_cli --quoted-input set {"arg1" "arg2"}} err
+        assert_match {*exited abnormally*} $err
+    }
+
+    test_nontty_cli "Read last argument from pipe" {
+        assert_equal "OK" [run_cli_with_input_pipe "echo foo" set key]
+        assert_equal "foo\n" [r get key]
+    }
+
+    test_nontty_cli "Read last argument from file" {
+        set tmpfile [write_tmpfile "from file"]
+        assert_equal "OK" [run_cli_with_input_file $tmpfile set key]
+        assert_equal "from file" [r get key]
+        file delete $tmpfile
+    }
+
+    proc test_redis_cli_rdb_dump {} {
+        r flushdb
+
+        set dir [lindex [r config get dir] 1]
+
+        assert_equal "OK" [r debug populate 100000 key 1000]
+        catch {run_cli --rdb "$dir/cli.rdb"} output
+        assert_match {*Transfer finished with success*} $output
+
+        file delete "$dir/dump.rdb"
+        file rename "$dir/cli.rdb" "$dir/dump.rdb"
+
+        assert_equal "OK" [r set should-not-exist 1]
+        assert_equal "OK" [r debug reload nosave]
+        assert_equal {} [r get should-not-exist]
+    }
+
+    test "Dumping an RDB" {
+        # Disk-based master
+        assert_match "OK" [r config set repl-diskless-sync no]
+        test_redis_cli_rdb_dump
+
+        # Disk-less master
+        assert_match "OK" [r config set repl-diskless-sync yes]
+        assert_match "OK" [r config set repl-diskless-sync-delay 0]
+        test_redis_cli_rdb_dump
+    } {} {needs:repl}
+
+    test "Scan mode" {
+        r flushdb
+        populate 1000 key: 1
+
+        # basic use
+        assert_equal 1000 [llength [split [run_cli --scan]]]
+
+        # pattern
+        assert_equal {key:2} [run_cli --scan --pattern "*:2"]
+
+        # pattern matching with a quoted string
+        assert_equal {key:2} [run_cli --scan --quoted-pattern {"*:\x32"}]
+    }
+
+    proc test_redis_cli_repl {} {
+        set fd [open_cli "--replica"]
+        wait_for_condition 500 100 {
+            [string match {*slave0:*state=online*} [r info]]
+        } else {
+            fail "redis-cli --replica did not connect"
+        }
+
+        for {set i 0} {$i < 100} {incr i} {
+           r set test-key test-value-$i
+        }
+
+        wait_for_condition 500 100 {
+            [string match {*test-value-99*} [read_cli $fd]]
+        } else {
+            fail "redis-cli --replica didn't read commands"
+        }
+
+        fconfigure $fd -blocking true
+        r client kill type slave
+        catch { close_cli $fd } err
+        assert_match {*Server closed the connection*} $err
+    }
+
+    test "Connecting as a replica" {
+        # Disk-based master
+        assert_match "OK" [r config set repl-diskless-sync no]
+        test_redis_cli_repl
+
+        # Disk-less master
+        assert_match "OK" [r config set repl-diskless-sync yes]
+        assert_match "OK" [r config set repl-diskless-sync-delay 0]
+        test_redis_cli_repl
+    } {} {needs:repl}
+
+    test "Piping raw protocol" {
+        set cmds [tmpfile "cli_cmds"]
+        set cmds_fd [open $cmds "w"]
+
+        set cmds_count 2101
+
+        if {!$::singledb} {
+            puts $cmds_fd [formatCommand select 9]
+            incr cmds_count
+        }
+        puts $cmds_fd [formatCommand del test-counter]
+
+        for {set i 0} {$i < 1000} {incr i} {
+            puts $cmds_fd [formatCommand incr test-counter]
+            puts $cmds_fd [formatCommand set large-key [string repeat "x" 20000]]
+        }
+
+        for {set i 0} {$i < 100} {incr i} {
+            puts $cmds_fd [formatCommand set very-large-key [string repeat "x" 512000]]
+        }
+        close $cmds_fd
+
+        set cli_fd [open_cli "--pipe" $cmds]
+        fconfigure $cli_fd -blocking true
+        set output [read_cli $cli_fd]
+
+        assert_equal {1000} [r get test-counter]
+        assert_match "*All data transferred*errors: 0*replies: ${cmds_count}*" $output
+
+        file delete $cmds
+    }
+}

+ 92 - 0
tests/integration/replication-2.tcl

@@ -0,0 +1,92 @@
+start_server {tags {"repl external:skip"}} {
+    start_server {} {
+        test {First server should have role slave after SLAVEOF} {
+            r -1 slaveof [srv 0 host] [srv 0 port]
+            wait_for_condition 50 100 {
+                [s -1 master_link_status] eq {up}
+            } else {
+                fail "Replication not started."
+            }
+        }
+
+        test {If min-slaves-to-write is honored, write is accepted} {
+            r config set min-slaves-to-write 1
+            r config set min-slaves-max-lag 10
+            r set foo 12345
+            wait_for_condition 50 100 {
+                [r -1 get foo] eq {12345}
+            } else {
+                fail "Write did not reached replica"
+            }
+        }
+
+        test {No write if min-slaves-to-write is < attached slaves} {
+            r config set min-slaves-to-write 2
+            r config set min-slaves-max-lag 10
+            catch {r set foo 12345} err
+            set err
+        } {NOREPLICAS*}
+
+        test {If min-slaves-to-write is honored, write is accepted (again)} {
+            r config set min-slaves-to-write 1
+            r config set min-slaves-max-lag 10
+            r set foo 12345
+            wait_for_condition 50 100 {
+                [r -1 get foo] eq {12345}
+            } else {
+                fail "Write did not reached replica"
+            }
+        }
+
+        test {No write if min-slaves-max-lag is > of the slave lag} {
+            r config set min-slaves-to-write 1
+            r config set min-slaves-max-lag 2
+            exec kill -SIGSTOP [srv -1 pid]
+            assert {[r set foo 12345] eq {OK}}
+            wait_for_condition 100 100 {
+                [catch {r set foo 12345}] != 0
+            } else {
+                fail "Master didn't become readonly"
+            }
+            catch {r set foo 12345} err
+            assert_match {NOREPLICAS*} $err
+        }
+        exec kill -SIGCONT [srv -1 pid]
+
+        test {min-slaves-to-write is ignored by slaves} {
+            r config set min-slaves-to-write 1
+            r config set min-slaves-max-lag 10
+            r -1 config set min-slaves-to-write 1
+            r -1 config set min-slaves-max-lag 10
+            r set foo aaabbb
+            wait_for_condition 50 100 {
+                [r -1 get foo] eq {aaabbb}
+            } else {
+                fail "Write did not reached replica"
+            }
+        }
+
+        # Fix parameters for the next test to work
+        r config set min-slaves-to-write 0
+        r -1 config set min-slaves-to-write 0
+        r flushall
+
+        test {MASTER and SLAVE dataset should be identical after complex ops} {
+            createComplexDataset r 10000
+            after 500
+            if {[r debug digest] ne [r -1 debug digest]} {
+                set csv1 [csvdump r]
+                set csv2 [csvdump {r -1}]
+                set fd [open /tmp/repldump1.txt w]
+                puts -nonewline $fd $csv1
+                close $fd
+                set fd [open /tmp/repldump2.txt w]
+                puts -nonewline $fd $csv2
+                close $fd
+                puts "Master - Replica inconsistency"
+                puts "Run diff -u against /tmp/repldump*.txt for more info"
+            }
+            assert_equal [r debug digest] [r -1 debug digest]
+        }
+    }
+}

+ 147 - 0
tests/integration/replication-3.tcl

@@ -0,0 +1,147 @@
+start_server {tags {"repl external:skip"}} {
+    start_server {} {
+        test {First server should have role slave after SLAVEOF} {
+            r -1 slaveof [srv 0 host] [srv 0 port]
+            wait_for_condition 50 100 {
+                [s -1 master_link_status] eq {up}
+            } else {
+                fail "Replication not started."
+            }
+        }
+
+        if {$::accurate} {set numops 50000} else {set numops 5000}
+
+        test {MASTER and SLAVE consistency with expire} {
+            createComplexDataset r $numops useexpire
+            after 4000 ;# Make sure everything expired before taking the digest
+            r keys *   ;# Force DEL syntesizing to slave
+            after 1000 ;# Wait another second. Now everything should be fine.
+            if {[r debug digest] ne [r -1 debug digest]} {
+                set csv1 [csvdump r]
+                set csv2 [csvdump {r -1}]
+                set fd [open /tmp/repldump1.txt w]
+                puts -nonewline $fd $csv1
+                close $fd
+                set fd [open /tmp/repldump2.txt w]
+                puts -nonewline $fd $csv2
+                close $fd
+                puts "Master - Replica inconsistency"
+                puts "Run diff -u against /tmp/repldump*.txt for more info"
+            }
+            assert_equal [r debug digest] [r -1 debug digest]
+        }
+
+        test {Master can replicate command longer than client-query-buffer-limit on replica} {
+            # Configure the master to have a bigger query buffer limit
+            r config set client-query-buffer-limit 2000000
+            r -1 config set client-query-buffer-limit 1048576
+            # Write a very large command onto the master
+            r set key [string repeat "x" 1100000]
+            wait_for_condition 300 100 {
+                [r -1 get key] eq [string repeat "x" 1100000]
+            } else {
+                fail "Unable to replicate command longer than client-query-buffer-limit"
+            }
+        }
+
+        test {Slave is able to evict keys created in writable slaves} {
+            r -1 select 5
+            assert {[r -1 dbsize] == 0}
+            r -1 config set slave-read-only no
+            r -1 set key1 1 ex 5
+            r -1 set key2 2 ex 5
+            r -1 set key3 3 ex 5
+            assert {[r -1 dbsize] == 3}
+            after 6000
+            r -1 dbsize
+        } {0}
+    }
+}
+
+start_server {tags {"repl external:skip"}} {
+    start_server {} {
+        test {First server should have role slave after SLAVEOF} {
+            r -1 slaveof [srv 0 host] [srv 0 port]
+            wait_for_condition 50 100 {
+                [s -1 master_link_status] eq {up}
+            } else {
+                fail "Replication not started."
+            }
+        }
+
+        set numops 20000 ;# Enough to trigger the Script Cache LRU eviction.
+
+        # While we are at it, enable AOF to test it will be consistent as well
+        # after the test.
+        r config set appendonly yes
+
+        test {MASTER and SLAVE consistency with EVALSHA replication} {
+            array set oldsha {}
+            for {set j 0} {$j < $numops} {incr j} {
+                set key "key:$j"
+                # Make sure to create scripts that have different SHA1s
+                set script "return redis.call('incr','$key')"
+                set sha1 [r eval "return redis.sha1hex(\"$script\")" 0]
+                set oldsha($j) $sha1
+                r eval $script 0
+                set res [r evalsha $sha1 0]
+                assert {$res == 2}
+                # Additionally call one of the old scripts as well, at random.
+                set res [r evalsha $oldsha([randomInt $j]) 0]
+                assert {$res > 2}
+
+                # Trigger an AOF rewrite while we are half-way, this also
+                # forces the flush of the script cache, and we will cover
+                # more code as a result.
+                if {$j == $numops / 2} {
+                    catch {r bgrewriteaof}
+                }
+            }
+
+            wait_for_condition 50 100 {
+                [r dbsize] == $numops &&
+                [r -1 dbsize] == $numops &&
+                [r debug digest] eq [r -1 debug digest]
+            } else {
+                set csv1 [csvdump r]
+                set csv2 [csvdump {r -1}]
+                set fd [open /tmp/repldump1.txt w]
+                puts -nonewline $fd $csv1
+                close $fd
+                set fd [open /tmp/repldump2.txt w]
+                puts -nonewline $fd $csv2
+                close $fd
+                puts "Master - Replica inconsistency"
+                puts "Run diff -u against /tmp/repldump*.txt for more info"
+            }
+
+            set old_digest [r debug digest]
+            r config set appendonly no
+            r debug loadaof
+            set new_digest [r debug digest]
+            assert {$old_digest eq $new_digest}
+        }
+
+        test {SLAVE can reload "lua" AUX RDB fields of duplicated scripts} {
+            # Force a Slave full resynchronization
+            r debug change-repl-id
+            r -1 client kill type master
+
+            # Check that after a full resync the slave can still load
+            # correctly the RDB file: such file will contain "lua" AUX
+            # sections with scripts already in the memory of the master.
+
+            wait_for_condition 1000 100 {
+                [s -1 master_link_status] eq {up}
+            } else {
+                fail "Replication not started."
+            }
+
+            wait_for_condition 50 100 {
+                [r debug digest] eq [r -1 debug digest]
+            } else {
+                fail "DEBUG DIGEST mismatch after full SYNC with many scripts"
+            }
+        }
+    }
+}

+ 143 - 0
tests/integration/replication-4.tcl

@@ -0,0 +1,143 @@
+start_server {tags {"repl network external:skip"}} {
+    start_server {} {
+
+        set master [srv -1 client]
+        set master_host [srv -1 host]
+        set master_port [srv -1 port]
+        set slave [srv 0 client]
+
+        set load_handle0 [start_bg_complex_data $master_host $master_port 9 100000]
+        set load_handle1 [start_bg_complex_data $master_host $master_port 11 100000]
+        set load_handle2 [start_bg_complex_data $master_host $master_port 12 100000]
+
+        test {First server should have role slave after SLAVEOF} {
+            $slave slaveof $master_host $master_port
+            after 1000
+            s 0 role
+        } {slave}
+
+        test {Test replication with parallel clients writing in different DBs} {
+            after 5000
+            stop_bg_complex_data $load_handle0
+            stop_bg_complex_data $load_handle1
+            stop_bg_complex_data $load_handle2
+            wait_for_condition 100 100 {
+                [$master debug digest] == [$slave debug digest]
+            } else {
+                set csv1 [csvdump r]
+                set csv2 [csvdump {r -1}]
+                set fd [open /tmp/repldump1.txt w]
+                puts -nonewline $fd $csv1
+                close $fd
+                set fd [open /tmp/repldump2.txt w]
+                puts -nonewline $fd $csv2
+                close $fd
+                fail "Master - Replica inconsistency, Run diff -u against /tmp/repldump*.txt for more info"
+            }
+            assert {[$master dbsize] > 0}
+        }
+    }
+}
+
+start_server {tags {"repl external:skip"}} {
+    start_server {} {
+        set master [srv -1 client]
+        set master_host [srv -1 host]
+        set master_port [srv -1 port]
+        set slave [srv 0 client]
+
+        test {First server should have role slave after SLAVEOF} {
+            $slave slaveof $master_host $master_port
+            wait_for_condition 50 100 {
+                [s 0 master_link_status] eq {up}
+            } else {
+                fail "Replication not started."
+            }
+        }
+
+        test {With min-slaves-to-write (1,3): master should be writable} {
+            $master config set min-slaves-max-lag 3
+            $master config set min-slaves-to-write 1
+            $master set foo bar
+        } {OK}
+
+        test {With min-slaves-to-write (2,3): master should not be writable} {
+            $master config set min-slaves-max-lag 3
+            $master config set min-slaves-to-write 2
+            catch {$master set foo bar} e
+            set e
+        } {NOREPLICAS*}
+
+        test {With min-slaves-to-write: master not writable with lagged slave} {
+            $master config set min-slaves-max-lag 2
+            $master config set min-slaves-to-write 1
+            assert {[$master set foo bar] eq {OK}}
+            exec kill -SIGSTOP [srv 0 pid]
+            wait_for_condition 100 100 {
+                [catch {$master set foo bar}] != 0
+            } else {
+                fail "Master didn't become readonly"
+            }
+            catch {$master set foo bar} err
+            assert_match {NOREPLICAS*} $err
+            exec kill -SIGCONT [srv 0 pid]
+        }
+    }
+}
+
+start_server {tags {"repl external:skip"}} {
+    start_server {} {
+        set master [srv -1 client]
+        set master_host [srv -1 host]
+        set master_port [srv -1 port]
+        set slave [srv 0 client]
+
+        test {First server should have role slave after SLAVEOF} {
+            $slave slaveof $master_host $master_port
+            wait_for_condition 50 100 {
+                [s 0 role] eq {slave}
+            } else {
+                fail "Replication not started."
+            }
+        }
+
+        test {Replication: commands with many arguments (issue #1221)} {
+            # We now issue large MSET commands, that may trigger a specific
+            # class of bugs, see issue #1221.
+            for {set j 0} {$j < 100} {incr j} {
+                set cmd [list mset]
+                for {set x 0} {$x < 1000} {incr x} {
+                    lappend cmd [randomKey] [randomValue]
+                }
+                $master {*}$cmd
+            }
+
+            set retry 10
+            while {$retry && ([$master debug digest] ne [$slave debug digest])}\
+            {
+                after 1000
+                incr retry -1
+            }
+            assert {[$master dbsize] > 0}
+        }
+
+        test {Replication of SPOP command -- alsoPropagate() API} {
+            $master del myset
+            set size [expr 1+[randomInt 100]]
+            set content {}
+            for {set j 0} {$j < $size} {incr j} {
+                lappend content [randomValue]
+            }
+            $master sadd myset {*}$content
+
+            set count [randomInt 100]
+            set result [$master spop myset $count]
+
+            wait_for_condition 50 100 {
+                [$master debug digest] eq [$slave debug digest]
+            } else {
+                fail "SPOP replication inconsistency"
+            }
+        }
+    }
+}

+ 143 - 0
tests/integration/replication-psync.tcl

@@ -0,0 +1,143 @@
+# Creates a master-slave pair and breaks the link continuously to force
+# partial resyncs attempts, all this while flooding the master with
+# write queries.
+#
+# You can specify backlog size, ttl, delay before reconnection, test duration
+# in seconds, and an additional condition to verify at the end.
+#
+# If reconnect is > 0, the test actually try to break the connection and
+# reconnect with the master, otherwise just the initial synchronization is
+# checked for consistency.
+proc test_psync {descr duration backlog_size backlog_ttl delay cond mdl sdl reconnect} {
+    start_server {tags {"repl"}} {
+        start_server {} {
+
+            set master [srv -1 client]
+            set master_host [srv -1 host]
+            set master_port [srv -1 port]
+            set slave [srv 0 client]
+
+            $master config set repl-backlog-size $backlog_size
+            $master config set repl-backlog-ttl $backlog_ttl
+            $master config set repl-diskless-sync $mdl
+            $master config set repl-diskless-sync-delay 1
+            $slave config set repl-diskless-load $sdl
+
+            set load_handle0 [start_bg_complex_data $master_host $master_port 9 100000]
+            set load_handle1 [start_bg_complex_data $master_host $master_port 11 100000]
+            set load_handle2 [start_bg_complex_data $master_host $master_port 12 100000]
+
+            test {Slave should be able to synchronize with the master} {
+                $slave slaveof $master_host $master_port
+                wait_for_condition 50 100 {
+                    [lindex [r role] 0] eq {slave} &&
+                    [lindex [r role] 3] eq {connected}
+                } else {
+                    fail "Replication not started."
+                }
+            }
+
+            # Check that the background clients are actually writing.
+            test {Detect write load to master} {
+                wait_for_condition 50 1000 {
+                    [$master dbsize] > 100
+                } else {
+                    fail "Can't detect write load from background clients."
+                }
+            }
+
+            test "Test replication partial resync: $descr (diskless: $mdl, $sdl, reconnect: $reconnect)" {
+                # Now while the clients are writing data, break the maste-slave
+                # link multiple times.
+                if ($reconnect) {
+                    for {set j 0} {$j < $duration*10} {incr j} {
+                        after 100
+                        # catch {puts "MASTER [$master dbsize] keys, REPLICA [$slave dbsize] keys"}
+
+                        if {($j % 20) == 0} {
+                            catch {
+                                if {$delay} {
+                                    $slave multi
+                                    $slave client kill $master_host:$master_port
+                                    $slave debug sleep $delay
+                                    $slave exec
+                                } else {
+                                    $slave client kill $master_host:$master_port
+                                }
+                            }
+                        }
+                    }
+                }
+                stop_bg_complex_data $load_handle0
+                stop_bg_complex_data $load_handle1
+                stop_bg_complex_data $load_handle2
+
+                # Wait for the slave to reach the "online"
+                # state from the POV of the master.
+                set retry 5000
+                while {$retry} {
+                    set info [$master info]
+                    if {[string match {*slave0:*state=online*} $info]} {
+                        break
+                    } else {
+                        incr retry -1
+                        after 100
+                    }
+                }
+                if {$retry == 0} {
+                    error "assertion:Slave not correctly synchronized"
+                }
+
+                # Wait that slave acknowledge it is online so
+                # we are sure that DBSIZE and DEBUG DIGEST will not
+                # fail because of timing issues. (-LOADING error)
+                wait_for_condition 5000 100 {
+                    [lindex [$slave role] 3] eq {connected}
+                } else {
+                    fail "Slave still not connected after some time"
+                }  
+
+                wait_for_condition 100 100 {
+                    [$master debug digest] == [$slave debug digest]
+                } else {
+                    set csv1 [csvdump r]
+                    set csv2 [csvdump {r -1}]
+                    set fd [open /tmp/repldump1.txt w]
+                    puts -nonewline $fd $csv1
+                    close $fd
+                    set fd [open /tmp/repldump2.txt w]
+                    puts -nonewline $fd $csv2
+                    close $fd
+                    fail "Master - Replica inconsistency, Run diff -u against /tmp/repldump*.txt for more info"
+                }
+                assert {[$master dbsize] > 0}
+                eval $cond
+            }
+        }
+    }
+}
+
+tags {"external:skip"} {
+foreach mdl {no yes} {
+    foreach sdl {disabled swapdb} {
+        test_psync {no reconnection, just sync} 6 1000000 3600 0 {
+        } $mdl $sdl 0
+
+        test_psync {ok psync} 6 100000000 3600 0 {
+        assert {[s -1 sync_partial_ok] > 0}
+        } $mdl $sdl 1
+
+        test_psync {no backlog} 6 100 3600 0.5 {
+        assert {[s -1 sync_partial_err] > 0}
+        } $mdl $sdl 1
+
+        test_psync {ok after delay} 3 100000000 3600 3 {
+        assert {[s -1 sync_partial_ok] > 0}
+        } $mdl $sdl 1
+
+        test_psync {backlog expired} 3 100000000 1 3 {
+        assert {[s -1 sync_partial_err] > 0}
+        } $mdl $sdl 1
+    }
+}
+}

+ 928 - 0
tests/integration/replication.tcl

@@ -0,0 +1,928 @@
+proc log_file_matches {log pattern} {
+    set fp [open $log r]
+    set content [read $fp]
+    close $fp
+    string match $pattern $content
+}
+
+start_server {tags {"repl network external:skip"}} {
+    set slave [srv 0 client]
+    set slave_host [srv 0 host]
+    set slave_port [srv 0 port]
+    set slave_log [srv 0 stdout]
+    start_server {} {
+        set master [srv 0 client]
+        set master_host [srv 0 host]
+        set master_port [srv 0 port]
+
+        # Configure the master in order to hang waiting for the BGSAVE
+        # operation, so that the slave remains in the handshake state.
+        $master config set repl-diskless-sync yes
+        $master config set repl-diskless-sync-delay 1000
+
+        # Use a short replication timeout on the slave, so that if there
+        # are no bugs the timeout is triggered in a reasonable amount
+        # of time.
+        $slave config set repl-timeout 5
+
+        # Start the replication process...
+        $slave slaveof $master_host $master_port
+
+        test {Slave enters handshake} {
+            wait_for_condition 50 1000 {
+                [string match *handshake* [$slave role]]
+            } else {
+                fail "Replica does not enter handshake state"
+            }
+        }
+
+        # But make the master unable to send
+        # the periodic newlines to refresh the connection. The slave
+        # should detect the timeout.
+        $master debug sleep 10
+
+        test {Slave is able to detect timeout during handshake} {
+            wait_for_condition 50 1000 {
+                [log_file_matches $slave_log "*Timeout connecting to the MASTER*"]
+            } else {
+                fail "Replica is not able to detect timeout"
+            }
+        }
+    }
+}
+
+start_server {tags {"repl external:skip"}} {
+    set A [srv 0 client]
+    set A_host [srv 0 host]
+    set A_port [srv 0 port]
+    start_server {} {
+        set B [srv 0 client]
+        set B_host [srv 0 host]
+        set B_port [srv 0 port]
+
+        test {Set instance A as slave of B} {
+            $A slaveof $B_host $B_port
+            wait_for_condition 50 100 {
+                [lindex [$A role] 0] eq {slave} &&
+                [string match {*master_link_status:up*} [$A info replication]]
+            } else {
+                fail "Can't turn the instance into a replica"
+            }
+        }
+
+        test {INCRBYFLOAT replication, should not remove expire} {
+            r set test 1 EX 100
+            r incrbyfloat test 0.1
+            after 1000
+            assert_equal [$A debug digest] [$B debug digest]
+        }
+
+        test {GETSET replication} {
+            $A config resetstat
+            $A config set loglevel debug
+            $B config set loglevel debug
+            r set test foo
+            assert_equal [r getset test bar] foo
+            wait_for_condition 500 10 {
+                [$A get test] eq "bar"
+            } else {
+                fail "getset wasn't propagated"
+            }
+            assert_equal [r set test vaz get] bar
+            wait_for_condition 500 10 {
+                [$A get test] eq "vaz"
+            } else {
+                fail "set get wasn't propagated"
+            }
+            assert_match {*calls=3,*} [cmdrstat set $A]
+            assert_match {} [cmdrstat getset $A]
+        }
+
+        test {BRPOPLPUSH replication, when blocking against empty list} {
+            $A config resetstat
+            set rd [redis_deferring_client]
+            $rd brpoplpush a b 5
+            r lpush a foo
+            wait_for_condition 50 100 {
+                [$A debug digest] eq [$B debug digest]
+            } else {
+                fail "Master and replica have different digest: [$A debug digest] VS [$B debug digest]"
+            }
+            assert_match {*calls=1,*} [cmdrstat rpoplpush $A]
+            assert_match {} [cmdrstat lmove $A]
+        }
+
+        test {BRPOPLPUSH replication, list exists} {
+            $A config resetstat
+            set rd [redis_deferring_client]
+            r lpush c 1
+            r lpush c 2
+            r lpush c 3
+            $rd brpoplpush c d 5
+            after 1000
+            assert_equal [$A debug digest] [$B debug digest]
+            assert_match {*calls=1,*} [cmdrstat rpoplpush $A]
+            assert_match {} [cmdrstat lmove $A]
+        }
+
+        foreach wherefrom {left right} {
+            foreach whereto {left right} {
+                test "BLMOVE ($wherefrom, $whereto) replication, when blocking against empty list" {
+                    $A config resetstat
+                    set rd [redis_deferring_client]
+                    $rd blmove a b $wherefrom $whereto 5
+                    r lpush a foo
+                    wait_for_condition 50 100 {
+                        [$A debug digest] eq [$B debug digest]
+                    } else {
+                        fail "Master and replica have different digest: [$A debug digest] VS [$B debug digest]"
+                    }
+                    assert_match {*calls=1,*} [cmdrstat lmove $A]
+                    assert_match {} [cmdrstat rpoplpush $A]
+                }
+
+                test "BLMOVE ($wherefrom, $whereto) replication, list exists" {
+                    $A config resetstat
+                    set rd [redis_deferring_client]
+                    r lpush c 1
+                    r lpush c 2
+                    r lpush c 3
+                    $rd blmove c d $wherefrom $whereto 5
+                    after 1000
+                    assert_equal [$A debug digest] [$B debug digest]
+                    assert_match {*calls=1,*} [cmdrstat lmove $A]
+                    assert_match {} [cmdrstat rpoplpush $A]
+                }
+            }
+        }
+
+        test {BLPOP followed by role change, issue #2473} {
+            set rd [redis_deferring_client]
+            $rd blpop foo 0 ; # Block while B is a master
+
+            # Turn B into master of A
+            $A slaveof no one
+            $B slaveof $A_host $A_port
+            wait_for_condition 50 100 {
+                [lindex [$B role] 0] eq {slave} &&
+                [string match {*master_link_status:up*} [$B info replication]]
+            } else {
+                fail "Can't turn the instance into a replica"
+            }
+
+            # Push elements into the "foo" list of the new replica.
+            # If the client is still attached to the instance, we'll get
+            # a desync between the two instances.
+            $A rpush foo a b c
+            after 100
+
+            wait_for_condition 50 100 {
+                [$A debug digest] eq [$B debug digest] &&
+                [$A lrange foo 0 -1] eq {a b c} &&
+                [$B lrange foo 0 -1] eq {a b c}
+            } else {
+                fail "Master and replica have different digest: [$A debug digest] VS [$B debug digest]"
+            }
+        }
+    }
+}
+
+start_server {tags {"repl external:skip"}} {
+    r set mykey foo
+
+    start_server {} {
+        test {Second server should have role master at first} {
+            s role
+        } {master}
+
+        test {SLAVEOF should start with link status "down"} {
+            r multi
+            r slaveof [srv -1 host] [srv -1 port]
+            r info replication
+            r exec
+        } {*master_link_status:down*}
+
+        test {The role should immediately be changed to "replica"} {
+            s role
+        } {slave}
+
+        wait_for_sync r
+        test {Sync should have transferred keys from master} {
+            r get mykey
+        } {foo}
+
+        test {The link status should be up} {
+            s master_link_status
+        } {up}
+
+        test {SET on the master should immediately propagate} {
+            r -1 set mykey bar
+
+            wait_for_condition 500 100 {
+                [r  0 get mykey] eq {bar}
+            } else {
+                fail "SET on master did not propagated on replica"
+            }
+        }
+
+        test {FLUSHALL should replicate} {
+            r -1 flushall
+            if {$::valgrind} {after 2000}
+            list [r -1 dbsize] [r 0 dbsize]
+        } {0 0}
+
+        test {ROLE in master reports master with a slave} {
+            set res [r -1 role]
+            lassign $res role offset slaves
+            assert {$role eq {master}}
+            assert {$offset > 0}
+            assert {[llength $slaves] == 1}
+            lassign [lindex $slaves 0] master_host master_port slave_offset
+            assert {$slave_offset <= $offset}
+        }
+
+        test {ROLE in slave reports slave in connected state} {
+            set res [r role]
+            lassign $res role master_host master_port slave_state slave_offset
+            assert {$role eq {slave}}
+            assert {$slave_state eq {connected}}
+        }
+    }
+}
+
+foreach mdl {no yes} {
+    foreach sdl {disabled swapdb} {
+        start_server {tags {"repl external:skip"}} {
+            set master [srv 0 client]
+            $master config set repl-diskless-sync $mdl
+            $master config set repl-diskless-sync-delay 1
+            set master_host [srv 0 host]
+            set master_port [srv 0 port]
+            set slaves {}
+            start_server {} {
+                lappend slaves [srv 0 client]
+                start_server {} {
+                    lappend slaves [srv 0 client]
+                    start_server {} {
+                        lappend slaves [srv 0 client]
+                        test "Connect multiple replicas at the same time (issue #141), master diskless=$mdl, replica diskless=$sdl" {
+                            # start load handles only inside the test, so that the test can be skipped
+                            set load_handle0 [start_bg_complex_data $master_host $master_port 9 100000000]
+                            set load_handle1 [start_bg_complex_data $master_host $master_port 11 100000000]
+                            set load_handle2 [start_bg_complex_data $master_host $master_port 12 100000000]
+                            set load_handle3 [start_write_load $master_host $master_port 8]
+                            set load_handle4 [start_write_load $master_host $master_port 4]
+                            after 5000 ;# wait for some data to accumulate so that we have RDB part for the fork
+
+                            # Send SLAVEOF commands to slaves
+                            [lindex $slaves 0] config set repl-diskless-load $sdl
+                            [lindex $slaves 1] config set repl-diskless-load $sdl
+                            [lindex $slaves 2] config set repl-diskless-load $sdl
+                            [lindex $slaves 0] slaveof $master_host $master_port
+                            [lindex $slaves 1] slaveof $master_host $master_port
+                            [lindex $slaves 2] slaveof $master_host $master_port
+
+                            # Wait for all the three slaves to reach the "online"
+                            # state from the POV of the master.
+                            set retry 500
+                            while {$retry} {
+                                set info [r -3 info]
+                                if {[string match {*slave0:*state=online*slave1:*state=online*slave2:*state=online*} $info]} {
+                                    break
+                                } else {
+                                    incr retry -1
+                                    after 100
+                                }
+                            }
+                            if {$retry == 0} {
+                                error "assertion:Slaves not correctly synchronized"
+                            }
+
+                            # Wait that slaves acknowledge they are online so
+                            # we are sure that DBSIZE and DEBUG DIGEST will not
+                            # fail because of timing issues.
+                            wait_for_condition 500 100 {
+                                [lindex [[lindex $slaves 0] role] 3] eq {connected} &&
+                                [lindex [[lindex $slaves 1] role] 3] eq {connected} &&
+                                [lindex [[lindex $slaves 2] role] 3] eq {connected}
+                            } else {
+                                fail "Slaves still not connected after some time"
+                            }
+
+                            # Stop the write load
+                            stop_bg_complex_data $load_handle0
+                            stop_bg_complex_data $load_handle1
+                            stop_bg_complex_data $load_handle2
+                            stop_write_load $load_handle3
+                            stop_write_load $load_handle4
+
+                            # Make sure no more commands processed
+                            wait_load_handlers_disconnected
+
+                            wait_for_ofs_sync $master [lindex $slaves 0]
+                            wait_for_ofs_sync $master [lindex $slaves 1]
+                            wait_for_ofs_sync $master [lindex $slaves 2]
+
+                            # Check digests
+                            set digest [$master debug digest]
+                            set digest0 [[lindex $slaves 0] debug digest]
+                            set digest1 [[lindex $slaves 1] debug digest]
+                            set digest2 [[lindex $slaves 2] debug digest]
+                            assert {$digest ne 0000000000000000000000000000000000000000}
+                            assert {$digest eq $digest0}
+                            assert {$digest eq $digest1}
+                            assert {$digest eq $digest2}
+                        }
+                   }
+                }
+            }
+        }
+    }
+}
+
+start_server {tags {"repl external:skip"}} {
+    set master [srv 0 client]
+    set master_host [srv 0 host]
+    set master_port [srv 0 port]
+    start_server {} {
+        test "Master stream is correctly processed while the replica has a script in -BUSY state" {
+            set load_handle0 [start_write_load $master_host $master_port 3]
+            set slave [srv 0 client]
+            $slave config set lua-time-limit 500
+            $slave slaveof $master_host $master_port
+
+            # Wait for the slave to be online
+            wait_for_condition 500 100 {
+                [lindex [$slave role] 3] eq {connected}
+            } else {
+                fail "Replica still not connected after some time"
+            }
+
+            # Wait some time to make sure the master is sending data
+            # to the slave.
+            after 5000
+
+            # Stop the ability of the slave to process data by sendig
+            # a script that will put it in BUSY state.
+            $slave eval {for i=1,3000000000 do end} 0
+
+            # Wait some time again so that more master stream will
+            # be processed.
+            after 2000
+
+            # Stop the write load
+            stop_write_load $load_handle0
+
+            # number of keys
+            wait_for_condition 500 100 {
+                [$master debug digest] eq [$slave debug digest]
+            } else {
+                fail "Different datasets between replica and master"
+            }
+        }
+    }
+}
+
+test {slave fails full sync and diskless load swapdb recovers it} {
+    start_server {tags {"repl"}} {
+        set slave [srv 0 client]
+        set slave_host [srv 0 host]
+        set slave_port [srv 0 port]
+        set slave_log [srv 0 stdout]
+        start_server {} {
+            set master [srv 0 client]
+            set master_host [srv 0 host]
+            set master_port [srv 0 port]
+
+            # Put different data sets on the master and slave
+            # we need to put large keys on the master since the slave replies to info only once in 2mb
+            $slave debug populate 2000 slave 10
+            $master debug populate 800 master 100000
+            $master config set rdbcompression no
+
+            # Set master and slave to use diskless replication
+            $master config set repl-diskless-sync yes
+            $master config set repl-diskless-sync-delay 0
+            $slave config set repl-diskless-load swapdb
+
+            # Set master with a slow rdb generation, so that we can easily disconnect it mid sync
+            # 10ms per key, with 800 keys is 8 seconds
+            $master config set rdb-key-save-delay 10000
+
+            # Start the replication process...
+            $slave slaveof $master_host $master_port
+
+            # wait for the slave to start reading the rdb
+            wait_for_condition 50 100 {
+                [s -1 loading] eq 1
+            } else {
+                fail "Replica didn't get into loading mode"
+            }
+
+            # make sure that next sync will not start immediately so that we can catch the slave in between syncs
+            $master config set repl-diskless-sync-delay 5
+            # for faster server shutdown, make rdb saving fast again (the fork is already uses the slow one)
+            $master config set rdb-key-save-delay 0
+
+            # waiting slave to do flushdb (key count drop)
+            wait_for_condition 50 100 {
+                2000 != [scan [regexp -inline {keys\=([\d]*)} [$slave info keyspace]] keys=%d]
+            } else {
+                fail "Replica didn't flush"
+            }
+
+            # make sure we're still loading
+            assert_equal [s -1 loading] 1
+
+            # kill the slave connection on the master
+            set killed [$master client kill type slave]
+
+            # wait for loading to stop (fail)
+            wait_for_condition 50 100 {
+                [s -1 loading] eq 0
+            } else {
+                fail "Replica didn't disconnect"
+            }
+
+            # make sure the original keys were restored
+            assert_equal [$slave dbsize] 2000
+        }
+    }
+} {} {external:skip}
+
+test {diskless loading short read} {
+    start_server {tags {"repl"}} {
+        set replica [srv 0 client]
+        set replica_host [srv 0 host]
+        set replica_port [srv 0 port]
+        start_server {} {
+            set master [srv 0 client]
+            set master_host [srv 0 host]
+            set master_port [srv 0 port]
+
+            # Set master and replica to use diskless replication
+            $master config set repl-diskless-sync yes
+            $master config set rdbcompression no
+            $replica config set repl-diskless-load swapdb
+            $master config set hz 500
+            $replica config set hz 500
+            $master config set dynamic-hz no
+            $replica config set dynamic-hz no
+            # Try to fill the master with all types of data types / encodings
+            set start [clock clicks -milliseconds]
+            for {set k 0} {$k < 3} {incr k} {
+                for {set i 0} {$i < 10} {incr i} {
+                    r set "$k int_$i" [expr {int(rand()*10000)}]
+                    r expire "$k int_$i" [expr {int(rand()*10000)}]
+                    r set "$k string_$i" [string repeat A [expr {int(rand()*1000000)}]]
+                    r hset "$k hash_small" [string repeat A [expr {int(rand()*10)}]]  0[string repeat A [expr {int(rand()*10)}]]
+                    r hset "$k hash_large" [string repeat A [expr {int(rand()*10000)}]] [string repeat A [expr {int(rand()*1000000)}]]
+                    r sadd "$k set_small" [string repeat A [expr {int(rand()*10)}]]
+                    r sadd "$k set_large" [string repeat A [expr {int(rand()*1000000)}]]
+                    r zadd "$k zset_small" [expr {rand()}] [string repeat A [expr {int(rand()*10)}]]
+                    r zadd "$k zset_large" [expr {rand()}] [string repeat A [expr {int(rand()*1000000)}]]
+                    r lpush "$k list_small" [string repeat A [expr {int(rand()*10)}]]
+                    r lpush "$k list_large" [string repeat A [expr {int(rand()*1000000)}]]
+                    for {set j 0} {$j < 10} {incr j} {
+                        r xadd "$k stream" * foo "asdf" bar "1234"
+                    }
+                    r xgroup create "$k stream" "mygroup_$i" 0
+                    r xreadgroup GROUP "mygroup_$i" Alice COUNT 1 STREAMS "$k stream" >
+                }
+            }
+
+            if {$::verbose} {
+                set end [clock clicks -milliseconds]
+                set duration [expr $end - $start]
+                puts "filling took $duration ms (TODO: use pipeline)"
+                set start [clock clicks -milliseconds]
+            }
+
+            # Start the replication process...
+            set loglines [count_log_lines -1]
+            $master config set repl-diskless-sync-delay 0
+            $replica replicaof $master_host $master_port
+
+            # kill the replication at various points
+            set attempts 100
+            if {$::accurate} { set attempts 500 }
+            for {set i 0} {$i < $attempts} {incr i} {
+                # wait for the replica to start reading the rdb
+                # using the log file since the replica only responds to INFO once in 2mb
+                set res [wait_for_log_messages -1 {"*Loading DB in memory*"} $loglines 2000 1]
+                set loglines [lindex $res 1]
+
+                # add some additional random sleep so that we kill the master on a different place each time
+                after [expr {int(rand()*50)}]
+
+                # kill the replica connection on the master
+                set killed [$master client kill type replica]
+
+                set res [wait_for_log_messages -1 {"*Internal error in RDB*" "*Finished with success*" "*Successful partial resynchronization*"} $loglines 1000 1]
+                if {$::verbose} { puts $res }
+                set log_text [lindex $res 0]
+                set loglines [lindex $res 1]
+                if {![string match "*Internal error in RDB*" $log_text]} {
+                    # force the replica to try another full sync
+                    $master multi
+                    $master client kill type replica
+                    $master set asdf asdf
+                    # the side effect of resizing the backlog is that it is flushed (16k is the min size)
+                    $master config set repl-backlog-size [expr {16384 + $i}]
+                    $master exec
+                }
+                # wait for loading to stop (fail)
+                wait_for_condition 1000 1 {
+                    [s -1 loading] eq 0
+                } else {
+                    fail "Replica didn't disconnect"
+                }
+            }
+            if {$::verbose} {
+                set end [clock clicks -milliseconds]
+                set duration [expr $end - $start]
+                puts "test took $duration ms"
+            }
+            # enable fast shutdown
+            $master config set rdb-key-save-delay 0
+        }
+    }
+} {} {external:skip}
+
+# get current stime and utime metrics for a thread (since it's creation)
+proc get_cpu_metrics { statfile } {
+    if { [ catch {
+        set fid   [ open $statfile r ]
+        set data  [ read $fid 1024 ]
+        ::close $fid
+        set data  [ split $data ]
+
+        ;## number of jiffies it has been scheduled...
+        set utime [ lindex $data 13 ]
+        set stime [ lindex $data 14 ]
+    } err ] } {
+        error "assertion:can't parse /proc: $err"
+    }
+    set mstime [clock milliseconds]
+    return [ list $mstime $utime $stime ]
+}
+
+# compute %utime and %stime of a thread between two measurements
+proc compute_cpu_usage {start end} {
+    set clock_ticks [exec getconf CLK_TCK]
+    # convert ms time to jiffies and calc delta
+    set dtime [ expr { ([lindex $end 0] - [lindex $start 0]) * double($clock_ticks) / 1000 } ]
+    set utime [ expr { [lindex $end 1] - [lindex $start 1] } ]
+    set stime [ expr { [lindex $end 2] - [lindex $start 2] } ]
+    set pucpu  [ expr { ($utime / $dtime) * 100 } ]
+    set pscpu  [ expr { ($stime / $dtime) * 100 } ]
+    return [ list $pucpu $pscpu ]
+}
+
+
+# test diskless rdb pipe with multiple replicas, which may drop half way
+start_server {tags {"repl external:skip"}} {
+    set master [srv 0 client]
+    $master config set repl-diskless-sync yes
+    $master config set repl-diskless-sync-delay 1
+    set master_host [srv 0 host]
+    set master_port [srv 0 port]
+    set master_pid [srv 0 pid]
+    # put enough data in the db that the rdb file will be bigger than the socket buffers
+    # and since we'll have key-load-delay of 100, 20000 keys will take at least 2 seconds
+    # we also need the replica to process requests during transfer (which it does only once in 2mb)
+    $master debug populate 20000 test 10000
+    $master config set rdbcompression no
+    # If running on Linux, we also measure utime/stime to detect possible I/O handling issues
+    set os [catch {exec uname}]
+    set measure_time [expr {$os == "Linux"} ? 1 : 0]
+    foreach all_drop {no slow fast all timeout} {
+        test "diskless $all_drop replicas drop during rdb pipe" {
+            set replicas {}
+            set replicas_alive {}
+            # start one replica that will read the rdb fast, and one that will be slow
+            start_server {} {
+                lappend replicas [srv 0 client]
+                lappend replicas_alive [srv 0 client]
+                start_server {} {
+                    lappend replicas [srv 0 client]
+                    lappend replicas_alive [srv 0 client]
+
+                    # start replication
+                    # it's enough for just one replica to be slow, and have it's write handler enabled
+                    # so that the whole rdb generation process is bound to that
+                    set loglines [count_log_lines -1]
+                    [lindex $replicas 0] config set repl-diskless-load swapdb
+                    [lindex $replicas 0] config set key-load-delay 100 ;# 20k keys and 100 microseconds sleep means at least 2 seconds
+                    [lindex $replicas 0] replicaof $master_host $master_port
+                    [lindex $replicas 1] replicaof $master_host $master_port
+
+                    # wait for the replicas to start reading the rdb
+                    # using the log file since the replica only responds to INFO once in 2mb
+                    wait_for_log_messages -1 {"*Loading DB in memory*"} $loglines 800 10
+
+                    if {$measure_time} {
+                        set master_statfile "/proc/$master_pid/stat"
+                        set master_start_metrics [get_cpu_metrics $master_statfile]
+                        set start_time [clock seconds]
+                    }
+
+                    # wait a while so that the pipe socket writer will be
+                    # blocked on write (since replica 0 is slow to read from the socket)
+                    after 500
+
+                    # add some command to be present in the command stream after the rdb.
+                    $master incr $all_drop
+
+                    # disconnect replicas depending on the current test
+                    set loglines [count_log_lines -2]
+                    if {$all_drop == "all" || $all_drop == "fast"} {
+                        exec kill [srv 0 pid]
+                        set replicas_alive [lreplace $replicas_alive 1 1]
+                    }
+                    if {$all_drop == "all" || $all_drop == "slow"} {
+                        exec kill [srv -1 pid]
+                        set replicas_alive [lreplace $replicas_alive 0 0]
+                    }
+                    if {$all_drop == "timeout"} {
+                        $master config set repl-timeout 2
+                        # we want the slow replica to hang on a key for very long so it'll reach repl-timeout
+                        exec kill -SIGSTOP [srv -1 pid]
+                        after 2000
+                    }
+
+                    # wait for rdb child to exit
+                    wait_for_condition 500 100 {
+                        [s -2 rdb_bgsave_in_progress] == 0
+                    } else {
+                        fail "rdb child didn't terminate"
+                    }
+
+                    # make sure we got what we were aiming for, by looking for the message in the log file
+                    if {$all_drop == "all"} {
+                        wait_for_log_messages -2 {"*Diskless rdb transfer, last replica dropped, killing fork child*"} $loglines 1 1
+                    }
+                    if {$all_drop == "no"} {
+                        wait_for_log_messages -2 {"*Diskless rdb transfer, done reading from pipe, 2 replicas still up*"} $loglines 1 1
+                    }
+                    if {$all_drop == "slow" || $all_drop == "fast"} {
+                        wait_for_log_messages -2 {"*Diskless rdb transfer, done reading from pipe, 1 replicas still up*"} $loglines 1 1
+                    }
+                    if {$all_drop == "timeout"} {
+                        wait_for_log_messages -2 {"*Disconnecting timedout replica (full sync)*"} $loglines 1 1
+                        wait_for_log_messages -2 {"*Diskless rdb transfer, done reading from pipe, 1 replicas still up*"} $loglines 1 1
+                        # master disconnected the slow replica, remove from array
+                        set replicas_alive [lreplace $replicas_alive 0 0]
+                        # release it
+                        exec kill -SIGCONT [srv -1 pid]
+                    }
+
+                    # make sure we don't have a busy loop going thought epoll_wait
+                    if {$measure_time} {
+                        set master_end_metrics [get_cpu_metrics $master_statfile]
+                        set time_elapsed [expr {[clock seconds]-$start_time}]
+                        set master_cpu [compute_cpu_usage $master_start_metrics $master_end_metrics]
+                        set master_utime [lindex $master_cpu 0]
+                        set master_stime [lindex $master_cpu 1]
+                        if {$::verbose} {
+                            puts "elapsed: $time_elapsed"
+                            puts "master utime: $master_utime"
+                            puts "master stime: $master_stime"
+                        }
+                        if {!$::no_latency && ($all_drop == "all" || $all_drop == "slow" || $all_drop == "timeout")} {
+                            assert {$master_utime < 70}
+                            assert {$master_stime < 70}
+                        }
+                        if {!$::no_latency && ($all_drop == "none" || $all_drop == "fast")} {
+                            assert {$master_utime < 15}
+                            assert {$master_stime < 15}
+                        }
+                    }
+
+                    # verify the data integrity
+                    foreach replica $replicas_alive {
+                        # Wait that replicas acknowledge they are online so
+                        # we are sure that DBSIZE and DEBUG DIGEST will not
+                        # fail because of timing issues.
+                        wait_for_condition 150 100 {
+                            [lindex [$replica role] 3] eq {connected}
+                        } else {
+                            fail "replicas still not connected after some time"
+                        }
+
+                        # Make sure that replicas and master have same
+                        # number of keys
+                        wait_for_condition 50 100 {
+                            [$master dbsize] == [$replica dbsize]
+                        } else {
+                            fail "Different number of keys between master and replicas after too long time."
+                        }
+
+                        # Check digests
+                        set digest [$master debug digest]
+                        set digest0 [$replica debug digest]
+                        assert {$digest ne 0000000000000000000000000000000000000000}
+                        assert {$digest eq $digest0}
+                    }
+                }
+            }
+        }
+    }
+}
+
+test "diskless replication child being killed is collected" {
+    # when diskless master is waiting for the replica to become writable
+    # it removes the read event from the rdb pipe so if the child gets killed
+    # the replica will hung. and the master may not collect the pid with waitpid
+    start_server {tags {"repl"}} {
+        set master [srv 0 client]
+        set master_host [srv 0 host]
+        set master_port [srv 0 port]
+        set master_pid [srv 0 pid]
+        $master config set repl-diskless-sync yes
+        $master config set repl-diskless-sync-delay 0
+        # put enough data in the db that the rdb file will be bigger than the socket buffers
+        $master debug populate 20000 test 10000
+        $master config set rdbcompression no
+        start_server {} {
+            set replica [srv 0 client]
+            set loglines [count_log_lines 0]
+            $replica config set repl-diskless-load swapdb
+            $replica config set key-load-delay 1000000
+            $replica replicaof $master_host $master_port
+
+            # wait for the replicas to start reading the rdb
+            wait_for_log_messages 0 {"*Loading DB in memory*"} $loglines 800 10
+
+            # wait to be sure the eplica is hung and the master is blocked on write
+            after 500
+
+            # simulate the OOM killer or anyone else kills the child
+            set fork_child_pid [get_child_pid -1]
+            exec kill -9 $fork_child_pid
+
+            # wait for the parent to notice the child have exited
+            wait_for_condition 50 100 {
+                [s -1 rdb_bgsave_in_progress] == 0
+            } else {
+                fail "rdb child didn't terminate"
+            }
+        }
+    }
+} {} {external:skip}
+
+test "diskless replication read pipe cleanup" {
+    # In diskless replication, we create a read pipe for the RDB, between the child and the parent.
+    # When we close this pipe (fd), the read handler also needs to be removed from the event loop (if it still registered).
+    # Otherwise, next time we will use the same fd, the registration will be fail (panic), because
+    # we will use EPOLL_CTL_MOD (the fd still register in the event loop), on fd that already removed from epoll_ctl
+    start_server {tags {"repl"}} {
+        set master [srv 0 client]
+        set master_host [srv 0 host]
+        set master_port [srv 0 port]
+        set master_pid [srv 0 pid]
+        $master config set repl-diskless-sync yes
+        $master config set repl-diskless-sync-delay 0
+
+        # put enough data in the db, and slowdown the save, to keep the parent busy at the read process
+        $master config set rdb-key-save-delay 100000
+        $master debug populate 20000 test 10000
+        $master config set rdbcompression no
+        start_server {} {
+            set replica [srv 0 client]
+            set loglines [count_log_lines 0]
+            $replica config set repl-diskless-load swapdb
+            $replica replicaof $master_host $master_port
+
+            # wait for the replicas to start reading the rdb
+            wait_for_log_messages 0 {"*Loading DB in memory*"} $loglines 800 10
+
+            set loglines [count_log_lines 0]
+            # send FLUSHALL so the RDB child will be killed
+            $master flushall
+
+            # wait for another RDB child process to be started
+            wait_for_log_messages -1 {"*Background RDB transfer started by pid*"} $loglines 800 10
+
+            # make sure master is alive
+            $master ping
+        }
+    }
+} {} {external:skip}
+
+test {replicaof right after disconnection} {
+    # this is a rare race condition that was reproduced sporadically by the psync2 unit.
+    # see details in #7205
+    start_server {tags {"repl"}} {
+        set replica1 [srv 0 client]
+        set replica1_host [srv 0 host]
+        set replica1_port [srv 0 port]
+        set replica1_log [srv 0 stdout]
+        start_server {} {
+            set replica2 [srv 0 client]
+            set replica2_host [srv 0 host]
+            set replica2_port [srv 0 port]
+            set replica2_log [srv 0 stdout]
+            start_server {} {
+                set master [srv 0 client]
+                set master_host [srv 0 host]
+                set master_port [srv 0 port]
+                $replica1 replicaof $master_host $master_port
+                $replica2 replicaof $master_host $master_port
+
+                wait_for_condition 50 100 {
+                    [string match {*master_link_status:up*} [$replica1 info replication]] &&
+                    [string match {*master_link_status:up*} [$replica2 info replication]]
+                } else {
+                    fail "Can't turn the instance into a replica"
+                }
+
+                set rd [redis_deferring_client -1]
+                $rd debug sleep 1
+                after 100
+
+                # when replica2 will wake up from the sleep it will find both disconnection
+                # from it's master and also a replicaof command at the same event loop
+                $master client kill type replica
+                $replica2 replicaof $replica1_host $replica1_port
+                $rd read
+
+                wait_for_condition 50 100 {
+                    [string match {*master_link_status:up*} [$replica2 info replication]]
+                } else {
+                    fail "role change failed."
+                }
+
+                # make sure psync succeeded, and there were no unexpected full syncs.
+                assert_equal [status $master sync_full] 2
+                assert_equal [status $replica1 sync_full] 0
+                assert_equal [status $replica2 sync_full] 0
+            }
+        }
+    }
+} {} {external:skip}
+
+test {Kill rdb child process if its dumping RDB is not useful} {
+    start_server {tags {"repl"}} {
+        set slave1 [srv 0 client]
+        start_server {} {
+            set slave2 [srv 0 client]
+            start_server {} {
+                set master [srv 0 client]
+                set master_host [srv 0 host]
+                set master_port [srv 0 port]
+                for {set i 0} {$i < 10} {incr i} {
+                    $master set $i $i
+                }
+                # Generating RDB will cost 10s(10 * 1s)
+                $master config set rdb-key-save-delay 1000000
+                $master config set repl-diskless-sync no
+                $master config set save ""
+
+                $slave1 slaveof $master_host $master_port
+                $slave2 slaveof $master_host $master_port
+
+                # Wait for starting child
+                wait_for_condition 50 100 {
+                    ([s 0 rdb_bgsave_in_progress] == 1) &&
+                    ([string match "*wait_bgsave*" [s 0 slave0]]) &&
+                    ([string match "*wait_bgsave*" [s 0 slave1]])
+                } else {
+                    fail "rdb child didn't start"
+                }
+
+                # Slave1 disconnect with master
+                $slave1 slaveof no one
+                # Shouldn't kill child since another slave wait for rdb
+                after 100
+                assert {[s 0 rdb_bgsave_in_progress] == 1}
+
+                # Slave2 disconnect with master
+                $slave2 slaveof no one
+                # Should kill child
+                wait_for_condition 100 10 {
+                    [s 0 rdb_bgsave_in_progress] eq 0
+                } else {
+                    fail "can't kill rdb child"
+                }
+
+                # If have save parameters, won't kill child
+                $master config set save "900 1"
+                $slave1 slaveof $master_host $master_port
+                $slave2 slaveof $master_host $master_port
+                wait_for_condition 50 100 {
+                    ([s 0 rdb_bgsave_in_progress] == 1) &&
+                    ([string match "*wait_bgsave*" [s 0 slave0]]) &&
+                    ([string match "*wait_bgsave*" [s 0 slave1]])
+                } else {
+                    fail "rdb child didn't start"
+                }
+                $slave1 slaveof no one
+                $slave2 slaveof no one
+                after 200
+                assert {[s 0 rdb_bgsave_in_progress] == 1}
+                catch {$master shutdown nosave}
+            }
+        }
+    }
+} {} {external:skip}

+ 61 - 0
tests/modules/Makefile

@@ -0,0 +1,61 @@
+
+# find the OS
+uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
+
+ifeq ($(uname_S),Darwin)
+	SHOBJ_CFLAGS ?= -W -Wall -dynamic -fno-common -g -ggdb -std=c99 -O2
+	SHOBJ_LDFLAGS ?= -bundle -undefined dynamic_lookup
+else	# Linux, others
+	SHOBJ_CFLAGS ?= -W -Wall -fno-common -g -ggdb -std=c99 -O2
+	SHOBJ_LDFLAGS ?= -shared
+endif
+
+# Needed to satisfy __stack_chk_fail_local on Linux with -m32, due to gcc
+# -fstack-protector by default. Breaks on FreeBSD so we exclude it.
+ifneq ($(uname_S),FreeBSD)
+    LIBS = -lc
+endif
+
+TEST_MODULES = \
+    commandfilter.so \
+    basics.so \
+    testrdb.so \
+    fork.so \
+    infotest.so \
+    propagate.so \
+    misc.so \
+    hooks.so \
+    blockonkeys.so \
+    blockonbackground.so \
+    scan.so \
+    datatype.so \
+    datatype2.so \
+    auth.so \
+    keyspace_events.so \
+    blockedclient.so \
+    getkeys.so \
+    test_lazyfree.so \
+    timer.so \
+    defragtest.so \
+    hash.so \
+    zset.so \
+    stream.so \
+
+
+.PHONY: all
+
+all: $(TEST_MODULES)
+
+32bit:
+	$(MAKE) CFLAGS="-m32" LDFLAGS="-melf_i386"
+
+%.xo: %.c ../../src/redismodule.h
+	$(CC) -I../../src $(CFLAGS) $(SHOBJ_CFLAGS) -fPIC -c $< -o $@
+
+%.so: %.xo
+	$(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LDFLAGS) $(LIBS)
+
+.PHONY: clean
+
+clean:
+	rm -f $(TEST_MODULES) $(TEST_MODULES:.so=.xo)

Some files were not shown because too many files changed in this diff