A Redis module that provides rate limiting in Redis as a single command.

Related tags

Database redis-cell
Overview

redis-cell Build Status

A Redis module that provides rate limiting in Redis as a single command. Implements the fairly sophisticated generic cell rate algorithm (GCRA) which provides a rolling time window and doesn't depend on a background drip process.

The primitives exposed by Redis are perfect for doing work around rate limiting, but because it's not built in, it's very common for companies and organizations to implement their own rate limiting logic on top of Redis using a mixture of basic commands and Lua scripts (I've seen this at both Heroku and Stripe for example). This can often result in naive implementations that take a few tries to get right. The directive of redis-cell is to provide a language-agnostic rate limiter that's easily pluggable into many cloud architectures.

Informal benchmarks show that redis-cell is pretty fast, taking a little under twice as long to run as a basic Redis SET (very roughly 0.1 ms per command as seen from a Redis client).

Install

Binaries for redis-cell are available for Mac and Linux. Open an issue if there's interest in having binaries for architectures or operating systems that are not currently supported.

Download and extract the library, then move it somewhere that Redis can access it (note that the extension will be .dylib instead of .so for Mac releases):

$ tar -zxf redis-cell-*.tar.gz
$ cp libredis_cell.so /path/to/modules/

Or, clone and build the project from source. You'll need to install Rust to do so (this may be as easy as a brew install rust if you're on Mac).

$ git clone https://github.com/brandur/redis-cell.git
$ cd redis-cell
$ cargo build --release
$ cp target/release/libredis_cell.dylib /path/to/modules/

Note that Rust 1.13.0+ is required.

Run Redis pointing to the newly built module:

redis-server --loadmodule /path/to/modules/libredis_cell.so

Alternatively add the following to a redis.conf file:

loadmodule /path/to/modules/libredis_cell.so

Usage

From Redis (try running redis-cli) use the new CL.THROTTLE command loaded by the module. It's used like this:

CL.THROTTLE <key> <max_burst> <count per period> <period> [<quantity>]

Where key is an identifier to rate limit against. Examples might be:

  • A user account's unique identifier.
  • The origin IP address of an incoming request.
  • A static string (e.g. global) to limit actions across the entire system.

For example:

CL.THROTTLE user123 15 30 60 1
               ▲     ▲  ▲  ▲ ▲
               |     |  |  | └───── apply 1 token (default if omitted)
               |     |  └──┴─────── 30 tokens / 60 seconds
               |     └───────────── 15 max_burst
               └─────────────────── key "user123"

Response

This means that a single token (the 1 in the last parameter) should be applied against the rate limit of the key user123. 30 tokens on the key are allowed over a 60 second period with a maximum initial burst of 15 tokens. Rate limiting parameters are provided with every invocation so that limits can easily be reconfigured on the fly.

The command will respond with an array of integers:

127.0.0.1:6379> CL.THROTTLE user123 15 30 60
1) (integer) 0
2) (integer) 16
3) (integer) 15
4) (integer) -1
5) (integer) 2

The meaning of each array item is:

  1. Whether the action was limited:
    • 0 indicates the action is allowed.
    • 1 indicates that the action was limited/blocked.
  2. The total limit of the key (max_burst + 1). This is equivalent to the common X-RateLimit-Limit HTTP header.
  3. The remaining limit of the key. Equivalent to X-RateLimit-Remaining.
  4. The number of seconds until the user should retry, and always -1 if the action was allowed. Equivalent to Retry-After.
  5. The number of seconds until the limit will reset to its maximum capacity. Equivalent to X-RateLimit-Reset.

Multiple Rate Limits

Implement different types of rate limiting by using different key names:

CL.THROTTLE user123-read-rate 15 30 60
CL.THROTTLE user123-write-rate 5 10 60

On Rust

redis-cell is written in Rust and uses the language's FFI module to interact with Redis' own module system. Rust makes a very good fit here because it doesn't need a GC and is bootstrapped with only a tiny runtime.

The author of this library is of the opinion that writing modules in Rust instead of C will convey similar performance characteristics, but result in an implementation that's more likely to be devoid of the bugs and memory pitfalls commonly found in many C programs.

License

This is free software under the terms of MIT the license (see the file LICENSE for details).

Development

Tests and checks

Run the test suite:

cargo test

# specific test
cargo test it_rates_limits

# with debug output on stdout
cargo test it_rates_limits -- --nocapture

CI has checks for both Rustfmt and Clippy (Rust's linter). These can be installed and run locally using Rustup's component framework:

rustup component add rustfmt
cargo fmt --all

rustup component add clippy
cargo clippy -- -D warnings

Releasing

Releases are performed automatically from a script in CI which activates when a new tag of the format v1.2.3 is released. The script builds binaries for all target systems and uploads them to GitHub's releases page.

To perform a release:

  1. Add a changelog entry in CHANGELOG.md using the existing format.
  2. Bump the version number in Cargo.toml.
  3. Commit these changes with a message like Bump to version 1.2.3.
  4. Tag the release with git tag v1.2.3 (make sure to include a leading v).
  5. ggpush --tags
  6. Edit the new release's title and body in GitHub (a human touch is still expected for the final product). Use the contents for the new version from CHANGELOG.md as the release's body, which allows Markdown content.
Comments
  • Refactor into separate subcrates, API refactor

    Refactor into separate subcrates, API refactor

    I've done some significant rework of the repo, and most of these choices are off the cuff and suiting me in my own use case, so I don't expect them to be everyone's cup of tea. However I did the work of separating things into distinct mods, as well as added a procedural macro which I think DRYs up the addition of new commands significantly.

    So in summary:

    • fixed a segfault in freeing a null ptr
    • added support for extracting Reply::Error and Reply::Array types from redis
    • bumped crate version to 0.2.0
    • separated Command into RedisCommand and RedisCommandAttrs, allowing part to be derived
    • #[derive(RedisCommandAttrs) support in a procedural macro (redis-command-gen subcrate)
    • updated redismodule.h to support redis 4.0-rc3 (current)
    • added call4 for variadic call with arity of 4 (though I'm not sure I use it)

    In conclusion, I think that we're now 90% of the way to extracting a useful crate to publish on crates.io, but I didn't get around to that, and wanted to see how this would be received before I went ahead and did that.

    I'd love to know people's thoughts.

    opened by dwerner 12
  • redis-cell support cluster?

    redis-cell support cluster?

    hi,i want to know redis celll support redis cluster? according to https://redis.io/topics/modules-api-ref, i not found "getkeys-api" in your code.

    opened by cnsky2016 6
  • Cluster & Client Support

    Cluster & Client Support

    I've got this up and running for some basic tests. I'm pretty excited about what it brings to the table. Thank you!

    Are you aware of this being implemented in a redis cluster scenario and how it may have performed? Also how does a redis client like predis support this new call?

    opened by flickerfly 6
  • stick with one version of gcc, fix linking of redismodule

    stick with one version of gcc, fix linking of redismodule

    Fixing this:

    [dwerner:~/Development/redis-cell]$ cargo build                                                                                                                                                                                                          (master✱) 
       Compiling redis-cell v0.1.0 (file:///home/dwerner/Development/redis-cell)
    error: linking with `cc` failed: exit code: 1
      |
      = note: "cc" "-Wl,--as-needed" "-Wl,-z,noexecstack" "-m64" "-L" "/home/dwerner/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib" "/home/dwerner/Development/redis-cell/target/debug/deps/redis_cell.0.o" "-o" "/home/dwerner/Development/redis-cell/target/debug/deps/libredis_cell.so" "/home/dwerner/Development/redis-cell/target/debug/deps/redis_cell.metadata.o" "-nodefaultlibs" "-L" "/home/dwerner/Development/redis-cell/target/debug/deps" "-L" "/home/dwerner/Development/redis-cell/target/debug/build/redis-cell-e6aa6e97cadb4225/out" "-L" "/home/dwerner/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib" "-Wl,-Bstatic" "-Wl,--whole-archive" "-l" "redismodule" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "-l" "redismodule" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "-l" "redismodule" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "-l" "redismodule" "-Wl,--no-whole-archive" "-Wl,-Bdynamic" "-Wl,--whole-archive" "/tmp/rustc.hv1RaTTFmLaL/libbitflags-65ddff5d2b91509e.rlib" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "/tmp/rustc.hv1RaTTFmLaL/libtime-cd5e3a346d1a17b6.rlib" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "/tmp/rustc.hv1RaTTFmLaL/liblibc-5dc7b85e748840b4.rlib" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "/tmp/rustc.hv1RaTTFmLaL/libstd-13f36e2630c2d79b.rlib" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "/tmp/rustc.hv1RaTTFmLaL/libpanic_unwind-3b9d178f1de89528.rlib" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "/tmp/rustc.hv1RaTTFmLaL/libunwind-93bb403c9fc56f72.rlib" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "/tmp/rustc.hv1RaTTFmLaL/librand-a2ef7979b4b3e1d5.rlib" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "/tmp/rustc.hv1RaTTFmLaL/libcollections-d22754c8c52de3a1.rlib" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "/tmp/rustc.hv1RaTTFmLaL/liballoc-c53f99154bf815c4.rlib" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "/tmp/rustc.hv1RaTTFmLaL/liballoc_system-17a71bb92a82956c.rlib" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "/tmp/rustc.hv1RaTTFmLaL/liblibc-739908a2e215dd88.rlib" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "/tmp/rustc.hv1RaTTFmLaL/libstd_unicode-1cc5fcd37568ebc4.rlib" "-Wl,--no-whole-archive" "-Wl,--whole-archive" "/tmp/rustc.hv1RaTTFmLaL/libcore-3f4289353c600297.rlib" "-Wl,--no-whole-archive" "/tmp/rustc.hv1RaTTFmLaL/libcompiler_builtins-07bfb3bcb2a51da0.rlib" "-l" "util" "-l" "dl" "-l" "rt" "-l" "pthread" "-l" "gcc_s" "-l" "pthread" "-l" "c" "-l" "m" "-l" "rt" "-l" "util" "-shared"
      = note: /home/dwerner/Development/redis-cell/target/debug/build/redis-cell-e6aa6e97cadb4225/out/libredismodule.a(redismodule.o): In function `Export_RedisModule_Init':
              /home/dwerner/Development/redis-cell/src/redismodule.c:6: multiple definition of `Export_RedisModule_Init'
              /home/dwerner/Development/redis-cell/target/debug/build/redis-cell-e6aa6e97cadb4225/out/libredismodule.a(redismodule.o):/home/dwerner/Development/redis-cell/src/redismodule.c:6: first defined here
              /home/dwerner/Development/redis-cell/target/debug/build/redis-cell-e6aa6e97cadb4225/out/libredismodule.a(redismodule.o): In function `Export_RedisModule_Init':
              /home/dwerner/Development/redis-cell/src/redismodule.c:6: multiple definition of `Export_RedisModule_Init'
              /home/dwerner/Development/redis-cell/target/debug/build/redis-cell-e6aa6e97cadb4225/out/libredismodule.a(redismodule.o):redismodule.c:(.text.Export_RedisModule_Init+0x0): first defined here
              /home/dwerner/Development/redis-cell/target/debug/build/redis-cell-e6aa6e97cadb4225/out/libredismodule.a(redismodule.o): In function `Export_RedisModule_Init':
              /home/dwerner/Development/redis-cell/src/redismodule.c:6: multiple definition of `Export_RedisModule_Init'
              /home/dwerner/Development/redis-cell/target/debug/build/redis-cell-e6aa6e97cadb4225/out/libredismodule.a(redismodule.o):redismodule.c:(.text.Export_RedisModule_Init+0x0): first defined here
              collect2: error: ld returned 1 exit status
              
    
    
    opened by dwerner 6
  • Decouple redismodule - rust integration from redis-cell

    Decouple redismodule - rust integration from redis-cell

    Hi @brandur,

    I don't have much experience in rust however I am wondering how complex would be to decouple the part of the code base that interact with redis from your module.

    Ideally I wonder how much work would be to create a crate to simply plug inside the dependencies.

    Best,

    Simone

    opened by siscia 6
  • Publish on crates.io?

    Publish on crates.io?

    Hi,

    I would like to use this library in my project. Basically I want a coupled/embedded rate limiter via in memory Store.

    But I can't find it on crates.io. Is there any reason for that?

    opened by dpc 4
  • Returning Unused Tokens

    Returning Unused Tokens

    I've been utilizing redis-cell for a research project and first and foremost thank you for your work.

    I'm in a situation where I want to all or nothing check two different keys that can be throttled. I need to check one, have it pass then check the other and have it also pass before I approve the action. I perform the entire atomic operation via a LUA script.

    In the event the second key cl.throttle returns 1 (throttled), is it appropriate to return the first via:

    cl.throttle key burst max period -1
    

    Preliminary tests seem to indicate this works, but I'm unsure if this was intentional. Is there a potential fringe case where this would fail or un-optimize key usage?

    opened by radbradd 4
  • CL.THROTTLE command does not trigger snapshot creation for RDB persistence.

    CL.THROTTLE command does not trigger snapshot creation for RDB persistence.

    How to reproduce:

    1. Start Redis server
    redis-server --loadmodule libredis_cell.dylib --save 10 1
    48665:C 21 Feb 2021 12:13:51.776 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
    48665:C 21 Feb 2021 12:13:51.776 # Redis version=6.0.10, bits=64, commit=00000000, modified=0, pid=48665, just started
    48665:C 21 Feb 2021 12:13:51.776 # Configuration loaded
    48665:M 21 Feb 2021 12:13:51.777 * Increased maximum number of open files to 10032 (it was originally set to 256).
                    _._
               _.-``__ ''-._
          _.-``    `.  `_.  ''-._           Redis 6.0.10 (00000000/0) 64 bit
      .-`` .-```.  ```\/    _.,_ ''-._
     (    '      ,       .-`  | `,    )     Running in standalone mode
     |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
     |    `-._   `._    /     _.-'    |     PID: 48665
      `-._    `-._  `-./  _.-'    _.-'
     |`-._`-._    `-.__.-'    _.-'_.-'|
     |    `-._`-._        _.-'_.-'    |           http://redis.io
      `-._    `-._`-.__.-'_.-'    _.-'
     |`-._`-._    `-.__.-'    _.-'_.-'|
     |    `-._`-._        _.-'_.-'    |
      `-._    `-._`-.__.-'_.-'    _.-'
          `-._    `-.__.-'    _.-'
              `-._        _.-'
                  `-.__.-'
    
    48665:M 21 Feb 2021 12:13:51.778 # Server initialized
    48665:M 21 Feb 2021 12:13:51.779 * Module 'redis-cell' loaded from libredis_cell.dylib
    48665:M 21 Feb 2021 12:13:51.780 * Loading RDB produced by version 6.0.10
    48665:M 21 Feb 2021 12:13:51.780 * RDB age 46 seconds
    48665:M 21 Feb 2021 12:13:51.780 * RDB memory usage when created 0.96 Mb
    48665:M 21 Feb 2021 12:13:51.780 * DB loaded from disk: 0.000 seconds
    48665:M 21 Feb 2021 12:13:51.780 * Ready to accept connections
    
    1. Execute command
    127.0.0.1:6379> keys *
    (empty array)
    127.0.0.1:6379> CL.THROTTLE user123 15 1 600 1
    1) (integer) 0
    2) (integer) 16
    3) (integer) 15
    4) (integer) -1
    5) (integer) 600
    127.0.0.1:6379> keys *
    1) "user123"
    

    The RDB snapshot is expected to be created but it is not.

    1. Now let's try to execute simple SET command:
    127.0.0.1:6379> set key value
    OK
    127.0.0.1:6379> keys *
    1) "key"
    2) "user123"
    
    1. DB saved as expected.
    48665:M 21 Feb 2021 12:15:26.450 * 1 changes in 10 seconds. Saving...
    48665:M 21 Feb 2021 12:15:26.450 * Background saving started by pid 48667
    48667:C 21 Feb 2021 12:15:26.452 * DB saved on disk
    

    The same occurs also in Redis for Docker version 5/6. I'm not sure is that Redis or this module issue.

    opened by armenak-baburyan 3
  • [BENCHMARK] Up to 1,100% faster than ratelimit.js

    [BENCHMARK] Up to 1,100% faster than ratelimit.js

    :thumbsup:

    Rough Specification

    As someone with obviously a ton of experience with rate limiting, would be happy to hear your thoughts on the abstraction (if you have the time, of course).

    I've been doing really extensive testing for load-testing etc on it and hope to put this into production where it would def be tested against some serious load!

    More tests and examples of implementation (including the aggregated result) are in the specification above.

    tl;dr

    Just want to say thank you! as this provides a huge performance improvement over what is pretty much the only other established method I have found ON TOP OF actually implementing GCRA rather than a simple "how many requests were made in the last n seconds.

    Stress Test Results

    Calls Setup, Starting Requests
    Running Tests for:  cell
    [ 'user', '1', 'trade' ]
    [ 'user', '2', 'trade' ]
    [ 'user', '3', 'trade' ]
    [ 'user', '4', 'trade' ]
    [ 'user', '5', 'trade' ]
    [ 'user', '6', 'trade' ]
    [ 'user', '7', 'trade' ]
    Running Tests for:  rl
    [ '1', 60, 'user:1:trade' ]
    [ '2', 60, 'user:2:trade' ]
    [ '3', 60, 'user:3:trade' ]
    [ '4', 60, 'user:4:trade' ]
    [ '5', 60, 'user:5:trade' ]
    [ '6', 60, 'user:6:trade' ]
    [ '7', 60, 'user:7:trade' ]
    
        --- Test Results ---
    
        Total Iterations: 5000 * 7 Tests (35,000 iterations each)
    
        rl:
          Total Duration: 343693971.74181193
          Average: 9819.82776405177
          Max: 14198.909738004208
          Min: 5541.352702006698
    
        cell:
          Total Duration: 26785993.573875546
          Average: 765.3141021107299
          Max: 1454.7679300010204
          Min: 322.44201999902725
    
        Diff: cell is 1183% faster
    

    Quick Test Results (consistent against around 500 runs of the test)

    // RUN FOR CELL ONLY (Dont Require rl or include in scope at all)
    Calls Setup, Starting Requests
    Running Tests for:  cell
    [ 'user', '1', 'trade' ]
    [ 'user', '2', 'trade' ]
    [ 'user', '3', 'trade' ]
    [ 'user', '4', 'trade' ]
    [ 'user', '5', 'trade' ]
    [ 'user', '6', 'trade' ]
    [ 'user', '7', 'trade' ]
    
        --- Test Results ---
    
        Total Iterations: 10 * 7 Tests (70 iterations each)
    
        cell:
          Total Duration: 231.97511593997478
          Average: 3.3139302277139255
          Max: 4.6971050053834915
          Min: 2.4761980026960373
    
    // RUN FOR RL ONLY (Dont Require cell or include in scope at all)
    Calls Setup, Starting Requests
    Running Tests for:  rl
    [ '1', 60, 'user:1:trade' ]
    [ '2', 60, 'user:2:trade' ]
    [ '3', 60, 'user:3:trade' ]
    [ '4', 60, 'user:4:trade' ]
    [ '5', 60, 'user:5:trade' ]
    [ '6', 60, 'user:6:trade' ]
    [ '7', 60, 'user:7:trade' ]
    
        --- Test Results ---
    
        Total Iterations: 10 * 7 Tests (70 iterations each)
    
        rl:
          Total Duration: 2249.6415529847145
          Average: 32.13773647121021
          Max: 36.03293499350548
          Min: 28.242240011692047
    

    Implementation / Benchmark

    So this is definitely not even a fair benchmark considering the module runs as a native module and I implemented lua logic to handle the situation - but my goal was to implement a multi-bucket rate limiter using this as a base.

    Team is super spooked by the "note on stability" part though and wants to go with RateLimit.js anyway which is completely client side :-\

    Essentially I take a limits.yaml and use it to build a rate limiting bucket:

    schema:
      # user:
      user:
        children:
          # if no others match
          "*":
            # limits against user:${username}
            limits:
              - 15
              - 30
              - 60
            children:
              # limits against user:${username}:trade
              trade:
                limits:
                  - 5
                  - 10
                  - 15
    

    Then I receive a "bucket key" which is an array of arguments ("user", "myuserid", "trade") and run against each step when limits is found, returning the results so that we can easily and efficiently implement multi-tiered rate limits against user actions with as few requests as possible.

    So essentially when I call ("user", "myuserid", "trade") it is running

    CL.THROTTLE user:myuserid 15 30 60
    CL.THROTTLE user:myuserid:trade 5 10 15
    
    --[[
      Summary:
        Takes the generated lua limits table (from limits.yaml which must be compiled 
        whenever we need to change it) and iterates the request, returning the results
        of all matching rate limits in the requested path.
    ]]
    local LimitsSchema = {
      ["user"] = {
        ["children"] = {["*"] = {["limits"] = {15, 30, 60}, ["children"] = {["trade"] = {["limits"] = {5, 10, 15}}}}}
      }
    }
    
    local Response, LimitsTable, CurrentPath = {}, {}, {}
    local Complete = 1
    local child
    
    local CheckLimit = function(bucket, args)
      return redis.call("CL.THROTTLE", bucket, unpack(args))
    end
    
    for k, v in ipairs(KEYS) do
      if LimitsSchema[v] then
        child = LimitsSchema[v]
      elseif LimitsSchema["*"] then
        child = LimitsSchema["*"]
      else
        Complete = 0
        break
      end
    
      table.insert(CurrentPath, v)
    
      if child["limits"] then
        LimitsTable[table.concat(CurrentPath, ":")] = child["limits"]
      end
      LimitsSchema = child["children"]
    end
    
    for k, v in pairs(LimitsTable) do
      table.insert(Response, CheckLimit(k, v))
    end
    
    if Complete == 0 then
      return redis.error_reply("Invalid Path at: " .. table.concat(CurrentPath, ":"))
    end
    
    return Response
    

    Lua Performance: In an initial check with the lua performance when running the benchmark, looks like the latency of the request being made averaged at around 0.50 milliseconds per call... pretty awesome!

    Then utilizing ioredis to handle the script

    // static setup that shouldnt count towards perf since it is
    // done one time per server instance
    const Redis = require("ioredis");
    const fs = require("fs");
    const path = require("path");
    const redis = new Redis();
    
    const cmd = {
      lua: fs.readFileSync(
        path.resolve(__dirname, "..", "lua", "traverse.lua")
      )
    };
    
    redis.defineCommand("limiter", cmd);
    
    module.exports = redis;
    
    const { performance } = require("perf_hooks");
    const redis = require("./cell-setup");
    
    function checkPath(...path) {
      return redis.limiter(path.length, ...path);
    }
    
    async function request(...args) {
      const startTime = performance.now();
      await checkPath(...args);
      return performance.now() - startTime;
    }
    
    module.exports = {
      request
    };
    
    opened by bradennapier 3
  • Doubts about remaining value

    Doubts about remaining value

    I think that the remaining value is incorrect, because it takes into account the max burst, but not the count per period.

    2 examples explaining my point of view. I am doing request to CL.THROTTLE every ~250 milliseconds with the following configurations

    Example with: CL.THROTTLE thekey 0 2 1

    Req	millis	limited	limit	remaining retry	reset
    1 	259 	0 	1 	0 	-1 	0
    2 	533 	0 	1 	0 	-1 	0
    3 	783 	0 	1 	0 	-1 	0
    4 	1035 	0 	1 	0 	-1 	1
    5 	1289 	0 	1 	0 	-1 	1
    6 	1541 	1 	1 	0 	1 	1
    7 	1790 	0 	1 	0 	-1 	1
    8 	2040 	1 	1 	0 	1 	1
    

    I would expect:

    Req	millis	limited	limit	remaining retry	reset
    1 	259 	0 	1 	2 	-1 	0
    2 	533 	0 	1 	1 	-1 	0
    3 	783 	0 	1 	1 	-1 	0
    4 	1035 	0 	1 	0 	-1 	1
    5 	1289 	0 	1 	0 	-1 	1
    6 	1541 	1 	1 	0 	1 	1
    7 	1790 	0 	1 	0 	-1 	1
    8 	2040 	1 	1 	0 	1 	1
    

    Example with: CL.THROTTLE thekey 2 2 1

    Req	millis	limited	limit	remaining retry	reset
    1 	259 	0 	3 	2 	-1 	0
    2 	512 	0 	3 	1 	-1 	0
    3 	765 	0 	3 	1 	-1 	0
    4 	1016 	0 	3 	0 	-1 	1
    5 	1268 	0 	3 	0 	-1 	1
    6 	1519 	0 	3 	0 	-1 	1
    7 	1769 	0 	3 	0 	-1 	1
    8 	2021 	0 	3 	0 	-1 	2
    9 	2273 	0 	3 	0 	-1 	2
    10 	2523 	1 	3 	0 	1 	2
    11 	2775 	0 	3 	0 	-1 	2
    12 	3025 	1 	3 	0 	1 	2
    

    I would expect:

    Req	millis	limited	limit	remaining retry	reset
    1 	259 	0 	3 	4 	-1 	0
    2 	512 	0 	3 	3 	-1 	0
    3 	765 	0 	3 	3 	-1 	0
    4 	1016 	0 	3 	2 	-1 	1
    5 	1268 	0 	3 	2 	-1 	1
    6 	1519 	0 	3 	1 	-1 	1
    7 	1769 	0 	3 	1 	-1 	1
    8 	2021 	0 	3 	0 	-1 	2
    9 	2273 	0 	3 	0 	-1 	2
    10 	2523 	1 	3 	0 	1 	2
    11 	2775 	0 	3 	0 	-1 	2
    12 	3025 	1 	3 	0 	1 	2
    

    What do you think @brandur? Am I forgetting something?

    Thanks!!

    opened by oleurud 3
  • Ceil fractional retry_after and reset_after

    Ceil fractional retry_after and reset_after

    I have faced 429s several times after waiting what the retry headers say. They are filled with num_seconds(), which obviates the fractional part of the duration.

    This patch ceils the value if there is a fractional part (in milliseconds).

    Closes #56.

    opened by fxn 2
  • Response type not string compatible.

    Response type not string compatible.

    I have a rust app that is using redis and redis-cell. I am using the bb8 crate for connection pooling. When I put the server under heavy load, there are errors in my log. I'm unsure if this is a problem in redis-cell or bb8 or my code.

    2022-08-10T21:46:09.361897Z  WARN web3_proxy::bb8_helpers: redis error err=Response was of incompatible type: "Response type not string compatible." (response was bulk(int(0), int(2000001), int(1999997), int(-1), int(0)))
    

    Any ideas?

    opened by WyseNynja 1
  • A redis-cell  Questions about strings

    A redis-cell Questions about strings

    I have ran into a problem using redis-cell. As shown in the code, passing a string variable will throw an error of "Cell error: invalid digit found in string" but passing a string literal does the job. To my knowledge, using a string variable should be the same as using a string literal. Is there any different interpretation in redis-cell or is there any difference in golang that causes this problem? How can I use a string variable? Thanks

    func test() { key := "example" cli := redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", Password: "", DB: 0, }) _, err := cli.Ping().Result() if err != nil { return nil, fmt.Errorf("redis ping failed,err is:%s", err.Error()) } _, err = control.redisCli.Do("cl.throttle", key, 89, 90, 10, 1).Result() //fail,this err is "Cell error: invalid digit found in string" if err != nil { return } _, err = control.redisCli.Do("cl.throttle", "111" + key, 89, 90, 10, 1).Result() // success if err != nil { return } _, err = control.redisCli.Do("cl.throttle", "example", 89, 90, 10, 1).Result() //success if err != nil { return } return }

    opened by Edelweiss-Snow 0
  • Reconfiguration caused unexpected behavior

    Reconfiguration caused unexpected behavior

    Firstly, it works good, as we can seen below.

    127.0.0.1:6579> CL.THROTTLE user1 15 10 20 1
    1) (integer) 0
    2) (integer) 16
    3) (integer) 15
    4) (integer) -1
    5) (integer) 2
    127.0.0.1:6579> CL.THROTTLE user1 15 10 20 1
    1) (integer) 0
    2) (integer) 16
    3) (integer) 15
    4) (integer) -1
    5) (integer) 2
    127.0.0.1:6579> CL.THROTTLE user1 15 10 20 1
    1) (integer) 0
    2) (integer) 16
    3) (integer) 14
    4) (integer) -1
    5) (integer) 3
    127.0.0.1:6579> CL.THROTTLE user1 15 10 20 1
    1) (integer) 0
    2) (integer) 16
    3) (integer) 13
    4) (integer) -1
    5) (integer) 4
    ...
    

    Then I tried reconfigured it. I just increased rate and it caused unexpected behavior. The returned values got deny. Why it returned deny since I increased rate?

    127.0.0.1:6579> CL.THROTTLE user1 15 10 20 1
    1) (integer) 0
    2) (integer) 16
    3) (integer) 13
    4) (integer) -1
    5) (integer) 4
    127.0.0.1:6579> CL.THROTTLE user1 15 1000 20 1
    1) (integer) 1
    2) (integer) 16
    3) (integer) 0
    4) (integer) 2
    5) (integer) 2
    127.0.0.1:6579> CL.THROTTLE user1 15 1000 20 1
    1) (integer) 1
    2) (integer) 16
    3) (integer) 0
    4) (integer) 1
    5) (integer) 1
    

    I guess it is because time need reset to its maximum capacity is not zero so when I change configuration it will deny unit it run out of last seconds. And I think it was not conforming to the statement in readme which said: Rate limiting parameters are provided with every invocation so that limits can easily be reconfigured on the fly.

    opened by whitehatboxer 0
  • Should provide flags for redis 6

    Should provide flags for redis 6

    I've noticed that the list of flags returned in the 7th value of COMMAND for cl.throttle in redis 6.x is empty. I think @write and @fast apply.

    This may be related to issues I'm having with redis-cell and lettuce on redis 6, but I'm not sure: https://github.com/lettuce-io/lettuce-core/issues/1327

    I'm not able to determine from the documentation what the new flag list is; so if anyone knows that would be helpful; otherwise I'll update this ticket when I find out:

     72) 1) "cl.throttle"
         2) (integer) -1
         3) 1) write
         4) (integer) 0
         5) (integer) 0
         6) (integer) 0
         7) (empty array)
    
    opened by TheCycoONE 2
  • does not work when parameters are large

    does not work when parameters are large

    I downloaded the latest version and used it with redis server version 5.0.7, I tested cl.throttle command with both small and large parameters (small capacity + refilling speed, large capacity + refilling speed). The result shows that when capacity and refilling speed are both large, like 6000 at most, and every second 6000 (kind like a tps throttling), it does not work at all. Besides, cl.throttle a 6000 6000 1, gives the third parameter larger than 6000 capacity, very odd.

    import redis
    import time
    client = redis.StrictRedis()
    
    def start_throttle():
        n = 0
        while True:
            n += 1
            result = client.execute_command("cl.throttle", "a", 5, 3, 1)
    # in another test, I used 6000, 6000, 1
            print n, result
    
    opened by Jovons 4
  • Why Retry-After and Reset is the same?

    Why Retry-After and Reset is the same?

    Should X-RateLimit-Reset response be number of seconds until maximum capacity of limit? If so, in my example it is always the same as Retry-After.

    irb(main):004:0> 60.times { p redis.call('CL.THROTTLE', 'testkey', 0, 3, 60, 1); sleep 1 }
    [0, 1, 0, -1, 20]
    [1, 1, 0, 18, 18]
    [1, 1, 0, 17, 17]
    [1, 1, 0, 16, 16]
    [1, 1, 0, 15, 15]
    [1, 1, 0, 14, 14]
    [1, 1, 0, 13, 13]
    [1, 1, 0, 12, 12]
    [1, 1, 0, 11, 11]
    [1, 1, 0, 10, 10]
    [1, 1, 0, 9, 9]
    [1, 1, 0, 8, 8]
    [1, 1, 0, 7, 7]
    [1, 1, 0, 6, 6]
    [1, 1, 0, 5, 5]
    [1, 1, 0, 4, 4]
    [1, 1, 0, 3, 3]
    [1, 1, 0, 2, 2]
    [1, 1, 0, 1, 1]
    [1, 1, 0, 0, 0]
    [0, 1, 0, -1, 20]
    [1, 1, 0, 18, 18]
    [1, 1, 0, 17, 17]
    [1, 1, 0, 16, 16]
    [1, 1, 0, 15, 15]
    [1, 1, 0, 14, 14]
    [1, 1, 0, 13, 13]
    [1, 1, 0, 12, 12]
    [1, 1, 0, 11, 11]
    [1, 1, 0, 10, 10]
    [1, 1, 0, 9, 9]
    [1, 1, 0, 8, 8]
    [1, 1, 0, 7, 7]
    [1, 1, 0, 6, 6]
    [1, 1, 0, 5, 5]
    [1, 1, 0, 4, 4]
    [1, 1, 0, 3, 3]
    [1, 1, 0, 2, 2]
    [1, 1, 0, 1, 1]
    [1, 1, 0, 0, 0]
    [0, 1, 0, -1, 20]
    [1, 1, 0, 18, 18]
    [1, 1, 0, 17, 17]
    [1, 1, 0, 16, 16]
    [1, 1, 0, 15, 15]
    [1, 1, 0, 14, 14]
    [1, 1, 0, 13, 13]
    [1, 1, 0, 12, 12]
    [1, 1, 0, 11, 11]
    [1, 1, 0, 10, 10]
    [1, 1, 0, 9, 9]
    [1, 1, 0, 8, 8]
    [1, 1, 0, 7, 7]
    [1, 1, 0, 6, 6]
    [1, 1, 0, 5, 5]
    [1, 1, 0, 4, 4]
    [1, 1, 0, 3, 3]
    [1, 1, 0, 2, 2]
    [1, 1, 0, 1, 1]
    [1, 1, 0, 0, 0]
    
    opened by molfar 1
Releases(v0.3.0)
Owner
Brandur Leach
Engineering at @CrunchyData. APIs, terminal productivity, running, and metal. Ex-@heroku, ex-@stripe.
Brandur Leach
A port of `java.util.*SummaryStatistics` as a Redis Module

RedisNumbersStats RedisNumbersStats is a Redis module that implements a Redis version of the Java Util *SummaryStatistics classes, such as DoubleSumma

Brian Sam-Bodden 4 Oct 4, 2022
Redis re-implemented in Rust.

rsedis Redis re-implemented in Rust. Why? To learn Rust. Use Cases rsedis does not rely on UNIX-specific features. Windows users can run it as a repla

Sebastian Waisbrot 1.6k Jan 6, 2023
Redis library for rust

redis-rs Redis-rs is a high level redis library for Rust. It provides convenient access to all Redis functionality through a very flexible but low-lev

Armin Ronacher 2.8k Jan 8, 2023
RedisLess is a fast, lightweight, embedded and scalable in-memory Key/Value store library compatible with the Redis API.

RedisLess is a fast, lightweight, embedded and scalable in-memory Key/Value store library compatible with the Redis API.

Qovery 145 Nov 23, 2022
RedisJSON - a JSON data type for Redis

RedisJSON RedisJSON is a Redis module that implements ECMA-404 The JSON Data Interchange Standard as a native data type. It allows storing, updating a

null 3.4k Jan 1, 2023
A rust Key-Value store based on Redis.

Key-Value Store A Key-Value store that uses Redis to store data. Built using an async web framework in Rust with a full Command-Line interface and log

Miguel David Salcedo 0 Jan 14, 2022
Incomplete Redis client and server implementation using Tokio - for learning purposes only

mini-redis mini-redis is an incomplete, idiomatic implementation of a Redis client and server built with Tokio. The intent of this project is to provi

Tokio 2.3k Jan 4, 2023
Basic Redis Protocol specification in Rust

Basic Redis Protocol specification in Rust

Bruno 1 Jan 20, 2022
Rewrite Redis in Rust for evaluation and learning.

Drill-Redis This library has been created for the purpose of evaluating Rust functionality and performance. As such, it has not been fully tested. The

Akira Kawahara 3 Oct 18, 2022
Redis compatible server framework for Rust

Redis compatible server framework for Rust Features Create a fast custom Redis compatible server in Rust Simple API. Support for pipelining and telnet

Josh Baker 61 Nov 12, 2022
A simplified version of a Redis server supporting SET/GET commands

This is a starting point for Rust solutions to the "Build Your Own Redis" Challenge. In this challenge, you'll build a toy Redis clone that's capable

Patrick Neilson 2 Nov 15, 2022
An intentionally-limited Rust implementation of the Redis server with no external dependencies.

lil-redis An intentionally-limited Rust implementation of the Redis server. lil redis is an accessible implementation of a very basic Redis server (wi

Miguel Piedrafita 37 Jan 1, 2023
Macros for redis-rs to serialize and deserialize structs automatically

redis-macros Simple macros and wrappers to redis-rs to automatically serialize and deserialize structs with serde. Installation To install it, simply

Daniel Grant 4 Feb 18, 2023
A cross-platform redis gui client

Redis-Manager A cross-platform redis gui client started developing with Tauri, React and Typescript in Vite. Get Started Prerequisites Install Node.js

Kurisu 11 Mar 31, 2023
📺 Netflix in Rust/ React-TS/ NextJS, Actix-Web, Async Apollo-GraphQl, Cassandra/ ScyllaDB, Async SQLx, Kafka, Redis, Tokio, Actix, Elasticsearch, Influxdb Iox, Tensorflow, AWS

Fullstack Movie Streaming Platform ?? Netflix in RUST/ NextJS, Actix-Web, Async Apollo-GraphQl, Cassandra/ ScyllaDB, Async SQLx, Spark, Kafka, Redis,

null 34 Apr 17, 2023
Lightweight async Redis client with connection pooling written in pure Rust and 100% memory safe

redi-rs (or redirs) redi-rs is a Lightweight Redis client with connection pooling written in Rust and 100% memory safe redi-rs is a Redis client writt

Oğuz Türkay 4 May 20, 2023
Simple and flexible queue implementation for Rust with support for multiple backends (Redis, RabbitMQ, SQS, etc.)

Omniqueue Omniqueue is an abstraction layer over queue backends for Rust. It includes support for RabbitMQ, Redis streams, and SQS out of the box. The

Svix 8 May 26, 2023
Sharded, concurrent mini redis that support http interface implemented in rust

Rudis A mini version of redis server that provides http interface implemented in Rust. The in-memorry kv-storage is sharded and concurrent safe. Inspi

Lorenzo Cao 43 May 30, 2023
Automatically publish MongoDB changes to Redis for Meteor.

changestream-to-redis Warning The project is currently in its alpha phase. There are no production loads using it yet nor any large-scale tests were c

Radosław Miernik 8 Jul 29, 2023