diff options
Diffstat (limited to '')
356 files changed, 26924 insertions, 0 deletions
diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..1edc391 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,9 @@ +[bumpversion] +current_version = 1.9.1 +commit = True +tag = True + +[bumpversion:file:iredis/__init__.py] + +[bumpversion:file:pyproject.toml] + @@ -0,0 +1,5 @@ +[flake8] +ignore = D203,W503,W605,C901,E203 +exclude = .git,__pycache__,build,dist,venv +max-complexity = 14 +max-line-length = 120 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..9db21e2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +patreon: laixintao +custom: ["https://www.kawabangga.com/sponsor", "http://paypal.me/laixintao"] diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..d0cc7df --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,152 @@ +name: Release + +on: + push: + tags: + - v* + +jobs: + release-pypi: + name: release-pypi + runs-on: ubuntu-16.04 + + # FIXME + # help test shouldn't depends on this to run + services: + redis: + image: redis:5 + ports: + - 6379:6379 + options: --entrypoint redis-server + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.7 + architecture: 'x64' + - name: Cache venv + uses: actions/cache@v1 + with: + path: venv + # Look to see if there is a cache hit for the corresponding requirements file + key: ubuntu-16.04-poetryenv-${{ hashFiles('poetry.lock') }} + - name: Install Dependencies + run: | + python3 -m venv venv + . venv/bin/activate + pip install -U pip + pip install poetry + poetry install + python -c "import sys; print(sys.version)" + pip list + - name: Poetry Build + run: | + . venv/bin/activate + poetry build + - name: Test Build + run: | + python3 -m venv fresh_env + . fresh_env/bin/activate + pip install dist/*.whl + + iredis -h + iredis help GET + + - name: Upload to Pypi + env: + PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: | + . venv/bin/activate + poetry publish --username __token__ --password ${PASSWORD} + + release-binary: + name: Release Executable Binary. + runs-on: ubuntu-16.04 + + # FIXME + # help test shouldn't depends on this to run + services: + redis: + image: redis + ports: + - 6379:6379 + options: --entrypoint redis-server + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.7 + architecture: 'x64' + - name: Cache venv + uses: actions/cache@v1 + with: + path: venv + # Look to see if there is a cache hit for the corresponding requirements file + key: ubuntu-16.04-poetryenv-${{ hashFiles('poetry.lock') }} + - name: Install Dependencies + run: | + python3 -m venv venv + . venv/bin/activate + pip install -U pip + pip install poetry + poetry install + python -c "import sys; print(sys.version)" + pip list + - name: Poetry Build + run: | + . venv/bin/activate + poetry build + - name: Test Build + run: | + python3 -m venv fresh_env + . fresh_env/bin/activate + pip install dist/*.whl + + iredis -h + iredis help GET + + - name: Cache cargo registry + uses: actions/cache@v1 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry + + - name: Executable Build + run: | + # pyoxidizer doesn't know the wheel path, and it doesn't support passing env vars + export WHEEL_PATH=`ls ./dist/iredis*.whl` + envsubst '$WHEEL_PATH' < pyoxidizer.template.bzl > pyoxidizer.bzl + cargo install pyoxidizer --vers 0.6.0 + pyoxidizer build --release install + cd ./build/x86*/release/install + tar -zcf ../../../iredis.tar.gz lib/ iredis + cd - + + - name: Test Executable + run: | + ./build/x86*/release/install/iredis -h + ./build/x86*/release/install/iredis help GET + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + + - name: Upload Release Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./build/iredis.tar.gz + asset_name: iredis.tar.gz + asset_content_type: application/gzip diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..1c0a360 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,81 @@ +name: Test + +on: + pull_request: + push: + branches: + - master + +jobs: + test: + name: Pytest + strategy: + matrix: + os: [ubuntu-16.04] + python: ['3.6', '3.7', '3.8'] + redis: [5, 6] + runs-on: ${{ matrix.os }} + + services: + redis: + image: redis:${{ matrix.redis }} + ports: + - 6379:6379 + options: --entrypoint redis-server + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + architecture: 'x64' + - name: Cache venv + uses: actions/cache@v1 + with: + path: venv + # Look to see if there is a cache hit for the corresponding requirements file + key: ${{ matrix.os }}-poetryenv-${{ hashFiles('poetry.lock') }} + - name: Install Dependencies + run: | + python3 -m venv venv + . venv/bin/activate + pip install -U pip + pip install poetry + poetry install + python -c "import sys; print(sys.version)" + pip list + - name: Pytest + env: + REDIS_VERSION: ${{ matrix.redis }} + run: | + . venv/bin/activate + pytest + lint: + name: flake8 & black + runs-on: ubuntu-16.04 + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.7 + architecture: 'x64' + - name: Cache venv + uses: actions/cache@v1 + with: + path: venv + # Look to see if there is a cache hit for the corresponding requirements file + key: lintenv-v2 + - name: Install Dependencies + run: | + python3 -m venv venv + . venv/bin/activate + pip install -U pip flake8 black + - name: Flake8 test + run: | + . venv/bin/activate + flake8 . + - name: Black test + run: | + . venv/bin/activate + black --check . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..86b0f1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,108 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +*.aof + +# IDE +.vscode
\ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..61423c6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "iredis/data/redis-doc"] + path = iredis/data/redis-doc + url = https://github.com/antirez/redis-doc.git +[submodule "redis-doc"] + path = redis-doc + url = https://github.com/antirez/redis-doc.git diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b565097 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,218 @@ +### 1.9.1 + +- Feature: support auto-reissue command to another Redis server, when got a + "MOVED" error in redis cluster. + +## 1.9 + +- Feature: Support `LPOS` command. +- Doc: Update docs in `HELP` command update to date. + +## 1.8 + +- Feature: Fully support Redis6! + - Support `STRALGO` command. + - `MIGRATE` command now support `AUTH2`. + - DISABLE `hello` command, IRedis not support RESP3. + +### 1.7.4 + +- Bugfix: Lock wcwidth's version on `1.9.0`. Fix binary build. + +### 1.7.3 + +- Bugfix: IRedis can be suspended by <kbd>Ctrl</kbd> + <kbd>Z</kbd>. (Thanks + [wooden-robot]) +- Bugfix: Press <kbd>Enter</kbd> when completion is open will not execute + commands. (Thanks [wooden-robot]) +- Feature: `AUTH` command is now compatible with both Redis 5 and Redis 6. +- Redis6 support: `CLIENT KILL` support kill by `USER`, `XINFO` command support + `FULL` option. + +### 1.7.2 + +- Feature: Support `ACL` ( [#340](https://github.com/laixintao/iredis/pull/343) + ). +- Bugfix: Include tests in source distribution. + +### 1.7.1 + +- Bugfix: `command in` considered as an invalid input case, due to matched with + `command`'s syntax, and `in` as an extra args. Fixed by falling back to + default grammar if there are ambiguous commands that can match. + +## 1.7 + +- Update: Builtin doc was updated with latest + redis-doc(dd4159397f115d53423c21337eedb04d3258d291). +- Feature: New command support: `CLIENT GETREDIR`, `CLIENT TRACKING` and +- Test: IRedis now was tested in both Redis 5 and Redis 6. +- Bugfix: Fix exception when transaction fails. (Thanks [brianmaissy]) +- Bugfix: Merging multiple spaces bug, e.g. `set foo "hello world"` will result + in sending `set foo "hello world"` to redis-server. `CLIENT CACHING`. +- Bugfix: `--url` options is ignored, but don't worry, it is fixed now by + [otms61]. + +### 1.6.2 + +- Bugfix: `INFO` command accepts `section` now. +- Bugfix: refused to start when can not create connection. + +### 1.6.1 + +- Bugfix: Dangerous command will still run even user canceled. + +## 1.6 + +- Feature: support pager. You can disable it using `--no-pager` or in your + `iredisrc`, or change the pager behavior by setting `pager` in `iredisrc`. + +## 1.5 + +- Bugfix: PEEK command do not use MEMORY USAGE before redis version 4.0. +- Feature: Support disable shell pipeline feature in iredisrc. (Thanks + [wooden-robot]) + +### 1.4.3 + +- Support `LOLWUT` command of Redis 6 version. + +### 1.4.2 + +- Password for `AUTH` command will be hidden as `*`. + +### 1.4.1 + +- This is a test release, nothing new. + +## 1.4.0 + +- Bugfix: Fix PyOxidizer binary build, by locking the importlib_resources + version. + +### 1.3.1 + +- Bugfix: Fix PyOxidizer binary build. +- Feature: Completer for HELP command. +- Bugfix: Lowercase for `--newbie` mode. +- Bugfix: Bottom hint for IRedis builtin commands. + +## 1.3.0 + +- Catch up with redis-doc: d19fb20..6927ef0: + - `SET` command support `KEEPTTL` option. + - `LPUSHX` accepts multiple elements. + - Add commands support for: + - CLUSTER BUMPEPOCH + - CLUSTER FLUSHSLOTS + - CLUSTER MYID + - MODULE LIST + - MODULE LOAD + - MODULE UNLOAD + - PSYNC + - LATENCY DOCTOR + - LATENCY GRAPH + - LATENCY HISTORY + - LATENCY LATEST + - LATENCY RESET + - LATENCY HELP + +## 1.2.0 + +- Feature: Peek command now displays more friendly, before each "info" will take + one line, now type/encoding/ttl/memory usage will display in one line, makes + the result looks more clear. +- Support DSN. (Thanks [lyqscmy]). +- Support URL. +- Support socket connection. + +### 1.1.2 + +- Feature: support history location config. + +### 1.1.1 + +- This release is for testing the binary build, nothing else changed. + +## 1.1 + +- Feature: Package into a single binary with PyOxidizer (thanks [Mac Chaffee]) + +### 1.0.5 + +- Feature: <kbd>Ctrl - X</kbd> then <kbd>Ctrl -E</kbd> to open an editor to edit + command. +- Feature: Support `completion_casing` config. + +### 1.0.4 + +- Bugfix: command completions when a command is substring of another command. + [issue#198](https://github.com/laixintao/iredis/issues/198) + +### 1.0.3 + +- Feature: Support `bitfield` command, and a new completer for int type. + +### 1.0.2 + +- Internal: Migrate CI from travis and circleci to github action. + +### 1.0.1 + +- Bugfix: Fix info command decode error on + decode=utf-8 #[266](https://github.com/laixintao/iredis/pull/266) + +# 1.0 + +- Feature: Support `EXIT` to exit iredis REPL. +- Feature: Support `CLEAR` to clear screen. +- Feature: Support config log location in iredisrc file, default to None. + +### 0.9.1 + +- Feature: Support `PEEK` Command. + +## 0.9 + +- Refactor: split completer update and response render; Move cli tests to travis + ci. (Thanks: [ruohan.chen]) +- Support stream commands. _ Timestamp completer support. _ Stream command + renders and lexers. +- Bugfix: When response is None, + `iredis.completers.udpate_completer_for_responase` will raise Exception. + +### 0.8.12 + +- Bugfix: Multi spaces between commands can be recongnised as correct commands + now. +- Feature: Warning on dangerous command. + +### 0.8.11 + +- Bugfix: Fix HELP command can not render markdown with a `<h3>` header. +- Bugfix: Pipeline using a builtin Python API. + +### 0.8.10 + +- Bugfix: previous version of iredis didn't package redis-doc correctly. +- Feature: prompt for dangerous commands. + +### 0.8.9 + +- Support config files. + +### 0.8.8 + +- Bugfix: pipeline in iredis can run shell command include pipes. thanks to + [Wooden-Robot]. + +### 0.8.7 + +- Support connect shell utilities with pipeline + +[wooden-robot]: https://github.com/Wooden-Robot +[ruohan.chen]: https://github.com/crhan +[mac chaffee]: https://github.com/mac-chaffee +[lyqscmy]: https://github.com/lyqscmy +[brianmaissy]: https://github.com/brianmaissy +[otms61]: https://github.com/otms61 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..99c0463 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3 + +WORKDIR /iredis +COPY README.md poetry.lock pyproject.toml ./ +COPY iredis ./iredis + +RUN apt-get update && apt-get install -y --allow-unauthenticated \ + redis-server && \ + rm -rf /var/lib/apt/lists/* + + +RUN python3 -m venv iredis_env && \ + . iredis_env/bin/activate && \ + pip install poetry && \ + poetry install --no-dev && \ + rm -rf ~/.cache + +CMD ["sh","-c","redis-server --daemonize yes && . iredis_env/bin/activate && iredis"] @@ -0,0 +1,10 @@ +Copyright (c) 2019, laixintao +All rights reserved. + +IRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * IRedistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * IRedistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of IRedis nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fff2cbe --- /dev/null +++ b/README.md @@ -0,0 +1,228 @@ +<p align="center"> + <img width="100" height="100" src="https://raw.githubusercontent.com/laixintao/iredis/master/docs/assets/logo.png" /> +</p> + +<h3 align="center">Interactive Redis: A Cli for Redis with AutoCompletion and Syntax Highlighting.</h4> + +<p align="center"> +<a href="https://github.com/laixintao/iredis/actions"><img src="https://github.com/laixintao/iredis/workflows/Test/badge.svg" alt="Github Action"></a> +<a href="https://badge.fury.io/py/iredis"><img src="https://badge.fury.io/py/iredis.svg" alt="PyPI version"></a> +<img src="https://badgen.net/badge/python/3.6%20|%203.7%20|%203.8/" alt="Python version"> +<a href="https://pepy.tech/project/iredis"><img src="https://pepy.tech/badge/iredis" alt="Download stats"></a> +<a href="https://t.me/iredis_users"><img src="https://badgen.net/badge/icon/join?icon=telegram&label=usergroup" alt="Chat on telegram"></a> +<a href="https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/laixintao/iredis&cloudshell_print=docs/cloudshell/run-in-docker.txt"><img src="https://badgen.net/badge/run/GoogleCloudShell/blue?icon=terminal" alt="Open in Cloud Shell"></a> +</p> + +<p align="center"> + <img src="./docs/assets/demo.svg" alt="demo"> +</p> + +IRedis is a terminal client for redis with auto-completion and syntax +highlighting. IRedis lets you type Redis commands smoothly, and displays results +in a user-friendly format. + +IRedis is an alternative for redis-cli. In most cases, IRedis behaves exactly +the same as redis-cli. Besides, it is safer to use IRedis on production servers +than redis-cli: IRedis will prevent accidentally running dangerous commands, +like `KEYS *` (see +[Redis docs / Latency generated by slow commands](https://redis.io/topics/latency#latency-generated-by-slow-commands)). + +## Features + +- Advanced code completion. If you run command `KEYS` then run `DEL`, IRedis + will auto-complete your command based on `KEYS` result. +- Command validation. IRedis will validate command while you are typing, and + highlight errors. E.g. try `CLUSTER MEET IP PORT`, IRedis will validate IP and + PORT for you. +- Command highlighting, fully based on redis grammar. Any valid command in + IRedis shell is a valid redis command. +- Human-friendly result display. +- _pipeline_ feature, you can use your favorite shell tools to parse redis' + response, like `get json | jq .`. +- Support pager for long output. +- Support connection via URL, `iredis --url redis://example.com:6379/1`. +- Store server configuration: `iredis -d prod-redis` (see [dsn](#using-dsn) for + more). +- `peek` command to check the key's type then automatically call + `get`/`lrange`/`sscan`, etc, depending on types. You don't need to call the + `type` command then type another command to get the value. `peek` will also + display the key's length and memory usage. +- <kbd>Ctrl</kbd> + <kbd>C</kbd> to cancel the current typed command, this won't + exit IRedis, exactly like bash behaviour. Use <kbd>Ctrl</kbd> + <kbd>D</kbd> + to send a EOF to exit IRedis. +- <kbd>Ctrl</kbd> + <kbd>R</kbd> to open **reverse-i-search** to search through + your command history. +- Auto suggestions. (Like [fish shell](http://fishshell.com/).) +- Support `--encode=utf-8`, to decode Redis' bytes responses. +- Command hint on bottom, include command syntax, supported redis version, and + time complexity. +- Official docs with built-in `HELP` command, try `HELP SET`! +- Written in pure Python, but IRedis was packaged into a single binary with + [PyOxidizer](https://github.com/indygreg/PyOxidizer), you can use cURL to + download and run, it just works, even you don't have a Python interpreter. +- Hide password for `AUTH` command. +- Says "Goodbye!" to you when you exit! +- For full features, please see: [iredis.io](https://www.iredis.io) + +## Install + +Install via pip: + +``` +pip install iredis +``` + +[pipx](https://github.com/pipxproject/pipx) is recommended: + +``` +pipx install iredis +``` + +Or you can download the executable binary with cURL(or wget), untar, then run. +It is especially useful when you don't have a python interpreter(E.g. the +[official Redis docker image](https://hub.docker.com/_/redis/) which doesn't +have Python installed.): + +``` +wget https://github.com/laixintao/iredis/releases/latest/download/iredis.tar.gz \ + && tar -xzf iredis.tar.gz \ + && ./iredis +``` + +(Check the [release page](https://github.com/laixintao/iredis/releases) if you +want to download an old version of IRedis.) + +## Usage + +Once you install IRedis, you will know how to use it. Just remember, IRedis +supports similar options like redis-cli, like `-h` for redis-server's host and +`-p` for port. + +``` +$ iredis --help +``` + +### Using DSN + +IRedis support storing server configuration in config file. Here is a DSN +config: + +``` +[alias_dsn] +dev=redis://localhost:6379/4 +staging=redis://username:password@staging-redis.example.com:6379/1 +``` + +Put this in your `iredisrc` then connect via `iredis -d staging` or +`iredis -d dev`. + +### Configuration + +IRedis supports config files. Command-line options will always take precedence +over config. Configuration resolution from highest to lowest precedence is: + +- _Options from command line_ +- `$PWD/.iredisrc` +- `~/.iredisrc` (this path can be changed with `iredis --iredisrc $YOUR_PATH`) +- `/etc/iredisrc` +- default config in IRedis package. + +You can copy the _self-explained_ default config here: + +https://raw.githubusercontent.com/laixintao/iredis/master/iredis/data/iredisrc + +And then make your own changes. + +(If you are using an old versions of IRedis, please use the config file below, +and change the version in URL): + +https://raw.githubusercontent.com/laixintao/iredis/v1.0.4/iredis/data/iredisrc + +### Keys + +IRedis support unix/readline-style REPL keyboard shortcuts, which means keys +like <kbd>Ctrl</kbd> + <kbd>F</kbd> to forward work. + +Also: + +- <kbd>Ctrl</kbd> + <kbd>F</kbd> (i.e. EOF) to exit; you can also use the `exit` + command. +- <kbd>Ctrl</kbd> + <kbd>L</kbd> to clear screen; you can also use the `clear` + command. +- <kbd>Ctrl</kbd> + <kbd>X</kbd> <kbd>Ctrl</kbd> + <kbd>E</kbd> to open an + editor to edit command, or <kbd>V</kbd> in vi-mode. + +## Development + +### Release Strategy + +IRedis is built and released by CircleCI. Whenever a tag is pushed to the +`master` branch, a new release is built and uploaded to pypi.org, it's very +convenient. + +Thus, we release as often as possible, so that users can always enjoy the new +features and bugfixes quickly. Any bugfix or new feature will get at least a +patch release, whereas big features will get a minor release. + +### Setup Environment + +IRedis favors [poetry](https://github.com/sdispater/poetry) as package +management tool. To setup a develop environment on your computer: + +First, install poetry (you can do it in a python's virtualenv): + +``` +pip install poetry +``` + +Then run (which is similar to `pip install -e .`): + +``` +poetry install +``` + +**Be careful running testcases locally, it may flush you db!!!** + +### Development Logs + +This is a command-line tool, so we don't write logs to stdout. + +You can `tail -f ~/.iredis.log` to see logs, the log is pretty clear, you can +see what actually happens from log files. + +### Catch Up with Latest Redis-doc + +IRedis use a git submodule to track current-up-to-date redis-doc version. To +catch up with latest: + +1. Git pull in redis-doc +2. Copy doc files to `/data`: `cp -r redis-doc/commands* iredis/data` +3. Prettier + markdown`prettier --prose-wrap always iredis/data/commands/*.md --write` +4. Check the diff, update IRedis' code if needed. + +## Related Projects + +- [redis-tui](https://github.com/mylxsw/redis-tui) + +If you like iredis, you may also like other cli tools by +[dbcli](https://www.dbcli.com/): + +- [pgcli](https://www.pgcli.com) - Postgres Client with Auto-completion and + Syntax Highlighting +- [mycli](https://www.mycli.net) - MySQL/MariaDB/Percona Client with + Auto-completion and Syntax Highlighting +- [litecli](https://litecli.com) - SQLite Client with Auto-completion and Syntax + Highlighting +- [mssql-cli](https://github.com/dbcli/mssql-cli) - Microsoft SQL Server Client + with Auto-completion and Syntax Highlighting +- [athenacli](https://github.com/dbcli/athenacli) - AWS Athena Client with + Auto-completion and Syntax Highlighting +- [vcli](https://github.com/dbcli/vcli) - VerticaDB client +- [iredis](https://github.com/laixintao/iredis/) - Client for Redis with + AutoCompletion and Syntax Highlighting + +IRedis is build on the top of +[prompt_toolkit](https://github.com/jonathanslenders/python-prompt-toolkit), a +Python library (by [Jonathan Slenders](https://twitter.com/jonathan_s)) for +building rich commandline applications. diff --git a/docs/assets/color.css b/docs/assets/color.css new file mode 100644 index 0000000..077cb37 --- /dev/null +++ b/docs/assets/color.css @@ -0,0 +1,294 @@ +" Vim color file +" +" Author: Tomas Restrepo <tomas@winterdom.com> +" https://github.com/tomasr/molokai +" +" Note: Based on the Monokai theme for TextMate +" by Wimer Hazenberg and its darker variant +" by Hamish Stuart Macpherson +" + +hi clear + +if version > 580 + " no guarantees for version 5.8 and below, but this makes it stop + " complaining + hi clear + if exists("syntax_on") + syntax reset + endif +endif +let g:colors_name="molokai" + +if exists("g:molokai_original") + let s:molokai_original = g:molokai_original +else + let s:molokai_original = 0 +endif + + +hi Boolean guifg=#AE81FF +hi Character guifg=#E6DB74 +hi Number guifg=#AE81FF +hi String guifg=#E6DB74 +hi Conditional guifg=#F92672 gui=bold +hi Constant guifg=#AE81FF gui=bold +hi Cursor guifg=#000000 guibg=#F8F8F0 +hi iCursor guifg=#000000 guibg=#F8F8F0 +hi Debug guifg=#BCA3A3 gui=bold +hi Define guifg=#66D9EF +hi Delimiter guifg=#8F8F8F +hi DiffAdd guibg=#13354A +hi DiffChange guifg=#89807D guibg=#4C4745 +hi DiffDelete guifg=#960050 guibg=#1E0010 +hi DiffText guibg=#4C4745 gui=italic,bold + +hi Directory guifg=#A6E22E gui=bold +hi Error guifg=#E6DB74 +guibg=#1E0010 +hi ErrorMsg guifg=#F92672 +guibg=#232526 gui=bold +hi Exception guifg=#A6E22E gui=bold +hi Float guifg=#AE81FF +hi FoldColumn guifg=#465457 guibg=#000000 +hi Folded guifg=#465457 guibg=#000000 +hi Function guifg=#A6E22E +hi Identifier guifg=#FD971F +hi Ignore guifg=#808080 guibg=bg +hi IncSearch guifg=#C4BE89 guibg=#000000 + +hi Keyword guifg=#F92672 gui=bold +hi Label guifg=#E6DB74 gui=none +hi Macro guifg=#C4BE89 gui=italic +hi SpecialKey guifg=#66D9EF gui=italic + +hi MatchParen guifg=#000000 guibg=#FD971F gui=bold +hi ModeMsg guifg=#E6DB74 +hi MoreMsg guifg=#E6DB74 +hi Operator guifg=#F92672 + +" complete menu +hi Pmenu guifg=#66D9EF guibg=#000000 +hi PmenuSel guibg=#808080 +hi PmenuSbar guibg=#080808 +hi PmenuThumb guifg=#66D9EF + +hi PreCondit guifg=#A6E22E gui=bold +hi PreProc guifg=#A6E22E +hi Question guifg=#66D9EF +hi Repeat guifg=#F92672 gui=bold +hi Search guifg=#000000 guibg=#FFE792 +" marks +hi SignColumn guifg=#A6E22E guibg=#232526 +hi SpecialChar guifg=#F92672 gui=bold +hi SpecialComment guifg=#7E8E91 gui=bold +hi Special guifg=#66D9EF guibg=bg gui=italic +if has("spell") + hi SpellBad guisp=#FF0000 gui=undercurl + hi SpellCap guisp=#7070F0 gui=undercurl + hi SpellLocal guisp=#70F0F0 gui=undercurl + hi SpellRare guisp=#FFFFFF gui=undercurl +endif +hi Statement guifg=#F92672 gui=bold +hi StatusLine guifg=#455354 guibg=fg +hi StatusLineNC guifg=#808080 guibg=#080808 +hi StorageClass guifg=#FD971F gui=italic +hi Structure guifg=#66D9EF +hi Tag guifg=#F92672 gui=italic +hi Title guifg=#ef5939 +hi Todo guifg=#FFFFFF guibg=bg gui=bold + +hi Typedef guifg=#66D9EF +hi Type guifg=#66D9EF gui=none +hi Underlined guifg=#808080 gui=underline + +hi VertSplit guifg=#808080 guibg=#080808 gui=bold +hi VisualNOS guibg=#403D3D +hi Visual guibg=#403D3D +hi WarningMsg guifg=#FFFFFF guibg=#333333 gui=bold +hi WildMenu guifg=#66D9EF guibg=#000000 + +hi TabLineFill guifg=#1B1D1E guibg=#1B1D1E +hi TabLine guibg=#1B1D1E guifg=#808080 gui=none + +if s:molokai_original == 1 + hi Normal guifg=#F8F8F2 guibg=#272822 + hi Comment guifg=#75715E + hi CursorLine guibg=#3E3D32 + hi CursorLineNr guifg=#FD971F gui=none + hi CursorColumn guibg=#3E3D32 + hi ColorColumn guibg=#3B3A32 + hi LineNr guifg=#BCBCBC guibg=#3B3A32 + hi NonText guifg=#75715E + hi SpecialKey guifg=#75715E +else + hi Normal guifg=#F8F8F2 guibg=#1B1D1E + hi Comment guifg=#7E8E91 + hi CursorLine guibg=#293739 + hi CursorLineNr guifg=#FD971F gui=none + hi CursorColumn guibg=#293739 + hi ColorColumn guibg=#232526 + hi LineNr guifg=#465457 guibg=#232526 + hi NonText guifg=#465457 + hi SpecialKey guifg=#465457 +end + +" +" Support for 256-color terminal +" +if &t_Co > 255 + if s:molokai_original == 1 + hi Normal ctermbg=234 + hi CursorLine ctermbg=235 cterm=none + hi CursorLineNr ctermfg=208 cterm=none + else + hi Normal ctermfg=252 ctermbg=233 + hi CursorLine ctermbg=234 cterm=none + hi CursorLineNr ctermfg=208 cterm=none + endif + hi Boolean ctermfg=135 + hi Character ctermfg=144 + hi Number ctermfg=135 + hi String ctermfg=144 + hi Conditional ctermfg=161 cterm=bold + hi Constant ctermfg=135 cterm=bold + hi Cursor ctermfg=16 ctermbg=253 + hi Debug ctermfg=225 cterm=bold + hi Define ctermfg=81 + hi Delimiter ctermfg=241 + + hi DiffAdd ctermbg=24 + hi DiffChange ctermfg=181 ctermbg=239 + hi DiffDelete ctermfg=162 ctermbg=53 + hi DiffText ctermbg=102 cterm=bold + + hi Directory ctermfg=118 cterm=bold + hi Error ctermfg=219 ctermbg=89 + hi ErrorMsg ctermfg=199 ctermbg=16 cterm=bold + hi Exception ctermfg=118 cterm=bold + hi Float ctermfg=135 + hi FoldColumn ctermfg=67 ctermbg=16 + hi Folded ctermfg=67 ctermbg=16 + hi Function ctermfg=118 + hi Identifier ctermfg=208 cterm=none + hi Ignore ctermfg=244 ctermbg=232 + hi IncSearch ctermfg=193 ctermbg=16 + + hi keyword ctermfg=161 cterm=bold + hi Label ctermfg=229 cterm=none + hi Macro ctermfg=193 + hi SpecialKey ctermfg=81 + + hi MatchParen ctermfg=233 ctermbg=208 cterm=bold + hi ModeMsg ctermfg=229 + hi MoreMsg ctermfg=229 + hi Operator ctermfg=161 + + " complete menu + hi Pmenu ctermfg=81 ctermbg=16 + hi PmenuSel ctermfg=255 ctermbg=242 + hi PmenuSbar ctermbg=232 + hi PmenuThumb ctermfg=81 + + hi PreCondit ctermfg=118 cterm=bold + hi PreProc ctermfg=118 + hi Question ctermfg=81 + hi Repeat ctermfg=161 cterm=bold + hi Search ctermfg=0 ctermbg=222 cterm=NONE + + " marks column + hi SignColumn ctermfg=118 ctermbg=235 + hi SpecialChar ctermfg=161 cterm=bold + hi SpecialComment ctermfg=245 cterm=bold + hi Special ctermfg=81 + if has("spell") + hi SpellBad ctermbg=52 + hi SpellCap ctermbg=17 + hi SpellLocal ctermbg=17 + hi SpellRare ctermfg=none ctermbg=none cterm=reverse + endif + hi Statement ctermfg=161 cterm=bold + hi StatusLine ctermfg=238 ctermbg=253 + hi StatusLineNC ctermfg=244 ctermbg=232 + hi StorageClass ctermfg=208 + hi Structure ctermfg=81 + hi Tag ctermfg=161 + hi Title ctermfg=166 + hi Todo ctermfg=231 ctermbg=232 cterm=bold + + hi Typedef ctermfg=81 + hi Type ctermfg=81 cterm=none + hi Underlined ctermfg=244 cterm=underline + + hi VertSplit ctermfg=244 ctermbg=232 cterm=bold + hi VisualNOS ctermbg=238 + hi Visual ctermbg=235 + hi WarningMsg ctermfg=231 ctermbg=238 cterm=bold + hi WildMenu ctermfg=81 ctermbg=16 + + hi Comment ctermfg=59 + hi CursorColumn ctermbg=236 + hi ColorColumn ctermbg=236 + hi LineNr ctermfg=250 ctermbg=236 + hi NonText ctermfg=59 + + hi SpecialKey ctermfg=59 + + if exists("g:rehash256") && g:rehash256 == 1 + hi Normal ctermfg=252 ctermbg=234 + hi CursorLine ctermbg=236 cterm=none + hi CursorLineNr ctermfg=208 cterm=none + + hi Boolean ctermfg=141 + hi Character ctermfg=222 + hi Number ctermfg=141 + hi String ctermfg=222 + hi Conditional ctermfg=197 cterm=bold + hi Constant ctermfg=141 cterm=bold + + hi DiffDelete ctermfg=125 ctermbg=233 + + hi Directory ctermfg=154 cterm=bold + hi Error ctermfg=222 ctermbg=233 + hi Exception ctermfg=154 cterm=bold + hi Float ctermfg=141 + hi Function ctermfg=154 + hi Identifier ctermfg=208 + + hi Keyword ctermfg=197 cterm=bold + hi Operator ctermfg=197 + hi PreCondit ctermfg=154 cterm=bold + hi PreProc ctermfg=154 + hi Repeat ctermfg=197 cterm=bold + + hi Statement ctermfg=197 cterm=bold + hi Tag ctermfg=197 + hi Title ctermfg=203 + hi Visual ctermbg=238 + + hi Comment ctermfg=244 + hi LineNr ctermfg=239 ctermbg=235 + hi NonText ctermfg=239 + hi SpecialKey ctermfg=239 + endif +end + +" Must be at the end, because of ctermbg=234 bug. +" https://groups.google.com/forum/#!msg/vim_dev/afPqwAFNdrU/nqh6tOM87QUJ +set background=dark + + + +[colors] + +bottom-toolbar = 'bg:#222222 #aaaaaa' +bottom-toolbar.off = 'bg:#222222 #888888' +bottom-toolbar.on = 'bg:#222222 #ffffff' + +bottom-toolbar.transaction.valid = 'bg:#222222 #00ff5f bold' +bottom-toolbar.transaction.failed = 'bg:#222222 #ff005f bold' + +# style classes for colored table output +output.header = "#00ff5f bold" +output.odd-row = "" +output.even-row = "" diff --git a/docs/assets/demo.cast b/docs/assets/demo.cast new file mode 100644 index 0000000..07b17b2 --- /dev/null +++ b/docs/assets/demo.cast @@ -0,0 +1,386 @@ +{"version": 2, "width": 66, "height": 20} +[0.0, "o", "\u001b]1337;RemoteHost=laixintao@Chico.local\u0007\u001b]1337;CurrentDir=/Users/laixintao\u0007\u001b]1337;ShellIntegrationVersion=6;shell=zsh\u0007"] +[0.034194, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] +[0.054034, "o", "\u001b]133;D;0\u0007\u001b]1337;RemoteHost=laixintao@Chico.local\u0007\u001b]1337;CurrentDir=/Users/laixintao\u0007"] +[0.05708, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]133;A\u0007$ \u001b]133;B\u0007\u001b[K"] +[0.057424, "o", "\u001b[?1h\u001b="] +[0.057603, "o", "\u001b[?2004h"] +[1.404776, "o", "i"] +[1.709882, "o", "\bir"] +[1.805422, "o", "e"] +[1.957047, "o", "d"] +[2.073349, "o", "i"] +[2.170964, "o", "s"] +[2.308611, "o", "\u001b[?1l\u001b>"] +[2.308783, "o", "\u001b[?2004l\r\r\n"] +[2.311011, "o", "\u001b]133;C;\u0007"] +[2.589501, "o", "\u001b[0m\u001b[?7h\u001b[0miredis 0.8.0\r\r\nredis-server 5.0.6 \r\r\nHome: https://iredis.io\r\r\nIssues: https://iredis.io/issues\u001b[0m\u001b[0m"] +[2.591596, "o", "\u001b[0m\u001b[?7h\u001b[0m\r\r\n\u001b[0m"] +[2.592274, "o", "\u001b[?1l"] +[2.592372, "o", "\u001b[6n"] +[2.599827, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \r\u001b[65C \r\u001b[7A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[2.617441, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \r\u001b[65C \u001b[0m\r\r\n\u001b[0;38;5;248;48;5;235mCtrl-D to exit; \r\u001b[65C \r\u001b[14A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[3.516857, "o", "\u001b[?25l\u001b[?7l\u001b[0mk \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m KEYS \u001b[0;38;5;16;48;5;238m \u001b[A\u001b[8D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[3.522751, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241meys *\u001b[0m \u001b[6D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[3.626927, "o", "\u001b[?25l\u001b[?7l\u001b[0me \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m KEYS\u001b[C \u001b[0;38;5;16;48;5;238m \u001b[A\u001b[8D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[3.632562, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mys *\u001b[0m \u001b[5D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[3.750562, "o", "\u001b[?25l\u001b[?7l\u001b[0my \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m KEYS\u001b[C \u001b[0;38;5;16;48;5;238m \u001b[A\u001b[8D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[3.755718, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241ms *\u001b[0m \u001b[4D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[3.859462, "o", "\u001b[?25l\u001b[?7l\u001b[3D\u001b[0;38;5;28;1mkeys\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m KEYS\u001b[C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;167;48;5;235;1m(generic) \u001b[0;38;5;28;48;5;235;1mKEYS\u001b[0;38;5;71;48;5;235;1m pattern\u001b[0;38;5;136;48;5;235m since: 1.0.0\u001b[0;38;5;241;48;5;235m complexity:O(N) with N bein\r\u001b[65Cg\u001b[14A\r\u001b[20C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[3.864549, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m *\u001b[0m \u001b[3D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[4.112384, "o", "\u001b[?25l\u001b[?7l\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[A\u001b[20C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[4.121805, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m*\u001b[0m \u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[4.49726, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71;1m*\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[4.503062, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[4.796083, "o", "\u001b[?25l\u001b[?7l\u001b[22D\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0;38;5;28;1mkeys\u001b[0m \u001b[0;38;5;71;1m*\u001b[0m \r\u001b[65C \r\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] +[4.80137, "o", "\u001b[0m\u001b[?7h\u001b[0m 1)\u001b[0m \u001b[0;38;5;71m\"myzset\"\u001b[0m\r\r\n\u001b[0m 2)\u001b[0m \u001b[0;38;5;71m\"mylist1\"\u001b[0m\r\r\n\u001b[0m 3)\u001b[0m \u001b[0;38;5;71m\"abc\"\u001b[0m\r\r\n\u001b[0m 4)\u001b[0m \u001b[0;38;5;71m\"a\"\u001b[0m\r\r\n\u001b[0m 5)\u001b[0m \u001b[0;38;5;71m\"mstream\"\u001b[0m\r\r\n\u001b[0m 6)\u001b[0m \u001b[0;38;5;71m\"testKeyDB2\"\u001b[0m\r\r\n\u001b[0m 7)\u001b[0m \u001b[0;38;5;71m\"list:restaurant\"\u001b[0m\r\r\n\u001b[0m 8)\u001b[0m \u001b[0;38;5;71m\"Sicily\"\u001b[0m\r\r\n\u001b[0m 9)\u001b[0m \u001b[0;38;5;71m\"cars\"\u001b[0m\r\r\n\u001b[0m10)\u001b[0m \u001b[0;38;5;71m\"hash1\"\u001b[0m\r\r\n\u001b[0m11)\u001b[0m \u001b[0;38;5;71m\"list:buildings\"\u001b[0m\r\r\n\u001b[0m12)\u001b[0m \u001b[0;38;5;71m\"hash3\"\u001b[0m\r\r\n\u001b[0m13)\u001b[0m \u001b[0;38;5;71m\"fooset\"\u001b[0m\r\r\n\u001b[0m14)\u001b[0m \u001b[0;38;5;71m\"foo\"\u001b[0m\r\r\n\u001b[0m15)\u001b[0m \u001b[0;38;5;71m\"myset\"\u001b[0m\r\r\n\u001b[0m16)\u001b[0m \u001b[0;38;5;71m\"hash2\"\u001b[0m\r\r\n\u001b[0m17)\u001b[0m \u001b[0;38;5;71m\"list:animals\"\u001b[0m\r\r\n\u001b[0m18)\u001b[0m \u001b[0;38;5;71m\"af\"\u001b[0m\r\r\n\u001b[0m19)\u001b[0m \u001b[0;38;5;71m\"somestream\"\u001b[0m\r\r\n\u001b[0m20)\u001b[0m \u001b[0;38;5;71m\"kkk\"\u001b[0m\u001b[0m"] +[4.803285, "o", "\u001b[0m\u001b[?7h\u001b[0m\r\r\n\u001b[0m"] +[4.804015, "o", "\u001b[?1l"] +[4.804128, "o", "\u001b[6n"] +[4.807349, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \r\u001b[65C \r\u001b[7A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[4.812015, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;248;48;5;235mCtrl-D to exit; \r\u001b[65C \r\u001b[8A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[5.558691, "o", "\u001b[?25l\u001b[?7l\u001b[0mt \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TTL \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TIME \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TYPE \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TOUCH \u001b[0;38;5;16;48;5;238m \u001b[4A\u001b[8D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[5.562825, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mype myset\u001b[0m \u001b[10D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[5.802861, "o", "\u001b[?25l\u001b[?7l\u001b[0my \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m TYPE \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[4A\u001b[17C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[5.807952, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mpe myset\u001b[0m \u001b[9D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[6.011176, "o", "\u001b[?25l\u001b[?7l\u001b[0mp \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m TYPE\u001b[C \u001b[0;38;5;16;48;5;238m \u001b[A\u001b[8D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[6.01494, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241me myset\u001b[0m \u001b[8D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[6.1425, "o", "\u001b[?25l\u001b[?7l\u001b[3D\u001b[0;38;5;28;1mtype\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m TYPE\u001b[C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;167;48;5;235;1m(generic) \u001b[0;38;5;28;48;5;235;1mTYPE\u001b[0;38;5;71;48;5;235m key\u001b[0;38;5;136;48;5;235m since: 1.0.0\u001b[0;38;5;241;48;5;235m complexity:O(1)\u001b[8A\u001b[29D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[6.14741, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m myset\u001b[0m \u001b[7D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[6.244466, "o", "\u001b[?25l\u001b[?7l\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m kkk \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m somestream \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m af \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m list:animals \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m hash2 \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m myset \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m foo \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[6.253095, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mmyset\u001b[0m \u001b[6D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[6.712731, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mm\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mm\u001b[0;38;5;238;48;5;30myset\u001b[8C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mm\u001b[0;38;5;238;48;5;30mstream\u001b[0;38;5;231;48;5;30m \u001b[4C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mm\u001b[0;38;5;238;48;5;30mylist1\u001b[6C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mm\u001b[0;38;5;238;48;5;30myzset\u001b[0;38;5;231;48;5;30m \u001b[2C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mso\u001b[0;38;5;231;48;5;30;1;4mm\u001b[0;38;5;238;48;5;30mestream\u001b[3C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mlist:ani\u001b[0;38;5;231;48;5;30;1;4mm\u001b[0;38;5;238;48;5;30mals\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[7A\u001b[17D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[6.718052, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241myset\u001b[0m \u001b[5D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[9.031011, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71my\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[22C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mmy\u001b[0;38;5;238;48;5;30mset\u001b[3C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[22C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mmy\u001b[0;38;5;238;48;5;30mlist1\u001b[C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[22C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mmy\u001b[0;38;5;238;48;5;30mzset\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[6A\u001b[22C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[9.048076, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mset\u001b[0m \u001b[4D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[9.337635, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mset\u001b[0m\r\r\n\u001b[23C\u001b[0;38;5;16;48;5;231m \u001b[0;38;5;16;48;5;231;4mmy\u001b[0;48;5;231mset\u001b[0;38;5;16;48;5;231m \u001b[A\u001b[6D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[9.341531, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[9.587591, "o", "\u001b[?25l\u001b[?7l\u001b[26D\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0;38;5;28;1mtype\u001b[0m \u001b[0;38;5;71mmyset\u001b[0m \r\u001b[65C \r\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] +[9.592731, "o", "\u001b[0m\u001b[?7h\u001b[0m\"set\"\u001b[0m\u001b[0m"] +[9.595501, "o", "\u001b[0m\u001b[?7h\u001b[0m\r\r\n\u001b[0m"] +[9.596128, "o", "\u001b[?1l"] +[9.59625, "o", "\u001b[6n"] +[9.598996, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \r\u001b[65C \r\u001b[7A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[9.602329, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;248;48;5;235mCtrl-D to exit; \r\u001b[65C \r\u001b[8A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[10.630148, "o", "\u001b[?25l\u001b[?7l\u001b[0ms \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m SET \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m SREM \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m SPOP \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m SADD \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m SYNC \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m SAVE \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m SORT \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[16D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[10.635184, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241met foo bar\u001b[0m \u001b[11D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[11.02782, "o", "\u001b[?25l\u001b[?7l\u001b[0mm \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m SMOVE\u001b[4C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m SMEMBERS \u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[7A\u001b[15D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[11.031124, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241members fooset\u001b[0m \u001b[14D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[11.103554, "o", "\u001b[?25l\u001b[?7l\u001b[0me \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m SMEMBERS \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[2A\u001b[18C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[11.107347, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mmbers fooset\u001b[0m \u001b[13D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[11.198052, "o", "\u001b[?25l\u001b[?7l\u001b[0mm \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m SMEMBERS \u001b[0;38;5;16;48;5;238m \u001b[A\u001b[11D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[11.201012, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mbers fooset\u001b[0m \u001b[12D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[11.568186, "o", "\u001b[?25l\u001b[?7l\u001b[4D\u001b[0;38;5;28;1mSMEMBERS\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[20C\u001b[0;38;5;16;48;5;231m SMEMBERS \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;167;48;5;235;1m(set) \u001b[0;38;5;28;48;5;235;1mSMEMBERS\u001b[0;38;5;71;48;5;235m key\u001b[0;38;5;136;48;5;235m since: 1.0.0\u001b[0;38;5;241;48;5;235m complexity:O(N) where N is the \r\u001b[65Cs\u001b[8A\r\u001b[24C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[11.572482, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[11.824354, "o", "\u001b[?25l\u001b[?7l\u001b[C\u001b[0m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m myset \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[25C\u001b[0;38;5;231;48;5;30m kkk \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[25C\u001b[0;38;5;231;48;5;30m somestream \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[25C\u001b[0;38;5;231;48;5;30m af \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[25C\u001b[0;38;5;231;48;5;30m list:animals \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[25C\u001b[0;38;5;231;48;5;30m hash2 \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[25C\u001b[0;38;5;231;48;5;30m foo \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[11.828823, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mmyset\u001b[0m \u001b[6D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[12.363643, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mm\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[25C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mm\u001b[0;38;5;238;48;5;30myset\u001b[8C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[25C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mm\u001b[0;38;5;238;48;5;30mstream\u001b[6C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[25C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mm\u001b[0;38;5;238;48;5;30mylist1\u001b[0;38;5;231;48;5;30m \u001b[4C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[25C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mm\u001b[0;38;5;238;48;5;30myzset\u001b[7C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[25C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mso\u001b[0;38;5;231;48;5;30;1;4mm\u001b[0;38;5;238;48;5;30mestream\u001b[0;38;5;231;48;5;30m \u001b[2C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[25C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mlist:ani\u001b[0;38;5;231;48;5;30;1;4mm\u001b[0;38;5;238;48;5;30mals\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[25C\u001b[0m \u001b[7A\u001b[17D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[12.368073, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241myset\u001b[0m \u001b[5D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[12.639026, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71my\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[26C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mmy\u001b[0;38;5;238;48;5;30mset\u001b[3C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[26C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mmy\u001b[0;38;5;238;48;5;30mlist1\u001b[C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[26C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mmy\u001b[0;38;5;238;48;5;30mzset\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[6A\u001b[26C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[12.642963, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mset\u001b[0m \u001b[4D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[12.84344, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mset\u001b[0m\r\r\n\u001b[27C\u001b[0;38;5;16;48;5;231m \u001b[0;38;5;16;48;5;231;4mmy\u001b[0;48;5;231mset\u001b[0;38;5;16;48;5;231m \u001b[A\u001b[6D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[12.847415, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[13.250149, "o", "\u001b[?25l\u001b[?7l\u001b[30D\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0;38;5;28;1mSMEMBERS\u001b[0m \u001b[0;38;5;71mmyset\u001b[0m \r\u001b[65C \r\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] +[13.255777, "o", "\u001b[0m\u001b[?7h\u001b[0m1)\u001b[0m \u001b[0;38;5;208m\"foo\"\u001b[0m\r\r\n\u001b[0m2)\u001b[0m \u001b[0;38;5;208m\"world\"\u001b[0m\r\r\n\u001b[0m3)\u001b[0m \u001b[0;38;5;208m\"bar\"\u001b[0m\r\r\n\u001b[0m4)\u001b[0m \u001b[0;38;5;208m\"hello\"\u001b[0m\u001b[0m"] +[13.257576, "o", "\u001b[0m\u001b[?7h\u001b[0m\r\r\n\u001b[0m"] +[13.25837, "o", "\u001b[?1l\u001b[6n"] +[13.260811, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \r\u001b[65C \r\u001b[7A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[13.264088, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;248;48;5;235mCtrl-D to exit; \r\u001b[65C \r\u001b[8A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[15.018012, "o", "\u001b[?25l\u001b[?7l\u001b[0mt \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TTL \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TIME \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TYPE \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TOUCH \u001b[0;38;5;16;48;5;238m \u001b[4A\u001b[8D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[15.021342, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mype myset\u001b[0m \u001b[10D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[15.136615, "o", "\u001b[?25l\u001b[?7l\u001b[0my \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m TYPE \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[4A\u001b[17C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[15.139872, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mpe myset\u001b[0m \u001b[9D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[15.301704, "o", "\u001b[?25l\u001b[?7l\u001b[0mp \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m TYPE\u001b[C \u001b[0;38;5;16;48;5;238m \u001b[A\u001b[8D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[15.305109, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241me myset\u001b[0m \u001b[8D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[15.381888, "o", "\u001b[?25l\u001b[?7l\u001b[3D\u001b[0;38;5;28;1mtype\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m TYPE\u001b[C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;167;48;5;235;1m(generic) \u001b[0;38;5;28;48;5;235;1mTYPE\u001b[0;38;5;71;48;5;235m key\u001b[0;38;5;136;48;5;235m since: 1.0.0\u001b[0;38;5;241;48;5;235m complexity:O(1)\u001b[8A\u001b[29D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[15.387144, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m myset\u001b[0m \u001b[7D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[15.493325, "o", "\u001b[?25l\u001b[?7l\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m myset \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m kkk \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m somestream \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m af \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m list:animals \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m hash2 \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m foo \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[15.498349, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mmyset\u001b[0m \u001b[6D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[15.792526, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mc\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mc\u001b[0;38;5;238;48;5;30mars\u001b[3C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mSi\u001b[0;38;5;231;48;5;30;1;4mc\u001b[0;38;5;238;48;5;30mily\u001b[C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mab\u001b[0;38;5;231;48;5;30;1;4mc\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[7A\u001b[17D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[15.797662, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mars\u001b[0m \u001b[4D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[15.880584, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71ma\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[22C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mca\u001b[0;38;5;238;48;5;30mrs\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[3A\u001b[22C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[15.885051, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mrs\u001b[0m \u001b[3D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[16.48149, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mrs\u001b[0m\r\r\n\u001b[23C\u001b[0;38;5;16;48;5;231m \u001b[0;38;5;16;48;5;231;4mca\u001b[0;48;5;231mrs\u001b[0;38;5;16;48;5;231m \u001b[A\u001b[5D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[16.486215, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[16.848883, "o", "\u001b[?25l\u001b[?7l\u001b[25D\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0;38;5;28;1mtype\u001b[0m \u001b[0;38;5;71mcars\u001b[0m \r\u001b[65C \r\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] +[16.852972, "o", "\u001b[0m\u001b[?7h\u001b[0m\"zset\"\u001b[0m\u001b[0m"] +[16.854529, "o", "\u001b[0m\u001b[?7h\u001b[0m\r\r\n\u001b[0m"] +[16.855119, "o", "\u001b[?1l\u001b[6n"] +[16.858567, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \r\u001b[65C \r\u001b[7A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[16.862469, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;248;48;5;235mCtrl-D to exit; \r\u001b[65C \r\u001b[8A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[17.715952, "o", "\u001b[?25l\u001b[?7l\u001b[0mz \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m ZREM \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m ZADD \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m ZSCAN \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m ZRANK \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m ZCARD \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m ZSCORE \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m ZRANGE \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[19D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[17.72013, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mscan kkk 0\u001b[0m \u001b[11D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[17.977719, "o", "\u001b[?25l\u001b[?7l\u001b[0ms \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m ZSCAN\u001b[2C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m ZSCORE \u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[17.981853, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mcan kkk 0\u001b[0m \u001b[10D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[18.172029, "o", "\u001b[?25l\u001b[?7l\u001b[0mc \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m ZSCAN\u001b[C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m ZSCORE \u001b[0;38;5;16;48;5;238m \u001b[2A\u001b[9D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[18.176161, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241man kkk 0\u001b[0m \u001b[9D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[18.555927, "o", "\u001b[?25l\u001b[?7l\u001b[3D\u001b[0;38;5;28;1mZSCAN\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[19C\u001b[0;38;5;16;48;5;231m ZSCAN \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;167;48;5;235;1m(sorted_set) \u001b[0;38;5;28;48;5;235;1mZSCAN\u001b[0;38;5;71;48;5;235m key\u001b[0;38;5;141;48;5;235m cursor\u001b[0;38;5;28;48;5;235;1m [MATCH\u001b[0;38;5;71;48;5;235;1m pattern\u001b[0;38;5;28;48;5;235;1m] [COUNT\u001b[0;38;5;141;48;5;235m count\u001b[0;38;5;28;48;5;235;1m]\u001b[0;38;5;136;48;5;235m sin\r\u001b[65Cc\u001b[8A\r\u001b[21C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[18.5603, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[18.890406, "o", "\u001b[?25l\u001b[?7l\u001b[C\u001b[0m \u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m cars \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m myset \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[22C\u001b[0;38;5;231;48;5;30m kkk \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[22C\u001b[0;38;5;231;48;5;30m somestream \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[22C\u001b[0;38;5;231;48;5;30m af \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[22C\u001b[0;38;5;231;48;5;30m list:animals \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[22C\u001b[0;38;5;231;48;5;30m hash2 \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[18.895303, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mcars 0\u001b[0m \u001b[7D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[19.875157, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mcars\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[22C\u001b[0;38;5;16;48;5;231m cars \u001b[A\u001b[13D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[19.880817, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[20.080926, "o", "\u001b[?25l\u001b[?7l\u001b[26D\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0;38;5;28;1mZSCAN\u001b[0m \u001b[0;38;5;71mcars\u001b[0m \r\u001b[65C \r\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] +[20.089992, "o", "\u001b[0m\u001b[?7h\u001b[0;38;5;102m(error) \u001b[0;38;5;197;1mwrong number of arguments for 'zscan' command\u001b[0m\u001b[0m"] +[20.091761, "o", "\u001b[0m\u001b[?7h\u001b[0m\r\r\n\u001b[0m"] +[20.092544, "o", "\u001b[?1l\u001b[6n"] +[20.101137, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \r\u001b[65C \r\u001b[7A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[20.109017, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;248;48;5;235mCtrl-D to exit; \r\u001b[65C \r\u001b[8A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[21.330573, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;28;1mZSCAN\u001b[0m \u001b[0;38;5;71mcars\u001b[0m \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;167;48;5;235;1m(sorted_set) \u001b[0;38;5;28;48;5;235;1mZSCAN\u001b[0;38;5;71;48;5;235m key\u001b[0;38;5;141;48;5;235m cursor\u001b[0;38;5;28;48;5;235;1m [MATCH\u001b[0;38;5;71;48;5;235;1m pattern\u001b[0;38;5;28;48;5;235;1m] [COUNT\u001b[0;38;5;141;48;5;235m count\u001b[0;38;5;28;48;5;235;1m]\u001b[0;38;5;136;48;5;235m sin\r\u001b[65Cc\u001b[8A\r\u001b[26C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[21.340066, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[21.796556, "o", "\u001b[?25l\u001b[?7l\u001b[C\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[21.800207, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m0\u001b[0m \u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[22.451791, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;141m0\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[22.460219, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[23.089162, "o", "\u001b[?25l\u001b[?7l\u001b[28D\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0;38;5;28;1mZSCAN\u001b[0m \u001b[0;38;5;71mcars\u001b[0m \u001b[0;38;5;141m0\u001b[0m \r\u001b[65C \r\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] +[23.100459, "o", "\u001b[0m\u001b[?7h\u001b[0;38;5;102m(cursor) \u001b[0;38;5;141m0\u001b[0m\r\r\n\u001b[0m1)\u001b[0m \u001b[0;38;5;141m1367522908124000 \u001b[0;38;5;208m\"robins-car\"\u001b[0m\u001b[0m"] +[23.102113, "o", "\u001b[0m\u001b[?7h\u001b[0m\r\r\n\u001b[0m"] +[23.102697, "o", "\u001b[?1l"] +[23.102802, "o", "\u001b[6n"] +[23.105131, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \r\u001b[65C \r\u001b[7A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[23.108162, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;248;48;5;235mCtrl-D to exit; \r\u001b[65C \r\u001b[8A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[25.209658, "o", "\u001b[?25l\u001b[?7l\u001b[0mt \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TTL \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TIME \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TYPE \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TOUCH \u001b[0;38;5;16;48;5;238m \u001b[4A\u001b[8D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[25.218301, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mype cars\u001b[0m \u001b[9D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[25.35018, "o", "\u001b[?25l\u001b[?7l\u001b[0my \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m TYPE \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[4A\u001b[17C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[25.354149, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mpe cars\u001b[0m \u001b[8D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[25.529032, "o", "\u001b[?25l\u001b[?7l\u001b[0mp \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m TYPE\u001b[C \u001b[0;38;5;16;48;5;238m \u001b[A\u001b[8D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[25.533149, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241me cars\u001b[0m \u001b[7D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[25.630584, "o", "\u001b[?25l\u001b[?7l\u001b[3D\u001b[0;38;5;28;1mtype\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m TYPE\u001b[C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;167;48;5;235;1m(generic) \u001b[0;38;5;28;48;5;235;1mTYPE\u001b[0;38;5;71;48;5;235m key\u001b[0;38;5;136;48;5;235m since: 1.0.0\u001b[0;38;5;241;48;5;235m complexity:O(1)\u001b[8A\u001b[29D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[25.634871, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m cars\u001b[0m \u001b[6D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[25.742093, "o", "\u001b[?25l\u001b[?7l\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m cars \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m myset \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m kkk \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m somestream \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m af \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m list:animals \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m hash2 \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[25.750379, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mcars\u001b[0m \u001b[5D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[25.984703, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71ml\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30mist:animals\u001b[3C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30mist:buildings\u001b[C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30mist:restaurant\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mmy\u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30mist1\u001b[0;38;5;231;48;5;30m \u001b[6C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mSici\u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30my\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[7A\u001b[17D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[25.988778, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mist:animals\u001b[0m \u001b[12D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[26.206791, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mi\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[22C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mli\u001b[0;38;5;238;48;5;30mst:animals\u001b[3C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[22C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mli\u001b[0;38;5;238;48;5;30mst:buildings\u001b[C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[22C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mli\u001b[0;38;5;238;48;5;30mst:restaurant\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[22C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mmy\u001b[0;38;5;231;48;5;30;1;4mli\u001b[0;38;5;238;48;5;30mst1\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[5A\u001b[22C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[26.213146, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mst:animals\u001b[0m \u001b[11D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[26.905529, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mst:animals\u001b[0m\r\r\n\u001b[23C\u001b[0;38;5;16;48;5;231m \u001b[0;38;5;16;48;5;231;4mli\u001b[0;48;5;231mst:animals\u001b[0;38;5;16;48;5;231m \u001b[A\u001b[7D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[26.915375, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[27.735947, "o", "\u001b[?25l\u001b[?7l\u001b[33D\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0;38;5;28;1mtype\u001b[0m \u001b[0;38;5;71mlist:animals\u001b[0m \r\u001b[65C \r\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] +[27.740639, "o", "\u001b[0m\u001b[?7h\u001b[0m\"list\"\u001b[0m\u001b[0m"] +[27.743095, "o", "\u001b[0m\u001b[?7h\u001b[0m\r\r\n\u001b[0m"] +[27.743675, "o", "\u001b[?1l"] +[27.74377, "o", "\u001b[6n"] +[27.746106, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \r\u001b[65C \r\u001b[7A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[27.750128, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;248;48;5;235mCtrl-D to exit; \r\u001b[65C \r\u001b[8A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[28.104759, "o", "\u001b[?25l\u001b[?7l\u001b[0ml \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m LSET \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m LREM \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m LPOP \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m LLEN \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m LTRIM \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m LPUSH \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m LRANGE \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[11D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[28.108111, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mrange list:animals 0 6\u001b[0m \u001b[23D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[28.254897, "o", "\u001b[?25l\u001b[?7l\u001b[0ml \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m LLEN \u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[7A\u001b[10D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[28.259097, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241men testKeyDB2\u001b[0m \u001b[14D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[28.758049, "o", "\u001b[?25l\u001b[?7l\u001b[2D\u001b[0;38;5;28;1mLLEN\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0;38;5;16;48;5;231m LLEN \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;167;48;5;235;1m(list) \u001b[0;38;5;28;48;5;235;1mLLEN\u001b[0;38;5;71;48;5;235m key\u001b[0;38;5;136;48;5;235m since: 1.0.0\u001b[0;38;5;241;48;5;235m complexity:O(1)\u001b[8A\u001b[26D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[28.762078, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[28.981063, "o", "\u001b[?25l\u001b[?7l\u001b[C\u001b[0m \u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m list:animals \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m cars \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m myset \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m kkk \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m somestream \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m af \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m hash2 \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[28.987768, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mlist:animals\u001b[0m \u001b[13D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[29.889228, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mi\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30ml\u001b[0;38;5;231;48;5;30;1;4mi\u001b[0;38;5;238;48;5;30mst:animals\u001b[3C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30ml\u001b[0;38;5;231;48;5;30;1;4mi\u001b[0;38;5;238;48;5;30mst:buildings\u001b[C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mS\u001b[0;38;5;231;48;5;30;1;4mi\u001b[0;38;5;238;48;5;30mcily\u001b[9C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30ml\u001b[0;38;5;231;48;5;30;1;4mi\u001b[0;38;5;238;48;5;30mst:restaurant\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mmyl\u001b[0;38;5;231;48;5;30;1;4mi\u001b[0;38;5;238;48;5;30mst1\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[7A\u001b[17D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[29.895168, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[30.516043, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[5A\u001b[20C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[30.520459, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[30.845385, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71ml\u001b[0m \u001b[0m\r\r\n\u001b[22C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30mist:animals\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[22C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30mist:buildings\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[22C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30mist:restaurant\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[22C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mmy\u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30mist1\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[22C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mSici\u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30my\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[5A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[30.84925, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mist:animals\u001b[0m \u001b[12D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[31.051905, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mi\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[22C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mli\u001b[0;38;5;238;48;5;30mst:animals\u001b[3C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[22C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mli\u001b[0;38;5;238;48;5;30mst:buildings\u001b[C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[22C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4mli\u001b[0;38;5;238;48;5;30mst:restaurant\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[22C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mmy\u001b[0;38;5;231;48;5;30;1;4mli\u001b[0;38;5;238;48;5;30mst1\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[5A\u001b[22C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[31.055859, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mst:animals\u001b[0m \u001b[11D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[31.292784, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mst:animals\u001b[0m\r\r\n\u001b[23C\u001b[0;38;5;16;48;5;231m \u001b[0;38;5;16;48;5;231;4mli\u001b[0;48;5;231mst:animals\u001b[0;38;5;16;48;5;231m \u001b[A\u001b[7D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[31.296722, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[32.658483, "o", "\u001b[?25l\u001b[?7l\u001b[33D\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0;38;5;28;1mLLEN\u001b[0m \u001b[0;38;5;71mlist:animals\u001b[0m \r\u001b[65C \r\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] +[32.662927, "o", "\u001b[0m\u001b[?7h\u001b[0;38;5;102m(integer) \u001b[0m51\u001b[0m\u001b[0m"] +[32.664937, "o", "\u001b[0m\u001b[?7h\u001b[0m\r\r\n\u001b[0m"] +[32.665552, "o", "\u001b[?1l"] +[32.665646, "o", "\u001b[6n"] +[32.66873, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \r\u001b[65C \r\u001b[7A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[32.671953, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;248;48;5;235mCtrl-D to exit; \r\u001b[65C \r\u001b[8A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[35.303978, "o", "\u001b[?25l\u001b[?7l\u001b[0ml \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m LSET \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m LREM \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m LPOP \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m LLEN \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m LTRIM \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m LPUSH \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m LRANGE \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[11D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[35.320666, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mrange list:animals 0 6\u001b[0m \u001b[23D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[35.652587, "o", "\u001b[?25l\u001b[?7l\u001b[0mr \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m LREM\u001b[3C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m LRANGE \u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[7A\u001b[10D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[35.656638, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mange list:animals 0 6\u001b[0m \u001b[22D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[35.741188, "o", "\u001b[?25l\u001b[?7l\u001b[0ma \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m LRANGE \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[2A\u001b[18C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[35.744004, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mnge list:animals 0 6\u001b[0m \u001b[21D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[35.819735, "o", "\u001b[?25l\u001b[?7l\u001b[0mn \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m LRANGE \u001b[0;38;5;16;48;5;238m \u001b[A\u001b[9D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[35.823559, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mge list:animals 0 6\u001b[0m \u001b[20D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[35.956968, "o", "\u001b[?25l\u001b[?7l\u001b[0mg \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m LRANGE \u001b[0;38;5;16;48;5;238m \u001b[A\u001b[9D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[35.960667, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241me list:animals 0 6\u001b[0m \u001b[19D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[36.069943, "o", "\u001b[?25l\u001b[?7l\u001b[5D\u001b[0;38;5;28;1mlrange\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m LRANGE \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;167;48;5;235;1m(list) \u001b[0;38;5;28;48;5;235;1mLRANGE\u001b[0;38;5;71;48;5;235m key\u001b[0;38;5;141;48;5;235m start stop\u001b[0;38;5;136;48;5;235m since: 1.0.0\u001b[0;38;5;241;48;5;235m complexity:O(S+N) whe\r\u001b[65Cr\u001b[8A\r\u001b[22C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[36.076669, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m list:animals 0 6\u001b[0m \u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[36.209277, "o", "\u001b[?25l\u001b[?7l\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[22C\u001b[0m \u001b[0;38;5;231;48;5;30m list:animals \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[23C\u001b[0;38;5;231;48;5;30m cars \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[23C\u001b[0;38;5;231;48;5;30m myset \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[23C\u001b[0;38;5;231;48;5;30m kkk \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[23C\u001b[0;38;5;231;48;5;30m somestream \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[23C\u001b[0;38;5;231;48;5;30m af \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[23C\u001b[0;38;5;231;48;5;30m hash2 \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[36.216403, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mlist:animals 0 6\u001b[0m \u001b[17D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[37.3095, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71ml\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[23C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30mist:animals\u001b[3C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[23C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30mist:buildings\u001b[C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[23C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30mist:restaurant\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[23C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mmy\u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30mist1\u001b[8C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[23C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mSici\u001b[0;38;5;231;48;5;30;1;4ml\u001b[0;38;5;238;48;5;30my\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[23C\u001b[0m \u001b[7A\u001b[17D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[37.319061, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mist:animals 0 6\u001b[0m \u001b[16D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[37.965239, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mist:animals\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[24C\u001b[0;38;5;16;48;5;231m \u001b[0;38;5;16;48;5;231;4ml\u001b[0;48;5;231mist:animals\u001b[0;38;5;16;48;5;231m \u001b[A\u001b[6D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[37.96939, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[38.184492, "o", "\u001b[?25l\u001b[?7l\u001b[C\u001b[0m \u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[5A\u001b[35C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[38.189811, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m0 6\u001b[0m \u001b[4D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[38.809319, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;141m0\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[38.813992, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m 6\u001b[0m \u001b[3D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[38.969048, "o", "\u001b[?25l\u001b[?7l\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[38.973767, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m6\u001b[0m \u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[41.005131, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;141m6\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[41.015952, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[41.427252, "o", "\u001b[?25l\u001b[?7l\u001b[39D\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0;38;5;28;1mlrange\u001b[0m \u001b[0;38;5;71mlist:animals\u001b[0m \u001b[0;38;5;141m0\u001b[0m \u001b[0;38;5;141m6\u001b[0m \r\u001b[65C \r\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] +[41.437426, "o", "\u001b[0m\u001b[?7h\u001b[0m1)\u001b[0m \u001b[0;38;5;208m\"wolf\"\u001b[0m\r\r\n\u001b[0m2)\u001b[0m \u001b[0;38;5;208m\"turtle\"\u001b[0m\r\r\n\u001b[0m3)\u001b[0m \u001b[0;38;5;208m\"tiger\"\u001b[0m\r\r\n\u001b[0m4)\u001b[0m \u001b[0;38;5;208m\"squirrel\"\u001b[0m\r\r\n\u001b[0m5)\u001b[0m \u001b[0;38;5;208m\"spider\"\u001b[0m\r\r\n\u001b[0m6)\u001b[0m \u001b[0;38;5;208m\"snake\"\u001b[0m\r\r\n\u001b[0m7)\u001b[0m \u001b[0;38;5;208m\"snail\"\u001b[0m\u001b[0m"] +[41.439085, "o", "\u001b[0m\u001b[?7h\u001b[0m\r\r\n\u001b[0m"] +[41.439711, "o", "\u001b[?1l"] +[41.439807, "o", "\u001b[6n"] +[41.444224, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \r\u001b[65C \r\u001b[7A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[41.447447, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;248;48;5;235mCtrl-D to exit; \r\u001b[65C \r\u001b[8A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[45.439479, "o", "\u001b[?25l\u001b[?7l\u001b[0mt \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TTL \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TIME \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TYPE \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m TOUCH \u001b[0;38;5;16;48;5;238m \u001b[4A\u001b[8D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[45.444328, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mype list:animals\u001b[0m \u001b[17D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[45.592128, "o", "\u001b[?25l\u001b[?7l\u001b[0my \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m TYPE \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[4A\u001b[17C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[45.59538, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mpe list:animals\u001b[0m \u001b[16D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[45.812073, "o", "\u001b[?25l\u001b[?7l\u001b[0mp \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m TYPE\u001b[C \u001b[0;38;5;16;48;5;238m \u001b[A\u001b[8D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[45.816301, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241me list:animals\u001b[0m \u001b[15D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[45.953551, "o", "\u001b[?25l\u001b[?7l\u001b[3D\u001b[0;38;5;28;1mtype\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m TYPE\u001b[C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;167;48;5;235;1m(generic) \u001b[0;38;5;28;48;5;235;1mTYPE\u001b[0;38;5;71;48;5;235m key\u001b[0;38;5;136;48;5;235m since: 1.0.0\u001b[0;38;5;241;48;5;235m complexity:O(1)\u001b[8A\u001b[29D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[45.958932, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m list:animals\u001b[0m \u001b[14D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[46.676064, "o", "\u001b[?25l\u001b[?7l\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m list:animals \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m cars \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m myset \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m kkk \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m somestream \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m af \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m hash2 \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[46.68263, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mlist:animals\u001b[0m \u001b[13D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[46.908434, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mlist:animals\u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;16;48;5;231m list:animals \u001b[A\u001b[5D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[46.912994, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[47.594674, "o", "\u001b[?25l\u001b[?7l\u001b[12D\u001b[0;38;5;71mcars\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m list:animals \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;16;48;5;231m cars \u001b[2A\u001b[13D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[47.599769, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[48.036896, "o", "\u001b[?25l\u001b[?7l\u001b[4D\u001b[0;38;5;71mmyset\u001b[0m \u001b[0m\r\r\n\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m cars \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;16;48;5;231m myset \u001b[3A\u001b[12D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[48.041407, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[48.555102, "o", "\u001b[?25l\u001b[?7l\u001b[5D\u001b[0;38;5;71mkkk\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\r\r\n\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m myset \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;16;48;5;231m kkk \u001b[4A\u001b[14D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[48.559223, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[48.744603, "o", "\u001b[?25l\u001b[?7l\u001b[3D\u001b[0;38;5;71msomestream\u001b[0m \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m kkk \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;16;48;5;231m somestream \u001b[5A\u001b[7D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[48.751509, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[48.895622, "o", "\u001b[?25l\u001b[?7l\u001b[10D\u001b[0;38;5;71maf\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[21C\u001b[0;38;5;231;48;5;30m somestream \u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;16;48;5;231m af \u001b[6A\u001b[15D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[48.900841, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[50.400124, "o", "\u001b[?25l\u001b[?7l\u001b[23D\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0;38;5;28;1mtype\u001b[0m \u001b[0;38;5;71maf\u001b[0m \r\u001b[65C \r\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] +[50.41034, "o", "\u001b[0m\u001b[?7h\u001b[0m\"string\"\u001b[0m\u001b[0m"] +[50.412186, "o", "\u001b[0m\u001b[?7h\u001b[0m\r\r\n\u001b[0m"] +[50.41283, "o", "\u001b[?1l"] +[50.41295, "o", "\u001b[6n"] +[50.415152, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \r\u001b[65C \r\u001b[7A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[50.419664, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;248;48;5;235mCtrl-D to exit; \r\u001b[65C \r\u001b[8A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[51.542536, "o", "\u001b[?25l\u001b[?7l\u001b[0mg \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m GET \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m GETSET \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m GETBIT \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m GEOPOS \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m GEOADD \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m GEOHASH \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m GEODIST \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[20D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[51.555077, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241met foo\u001b[0m \u001b[7D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[51.630144, "o", "\u001b[?25l\u001b[?7l\u001b[0me \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m GET\u001b[14C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m GETSET\u001b[11C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m GETBIT\u001b[11C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m GEOPOS\u001b[11C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m GEOA\u001b[CD\u001b[11C \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m GEOHASH\u001b[10C \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m GEODIST\u001b[10C \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[20D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[51.635334, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mt foo\u001b[0m \u001b[6D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[51.773743, "o", "\u001b[?25l\u001b[?7l\u001b[2D\u001b[0;38;5;28;1mget\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m GET\u001b[6C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m GETSET\u001b[3C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m GETBIT\u001b[3C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m GETRANGE \u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0m\r\r\n\u001b[0;38;5;167;48;5;235;1m(string) \u001b[0;38;5;28;48;5;235;1mGET\u001b[0;38;5;71;48;5;235m key\u001b[0;38;5;136;48;5;235m since: 1.0.0\u001b[0;38;5;241;48;5;235m complexity:O(1)\u001b[8A\u001b[28D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[51.77765, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m foo\u001b[0m \u001b[5D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[51.91592, "o", "\u001b[?25l\u001b[?7l\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m af\u001b[6C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m list:animals \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m cars \u001b[3C \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m myset \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[20C\u001b[0;38;5;231;48;5;30m kkk \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[20C\u001b[0;38;5;231;48;5;30m somestream \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[20C\u001b[0;38;5;231;48;5;30m hash2 \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[51.921861, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mfoo\u001b[0m \u001b[4D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[52.325226, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71ma\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30mf\u001b[13C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;231;48;5;30m \u001b[4C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30mbc\u001b[12C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mc\u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30mrs\u001b[11C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mh\u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30msh2\u001b[10C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mh\u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30msh3\u001b[0;38;5;231;48;5;30m \u001b[6C \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mh\u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30msh1"] +[52.32535, "o", "\u001b[10C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[52.329701, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[52.448681, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mf\u001b[0m \u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4maf\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[7A\u001b[17D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[52.453078, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[A\u001b[21C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[52.892111, "o", "\u001b[?25l\u001b[?7l\u001b[22D\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0;38;5;28;1mget\u001b[0m \u001b[0;38;5;71maf\u001b[0m \r\u001b[65C \r\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] +[52.896263, "o", "\u001b[0m\u001b[?7h\u001b[0m\"asdf\"\u001b[0m\u001b[0m"] +[52.898075, "o", "\u001b[0m\u001b[?7h\u001b[0m\r\r\n\u001b[0m"] +[52.898599, "o", "\u001b[?1l\u001b[6n"] +[52.90232, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \r\u001b[65C \r\u001b[7A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[52.905874, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;248;48;5;235mCtrl-D to exit; \r\u001b[65C \r\u001b[8A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[53.943112, "o", "\u001b[?25l\u001b[?7l\u001b[0mg \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m GET \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m GETSET \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m GETBIT \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m GEOPOS \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m GEOADD \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m GEOHASH \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m GEODIST \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[20D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[53.9504, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241met af\u001b[0m \u001b[6D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[54.043955, "o", "\u001b[?25l\u001b[?7l\u001b[0me \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m GET\u001b[14C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m GETSET\u001b[11C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m GETBIT\u001b[11C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m GEOPOS\u001b[11C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m GEOA\u001b[CD\u001b[11C \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m GEOHASH\u001b[10C \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m GEODIST\u001b[10C \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[20D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[54.047839, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mt af\u001b[0m \u001b[5D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[54.192601, "o", "\u001b[?25l\u001b[?7l\u001b[2D\u001b[0;38;5;28;1mget\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m GET\u001b[6C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m GETSET\u001b[3C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m GETBIT\u001b[3C\u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m GETRANGE \u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0m\r\r\n\u001b[0;38;5;167;48;5;235;1m(string) \u001b[0;38;5;28;48;5;235;1mGET\u001b[0;38;5;71;48;5;235m key\u001b[0;38;5;136;48;5;235m since: 1.0.0\u001b[0;38;5;241;48;5;235m complexity:O(1)\u001b[8A\u001b[28D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[54.197235, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m af\u001b[0m \u001b[4D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[54.335864, "o", "\u001b[?25l\u001b[?7l\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m af\u001b[6C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m list:animals \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m cars \u001b[3C \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m myset \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[20C\u001b[0;38;5;231;48;5;30m kkk \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[20C\u001b[0;38;5;231;48;5;30m somestream \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[20C\u001b[0;38;5;231;48;5;30m hash2 \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[54.342312, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241maf\u001b[0m \u001b[3D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[54.76708, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71ma\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30mf\u001b[13C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;231;48;5;30m \u001b[4C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30mbc\u001b[12C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mc\u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30mrs\u001b[11C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mh\u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30msh2\u001b[10C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mh\u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30msh3\u001b[0;38;5;231;48;5;30m \u001b[6C \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mh\u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30msh1"] +[54.767202, "o", "\u001b[10C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[54.771478, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mf\u001b[0m \u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[55.027261, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mf\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4maf\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0m \u001b[7A\u001b[17D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[55.03053, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[A\u001b[21C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[55.228293, "o", "\u001b[?25l\u001b[?7l\u001b[C\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[55.231931, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[55.573146, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;16;48;5;196mi\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[55.577988, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[55.66374, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;16;48;5;196mn\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[55.667964, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[55.984948, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;16;48;5;196mv\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[55.98961, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[56.157229, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;16;48;5;196ma\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[56.162529, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[56.423, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;16;48;5;196ml\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[56.427638, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[56.586877, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;16;48;5;196mi\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[56.593834, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[56.913957, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;16;48;5;196md\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[56.918423, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[57.091248, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;16;48;5;196me\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[57.095887, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[57.190026, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;16;48;5;196m \u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[57.1952, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[57.430926, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;16;48;5;196mi\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[57.435255, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[57.500268, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;16;48;5;196mn\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[57.506086, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[57.711636, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;16;48;5;196mp\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[57.715911, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[57.984832, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;16;48;5;196mu\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[57.992887, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[58.141271, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;16;48;5;196mt\u001b[0m \b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[58.145592, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.061523, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.073482, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.309508, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.315274, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.34596, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.350846, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.379097, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.38583, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.416019, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.431899, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.446868, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.45367, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.479192, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.483837, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.512893, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.519325, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.546693, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.550989, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.581696, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.586075, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.613113, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.617986, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.648799, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.652827, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.800181, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.805545, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.955905, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[59.959298, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.208194, "o", "\u001b[?25l\u001b[?7l\u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.212447, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.247807, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.254547, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.278, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.2824, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.309265, "o", "\u001b[?25l\u001b[?7l\u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.313357, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.342096, "o", "\u001b[?25l\u001b[?7l\u001b[3D\u001b[0mge \u001b[0m\u001b[K\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;248;48;5;235mCtrl-D to exit; \u001b[8A\u001b[29D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.352259, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.377642, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.38131, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.408313, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m \u001b[0m\u001b[K\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.412424, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.449815, "o", "\u0007"] +[60.453021, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[60.473651, "o", "\u0007"] +[60.476542, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[62.514509, "o", "\u001b[?25l\u001b[?7l\u001b[0md \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m DEL \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m DECR \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m DUMP \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m DECRBY \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m DBSIZE \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m DISCARD \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0;38;5;231;48;5;30m DEBUG OBJECT \u001b[0;38;5;16;48;5;37m \u001b[7A\u001b[17D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[62.520623, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241mel _kombu.binding.celeryev\u001b[0m \u001b[27D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[62.683241, "o", "\u001b[?25l\u001b[?7l\u001b[0me \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m DEL\u001b[11C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m DECR\u001b[10C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m DECRBY\u001b[8C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m DEBUG\u001b[COBJECT\u001b[2C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[0;38;5;231;48;5;30m DEBUG SEGFAULT \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[17C\u001b[0m \u001b[7A\u001b[16D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[62.686861, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241ml _kombu.binding.celeryev\u001b[0m \u001b[26D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[62.885578, "o", "\u001b[?25l\u001b[?7l\u001b[2D\u001b[0;38;5;28;1mdel\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[18C\u001b[0m \u001b[0;38;5;231;48;5;30m DEL \u001b[0;38;5;16;48;5;238m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[C\u001b[0m\u001b[K\u001b[0m\r\r\n\r\r\n\r\r\n\u001b[0;38;5;167;48;5;235;1m(generic) \u001b[0;38;5;28;48;5;235;1mDEL\u001b[0;38;5;71;48;5;235m key\u001b[0;38;5;136;48;5;235m since: 1.0.0\u001b[0;38;5;241;48;5;235m complexity:O(N) where N is the n\r\u001b[65Cu\u001b[8A\r\u001b[19C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[62.890125, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m _kombu.binding.celeryev\u001b[0m \u001b[25D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[63.433741, "o", "\u001b[?25l\u001b[?7l\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[19C\u001b[0m \u001b[0;38;5;231;48;5;30m af \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[20C\u001b[0;38;5;231;48;5;30m list:animals \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[20C\u001b[0;38;5;231;48;5;30m cars \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[20C\u001b[0;38;5;231;48;5;30m myset \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[20C\u001b[0;38;5;231;48;5;30m kkk \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[20C\u001b[0;38;5;231;48;5;30m somestream \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[20C\u001b[0;38;5;231;48;5;30m hash2 \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[63.438223, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m_kombu.binding.celeryev\u001b[0m \u001b[24D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[64.652788, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71ma\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30mf\u001b[13C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;231;48;5;30m \u001b[4C \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30mbc\u001b[12C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mc\u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30mrs\u001b[11C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;238m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mh\u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30msh2\u001b[10C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;37m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mh\u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30msh3\u001b[0;38;5;231;48;5;30m \u001b[6C \u001b[0;38;5;16;48;5;248m \u001b[0m\r\r\n\u001b[20C\u001b[0m \u001b[0;38;5;231;48;5;30m \u001b[0;38;5;238;48;5;30mh\u001b[0;38;5;231;48;5;30;1;4ma\u001b[0;38;5;238;48;5;30msh1"] +[64.652904, "o", "\u001b[10C\u001b[0;38;5;231;48;5;30m \u001b[0;38;5;16;48;5;248m \u001b[7A\u001b[18D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[64.66165, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;241m\\\"bc\u001b[0m \u001b[5D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[65.254736, "o", "\u001b[?25l\u001b[?7l\u001b[0;38;5;71mf\u001b[0m \u001b[0m\u001b[K\u001b[0m\r\r\n\u001b[21C\u001b[0;38;5;16;48;5;231m \u001b[0;38;5;16;48;5;231;4ma\u001b[0;48;5;231mf\u001b[0;38;5;16;48;5;231m \u001b[A\u001b[16D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[65.26029, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[65.761821, "o", "\u001b[?25l\u001b[?7l\u001b[22D\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0;38;5;28;1mdel\u001b[0m \u001b[0;38;5;71maf\u001b[0m \r\u001b[65C \r\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] +[65.766399, "o", "\u001b[0m\u001b[?7h\u001b[0;38;5;102m(integer) \u001b[0m1\u001b[0m\u001b[0m"] +[65.768122, "o", "\u001b[0m\u001b[?7h\u001b[0m\r\r\n\u001b[0m"] +[65.768819, "o", "\u001b[?1l\u001b[6n"] +[65.771167, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0m127.0.0.1:6379> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \r\u001b[65C \r\u001b[7A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[65.774133, "o", "\u001b[?25l\u001b[?7l\u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0;38;5;248;48;5;235mCtrl-D to exit; \r\u001b[65C \r\u001b[8A\u001b[16C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] +[66.838972, "o", "\u001b[?25l\u001b[?7l\u001b[16D\u001b[0m\u001b[J\u001b[0;38;5;102m127.0.0.1:6379> \r\u001b[65C \r\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] +[66.839665, "o", "Goodbye!\r\n"] +[66.871374, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] +[66.893132, "o", "\u001b]133;D;0\u0007\u001b]1337;RemoteHost=laixintao@Chico.local\u0007\u001b]1337;CurrentDir=/Users/laixintao\u0007"] +[66.896847, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]133;A\u0007$ \u001b]133;B\u0007\u001b[K"] +[66.897062, "o", "\u001b[?1h\u001b="] +[66.897178, "o", "\u001b[?2004h"] +[67.725923, "o", "\u001b[?2004l\r\r\n"] diff --git a/docs/assets/demo.svg b/docs/assets/demo.svg new file mode 100644 index 0000000..6213d56 --- /dev/null +++ b/docs/assets/demo.svg @@ -0,0 +1,214 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="terminal" baseProfile="full" viewBox="0 0 528 342" width="528" version="1.1"> + <defs> + <termtosvg:template_settings xmlns:termtosvg="https://github.com/nbedos/termtosvg"> + <termtosvg:screen_geometry columns="66" rows="20"/> + <termtosvg:animation type="css"/> + </termtosvg:template_settings> + <style type="text/css" id="generated-style"><![CDATA[#screen { + font-family: 'DejaVu Sans Mono', monospace; + font-style: normal; + font-size: 14px; + } + + text { + dominant-baseline: text-before-edge; + white-space: pre; + } + + :root { + --animation-duration: 32720ms; + } + + @keyframes roll { + 0.000%{transform:translateY(0px)} +0.165%{transform:translateY(-374px)} +1.082%{transform:translateY(-748px)} +1.999%{transform:translateY(-1122px)} +2.289%{transform:translateY(-1496px)} +2.754%{transform:translateY(-1870px)} +3.108%{transform:translateY(-2244px)} +3.408%{transform:translateY(-2618px)} +3.826%{transform:translateY(-2992px)} +4.685%{transform:translateY(-3366px)} +5.602%{transform:translateY(-3740px)} +5.938%{transform:translateY(-4114px)} +6.317%{transform:translateY(-4488px)} +6.650%{transform:translateY(-4862px)} +7.424%{transform:translateY(-5236px)} +8.340%{transform:translateY(-5610px)} +9.254%{transform:translateY(-5984px)} +10.171%{transform:translateY(-6358px)} +10.917%{transform:translateY(-6732px)} +11.553%{transform:translateY(-7106px)} +11.953%{transform:translateY(-7480px)} +12.265%{transform:translateY(-7854px)} +13.182%{transform:translateY(-8228px)} +14.098%{transform:translateY(-8602px)} +15.015%{transform:translateY(-8976px)} +15.779%{transform:translateY(-9350px)} +16.696%{transform:translateY(-9724px)} +17.613%{transform:translateY(-10098px)} +17.845%{transform:translateY(-10472px)} +18.133%{transform:translateY(-10846px)} +19.050%{transform:translateY(-11220px)} +19.835%{transform:translateY(-11594px)} +20.752%{transform:translateY(-11968px)} +21.592%{transform:translateY(-12342px)} +22.216%{transform:translateY(-12716px)} +23.133%{transform:translateY(-13090px)} +24.050%{transform:translateY(-13464px)} +24.413%{transform:translateY(-13838px)} +24.917%{transform:translateY(-14212px)} +25.162%{transform:translateY(-14586px)} +25.504%{transform:translateY(-14960px)} +26.418%{transform:translateY(-15334px)} +26.687%{transform:translateY(-15708px)} +27.604%{transform:translateY(-16082px)} +28.521%{transform:translateY(-16456px)} +29.438%{transform:translateY(-16830px)} +30.238%{transform:translateY(-17204px)} +30.831%{transform:translateY(-17578px)} +31.748%{transform:translateY(-17952px)} +32.665%{transform:translateY(-18326px)} +33.582%{transform:translateY(-18700px)} +34.211%{transform:translateY(-19074px)} +35.128%{transform:translateY(-19448px)} +36.045%{transform:translateY(-19822px)} +36.962%{transform:translateY(-20196px)} +37.879%{transform:translateY(-20570px)} +38.796%{transform:translateY(-20944px)} +39.224%{transform:translateY(-21318px)} +39.771%{transform:translateY(-21692px)} +40.083%{transform:translateY(-22066px)} +40.422%{transform:translateY(-22440px)} +41.164%{transform:translateY(-22814px)} +41.843%{transform:translateY(-23188px)} +42.760%{transform:translateY(-23562px)} +43.677%{transform:translateY(-23936px)} +44.594%{transform:translateY(-24310px)} +45.052%{transform:translateY(-24684px)} +45.969%{transform:translateY(-25058px)} +46.650%{transform:translateY(-25432px)} +47.567%{transform:translateY(-25806px)} +48.484%{transform:translateY(-26180px)} +49.401%{transform:translateY(-26554px)} +50.034%{transform:translateY(-26928px)} +50.770%{transform:translateY(-27302px)} +51.687%{transform:translateY(-27676px)} +52.604%{transform:translateY(-28050px)} +53.521%{transform:translateY(-28424px)} +53.790%{transform:translateY(-28798px)} +54.031%{transform:translateY(-29172px)} +54.450%{transform:translateY(-29546px)} +54.795%{transform:translateY(-29920px)} +55.220%{transform:translateY(-30294px)} +56.137%{transform:translateY(-30668px)} +57.054%{transform:translateY(-31042px)} +57.723%{transform:translateY(-31416px)} +58.640%{transform:translateY(-31790px)} +59.129%{transform:translateY(-32164px)} +60.046%{transform:translateY(-32538px)} +60.963%{transform:translateY(-32912px)} +61.880%{transform:translateY(-33286px)} +62.347%{transform:translateY(-33660px)} +63.020%{transform:translateY(-34034px)} +63.450%{transform:translateY(-34408px)} +64.367%{transform:translateY(-34782px)} +65.079%{transform:translateY(-35156px)} +65.996%{transform:translateY(-35530px)} +66.913%{transform:translateY(-35904px)} +67.830%{transform:translateY(-36278px)} +68.408%{transform:translateY(-36652px)} +68.869%{transform:translateY(-37026px)} +69.786%{transform:translateY(-37400px)} +70.703%{transform:translateY(-37774px)} +70.972%{transform:translateY(-38148px)} +71.409%{transform:translateY(-38522px)} +71.846%{transform:translateY(-38896px)} +72.763%{transform:translateY(-39270px)} +73.139%{transform:translateY(-39644px)} +74.056%{transform:translateY(-40018px)} +74.972%{transform:translateY(-40392px)} +75.281%{transform:translateY(-40766px)} +75.733%{transform:translateY(-41140px)} +76.174%{transform:translateY(-41514px)} +77.090%{transform:translateY(-41888px)} +77.885%{transform:translateY(-42262px)} +78.499%{transform:translateY(-42636px)} +79.416%{transform:translateY(-43010px)} +79.694%{transform:translateY(-43384px)} +80.611%{transform:translateY(-43758px)} +81.137%{transform:translateY(-44132px)} +81.950%{transform:translateY(-44506px)} +82.448%{transform:translateY(-44880px)} +83.365%{transform:translateY(-45254px)} +83.909%{transform:translateY(-45628px)} +84.211%{transform:translateY(-46002px)} +84.945%{transform:translateY(-46376px)} +85.159%{transform:translateY(-46750px)} +85.804%{transform:translateY(-47124px)} +86.638%{transform:translateY(-47498px)} +87.118%{transform:translateY(-47872px)} +88.035%{transform:translateY(-48246px)} +88.793%{transform:translateY(-48620px)} +88.918%{transform:translateY(-48994px)} +89.117%{transform:translateY(-49368px)} +89.309%{transform:translateY(-49742px)} +89.435%{transform:translateY(-50116px)} +89.624%{transform:translateY(-50490px)} +89.829%{transform:translateY(-50864px)} +90.290%{transform:translateY(-51238px)} +90.767%{transform:translateY(-51612px)} +91.537%{transform:translateY(-51986px)} +91.681%{transform:translateY(-52360px)} +91.849%{transform:translateY(-52734px)} +91.980%{transform:translateY(-53108px)} +92.152%{transform:translateY(-53482px)} +92.277%{transform:translateY(-53856px)} +93.194%{transform:translateY(-54230px)} +93.710%{transform:translateY(-54604px)} +94.328%{transform:translateY(-54978px)} +95.244%{transform:translateY(-55352px)} +96.161%{transform:translateY(-55726px)} +97.078%{transform:translateY(-56100px)} +97.995%{transform:translateY(-56474px)} +98.912%{transform:translateY(-56848px)} +99.077%{transform:translateY(-57222px)} +99.994%{transform:translateY(-57596px)} +100.000%{transform:translateY(-57596px)} + } + + #screen_view { + animation-duration: 32720ms; + animation-iteration-count:infinite; + animation-name:roll; + animation-timing-function: steps(1,end); + animation-fill-mode: forwards; + } + ]]></style> + <style type="text/css" id="user-style"> + /* The colors defined below are the default 16 colors used for rendering text of the terminal. Adjust + them as needed. + gjm8 color theme (source: https://terminal.sexy/) */ + .foreground {fill: #c5c5c5;} + .background {fill: #1c1c1c;} + .color0 {fill: #1c1c1c;} + .color1 {fill: #ff005b;} + .color2 {fill: #cee318;} + .color3 {fill: #ffe755;} + .color4 {fill: #048ac7;} + .color5 {fill: #833c9f;} + .color6 {fill: #0ac1cd;} + .color7 {fill: #e5e5e5;} + .color8 {fill: #1c1c1c;} + .color9 {fill: #ff005b;} + .color10 {fill: #cee318;} + .color11 {fill: #ffe755;} + .color12 {fill: #048ac7;} + .color13 {fill: #833c9f;} + .color14 {fill: #0ac1cd;} + .color15 {fill: #e5e5e5;} + </style> + </defs> + <svg id="screen" width="528" height="340" viewBox="0 0 528 340" preserveAspectRatio="xMidYMin slice"><rect class="background" height="100%" width="100%" x="0" y="0"/><defs><g id="g1"><text x="0" textLength="8" class="background"> </text><text x="8" textLength="520" class="foreground"> </text></g><g id="g2"><text x="0" textLength="16" class="foreground">$ </text><text x="16" textLength="8" class="background"> </text><text x="24" textLength="504" class="foreground"> </text></g><g id="g3"><text x="0" textLength="24" class="foreground">$ i</text><text x="24" textLength="8" class="background"> </text><text x="32" textLength="496" class="foreground"> </text></g><g id="g4"><text x="0" textLength="32" class="foreground">$ ir</text><text x="32" textLength="8" class="background"> </text><text x="40" textLength="488" class="foreground"> </text></g><g id="g5"><text x="0" textLength="40" class="foreground">$ ire</text><text x="40" textLength="8" class="background"> </text><text x="48" textLength="480" class="foreground"> </text></g><g id="g6"><text x="0" textLength="48" class="foreground">$ ired</text><text x="48" textLength="8" class="background"> </text><text x="56" textLength="472" class="foreground"> </text></g><g id="g7"><text x="0" textLength="56" class="foreground">$ iredi</text><text x="56" textLength="8" class="background"> </text><text x="64" textLength="464" class="foreground"> </text></g><g id="g8"><text x="0" textLength="64" class="foreground">$ iredis</text><text x="64" textLength="8" class="background"> </text><text x="72" textLength="456" class="foreground"> </text></g><g id="g9"><text x="0" textLength="528" class="foreground">$ iredis </text></g><g id="g10"><text x="0" textLength="8" class="background"> </text></g><g id="g11"><text x="0" textLength="104" class="foreground">iredis 0.8.0</text></g><g id="g12"><text x="0" textLength="160" class="foreground">redis-server 5.0.6 </text></g><g id="g13"><text x="0" textLength="200" class="foreground">Home: https://iredis.io</text></g><g id="g14"><text x="0" textLength="256" class="foreground">Issues: https://iredis.io/issues</text></g><g id="g15"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="8" class="background"> </text><text x="136" textLength="392" class="foreground"> </text></g><g id="g16"><text x="0" textLength="528" class="foreground"> </text></g><g id="g17"><text x="0" textLength="528" fill="#a8a8a8">Ctrl-D to exit; </text></g><g id="g18"><text x="0" textLength="136" class="foreground">127.0.0.1:6379> k</text><text x="136" textLength="8" class="background">e</text><text x="144" textLength="32" fill="#626262">ys *</text><text x="176" textLength="352" class="foreground"> </text></g><g id="g19"><text x="136" textLength="56" fill="#ffffff"> KEYS </text><text x="192" textLength="8" fill="#000000"> </text></g><g id="g20"><text x="0" textLength="144" class="foreground">127.0.0.1:6379> ke</text><text x="144" textLength="8" class="background">y</text><text x="152" textLength="24" fill="#626262">s *</text><text x="176" textLength="352" class="foreground"> </text></g><g id="g21"><text x="136" textLength="8" class="foreground"> </text><text x="144" textLength="56" fill="#ffffff"> KEYS </text><text x="200" textLength="8" fill="#000000"> </text></g><g id="g22"><text x="0" textLength="152" class="foreground">127.0.0.1:6379> key</text><text x="152" textLength="8" class="background">s</text><text x="160" textLength="16" fill="#626262"> *</text><text x="176" textLength="352" class="foreground"> </text></g><g id="g23"><text x="136" textLength="16" class="foreground"> </text><text x="152" textLength="56" fill="#ffffff"> KEYS </text><text x="208" textLength="8" fill="#000000"> </text></g><g id="g24"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">keys</text><text x="160" textLength="8" class="background"> </text><text x="168" textLength="8" fill="#626262">*</text><text x="176" textLength="352" class="foreground"> </text></g><g id="g25"><text x="136" textLength="24" class="foreground"> </text><text x="160" textLength="56" fill="#ffffff"> KEYS </text><text x="216" textLength="8" fill="#000000"> </text></g><g id="g26"><text x="0" textLength="80" font-weight="bold" fill="#d75f5f">(generic) </text><text x="80" textLength="32" font-weight="bold" fill="#008700">KEYS</text><text x="112" textLength="64" font-weight="bold" fill="#5faf5f"> pattern</text><text x="176" textLength="120" fill="#af8700"> since: 1.0.0</text><text x="296" textLength="232" fill="#626262"> complexity:O(N) with N being</text></g><g id="g27"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">keys</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="8" class="background">*</text><text x="176" textLength="352" class="foreground"> </text></g><g id="g28"><text x="8" textLength="520" class="foreground"> </text></g><g id="g29"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">keys</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="8" font-weight="bold" fill="#5faf5f">*</text><text x="176" textLength="8" class="background"> </text><text x="184" textLength="344" class="foreground"> </text></g><g id="g30"><text x="0" textLength="32" class="foreground">10) </text><text x="32" textLength="56" fill="#5faf5f">"hash1"</text></g><g id="g31"><text x="0" textLength="32" class="foreground">11) </text><text x="32" textLength="128" fill="#5faf5f">"list:buildings"</text></g><g id="g32"><text x="0" textLength="32" class="foreground">12) </text><text x="32" textLength="56" fill="#5faf5f">"hash3"</text></g><g id="g33"><text x="0" textLength="32" class="foreground">13) </text><text x="32" textLength="64" fill="#5faf5f">"fooset"</text><text x="96" textLength="432" class="foreground"> </text></g><g id="g34"><text x="0" textLength="32" class="foreground">14) </text><text x="32" textLength="40" fill="#5faf5f">"foo"</text><text x="72" textLength="456" class="foreground"> </text></g><g id="g35"><text x="0" textLength="32" class="foreground">15) </text><text x="32" textLength="56" fill="#5faf5f">"myset"</text></g><g id="g36"><text x="0" textLength="32" class="foreground">16) </text><text x="32" textLength="56" fill="#5faf5f">"hash2"</text></g><g id="g37"><text x="0" textLength="32" class="foreground">17) </text><text x="32" textLength="112" fill="#5faf5f">"list:animals"</text></g><g id="g38"><text x="0" textLength="32" class="foreground">18) </text><text x="32" textLength="32" fill="#5faf5f">"af"</text></g><g id="g39"><text x="0" textLength="32" class="foreground">19) </text><text x="32" textLength="96" fill="#5faf5f">"somestream"</text></g><g id="g40"><text x="0" textLength="32" class="foreground">20) </text><text x="32" textLength="40" fill="#5faf5f">"kkk"</text></g><g id="g41"><text x="0" textLength="136" class="foreground">127.0.0.1:6379> t</text><text x="136" textLength="8" class="background">y</text><text x="144" textLength="64" fill="#626262">pe myset</text><text x="208" textLength="320" class="foreground"> </text></g><g id="g42"><text x="136" textLength="56" fill="#ffffff"> TTL </text><text x="192" textLength="8" fill="#000000"> </text></g><g id="g43"><text x="136" textLength="56" fill="#ffffff"> TIME </text><text x="192" textLength="8" fill="#000000"> </text></g><g id="g44"><text x="136" textLength="56" fill="#ffffff"> TYPE </text><text x="192" textLength="8" fill="#000000"> </text></g><g id="g45"><text x="136" textLength="56" fill="#ffffff"> TOUCH </text><text x="192" textLength="8" fill="#000000"> </text></g><g id="g46"><text x="0" textLength="144" class="foreground">127.0.0.1:6379> ty</text><text x="144" textLength="8" class="background">p</text><text x="152" textLength="56" fill="#626262">e myset</text><text x="208" textLength="320" class="foreground"> </text></g><g id="g47"><text x="136" textLength="8" class="foreground"> </text><text x="144" textLength="56" fill="#ffffff"> TYPE </text><text x="200" textLength="8" fill="#000000"> </text></g><g id="g48"><text x="0" textLength="152" class="foreground">127.0.0.1:6379> typ</text><text x="152" textLength="8" class="background">e</text><text x="160" textLength="48" fill="#626262"> myset</text><text x="208" textLength="320" class="foreground"> </text></g><g id="g49"><text x="136" textLength="16" class="foreground"> </text><text x="152" textLength="56" fill="#ffffff"> TYPE </text><text x="208" textLength="8" fill="#000000"> </text></g><g id="g50"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="background"> </text><text x="168" textLength="40" fill="#626262">myset</text><text x="208" textLength="320" class="foreground"> </text></g><g id="g51"><text x="136" textLength="24" class="foreground"> </text><text x="160" textLength="56" fill="#ffffff"> TYPE </text><text x="216" textLength="8" fill="#000000"> </text></g><g id="g52"><text x="0" textLength="80" font-weight="bold" fill="#d75f5f">(generic) </text><text x="80" textLength="32" font-weight="bold" fill="#008700">TYPE</text><text x="112" textLength="32" fill="#5faf5f"> key</text><text x="144" textLength="120" fill="#af8700"> since: 1.0.0</text><text x="264" textLength="128" fill="#626262"> complexity:O(1)</text><text x="392" textLength="136" fill="#a8a8a8"> </text></g><g id="g53"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="8" class="background">m</text><text x="176" textLength="32" fill="#626262">yset</text><text x="208" textLength="320" class="foreground"> </text></g><g id="g54"><text x="136" textLength="32" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> kkk </text><text x="304" textLength="8" fill="#000000"> </text></g><g id="g55"><text x="8" textLength="160" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> somestream </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g56"><text x="8" textLength="160" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> af </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g57"><text x="8" textLength="160" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> list:animals </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g58"><text x="168" textLength="136" fill="#ffffff"> hash2 </text><text x="304" textLength="8" fill="#000000"> </text></g><g id="g59"><text x="168" textLength="136" fill="#ffffff"> myset </text><text x="304" textLength="8" fill="#000000"> </text></g><g id="g60"><text x="0" textLength="168" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> foo </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g61"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="8" fill="#5faf5f">m</text><text x="176" textLength="8" class="background">y</text><text x="184" textLength="24" fill="#626262">set</text><text x="208" textLength="320" class="foreground"> </text></g><g id="g62"><text x="136" textLength="40" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">m</text><text x="192" textLength="32" fill="#444444">yset</text><text x="224" textLength="64" fill="#ffffff"> </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g63"><text x="8" textLength="168" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">m</text><text x="192" textLength="48" fill="#444444">stream</text><text x="240" textLength="48" fill="#ffffff"> </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g64"><text x="8" textLength="168" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">m</text><text x="192" textLength="48" fill="#444444">ylist1</text><text x="240" textLength="48" fill="#ffffff"> </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g65"><text x="8" textLength="168" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">m</text><text x="192" textLength="40" fill="#444444">yzset</text><text x="232" textLength="56" fill="#ffffff"> </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g66"><text x="168" textLength="8" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="16" fill="#444444">so</text><text x="200" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">m</text><text x="208" textLength="56" fill="#444444">estream</text><text x="264" textLength="24" fill="#ffffff"> </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g67"><text x="168" textLength="8" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="64" fill="#444444">list:ani</text><text x="248" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">m</text><text x="256" textLength="24" fill="#444444">als</text><text x="280" textLength="8" fill="#ffffff"> </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g68"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="16" fill="#5faf5f">my</text><text x="184" textLength="8" class="background">s</text><text x="192" textLength="16" fill="#626262">et</text><text x="208" textLength="320" class="foreground"> </text></g><g id="g69"><text x="136" textLength="48" class="foreground"> </text><text x="184" textLength="8" fill="#ffffff"> </text><text x="192" textLength="16" font-weight="bold" text-decoration="underline" fill="#ffffff">my</text><text x="208" textLength="24" fill="#444444">set</text><text x="232" textLength="24" fill="#ffffff"> </text><text x="256" textLength="8" fill="#000000"> </text><text x="264" textLength="264" class="foreground"> </text></g><g id="g70"><text x="8" textLength="176" class="foreground"> </text><text x="184" textLength="8" fill="#ffffff"> </text><text x="192" textLength="16" font-weight="bold" text-decoration="underline" fill="#ffffff">my</text><text x="208" textLength="40" fill="#444444">list1</text><text x="248" textLength="8" fill="#ffffff"> </text><text x="256" textLength="8" fill="#000000"> </text><text x="264" textLength="264" class="foreground"> </text></g><g id="g71"><text x="8" textLength="176" class="foreground"> </text><text x="184" textLength="8" fill="#ffffff"> </text><text x="192" textLength="16" font-weight="bold" text-decoration="underline" fill="#ffffff">my</text><text x="208" textLength="32" fill="#444444">zset</text><text x="240" textLength="16" fill="#ffffff"> </text><text x="256" textLength="8" fill="#000000"> </text><text x="264" textLength="264" class="foreground"> </text></g><g id="g72"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="40" fill="#5faf5f">myset</text><text x="208" textLength="8" class="background"> </text><text x="216" textLength="312" class="foreground"> </text></g><g id="g73"><text x="136" textLength="48" class="foreground"> </text><text x="184" textLength="8" fill="#000000"> </text><text x="192" textLength="16" text-decoration="underline" fill="#000000">my</text><text x="208" textLength="24" class="foreground">set</text><text x="232" textLength="32" fill="#000000"> </text><text x="264" textLength="264" class="foreground"> </text></g><g id="g74"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="40" fill="#5faf5f">myset</text><text x="208" textLength="320" class="foreground"> </text></g><g id="g75"><text x="0" textLength="528" class="foreground">"set" </text></g><g id="g76"><text x="0" textLength="136" class="foreground">127.0.0.1:6379> s</text><text x="136" textLength="8" class="background">e</text><text x="144" textLength="72" fill="#626262">t foo bar</text><text x="216" textLength="312" class="foreground"> </text></g><g id="g77"><text x="8" textLength="128" class="foreground"> </text><text x="136" textLength="120" fill="#ffffff"> SET </text><text x="256" textLength="8" fill="#000000"> </text><text x="264" textLength="264" class="foreground"> </text></g><g id="g78"><text x="8" textLength="128" class="foreground"> </text><text x="136" textLength="120" fill="#ffffff"> SREM </text><text x="256" textLength="8" fill="#000000"> </text><text x="264" textLength="264" class="foreground"> </text></g><g id="g79"><text x="8" textLength="128" class="foreground"> </text><text x="136" textLength="120" fill="#ffffff"> SPOP </text><text x="256" textLength="8" fill="#000000"> </text><text x="264" textLength="264" class="foreground"> </text></g><g id="g80"><text x="8" textLength="128" class="foreground"> </text><text x="136" textLength="120" fill="#ffffff"> SADD </text><text x="256" textLength="8" fill="#000000"> </text><text x="264" textLength="264" class="foreground"> </text></g><g id="g81"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="120" fill="#ffffff"> SYNC </text><text x="256" textLength="8" fill="#000000"> </text><text x="264" textLength="264" class="foreground"> </text></g><g id="g82"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="120" fill="#ffffff"> SAVE </text><text x="256" textLength="8" fill="#000000"> </text><text x="264" textLength="264" class="foreground"> </text></g><g id="g83"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="120" fill="#ffffff"> SORT </text><text x="256" textLength="8" fill="#000000"> </text><text x="264" textLength="264" class="foreground"> </text></g><g id="g84"><text x="0" textLength="144" class="foreground">127.0.0.1:6379> sm</text><text x="144" textLength="8" class="background">e</text><text x="152" textLength="96" fill="#626262">mbers fooset</text><text x="248" textLength="280" class="foreground"> </text></g><g id="g85"><text x="8" textLength="136" class="foreground"> </text><text x="144" textLength="80" fill="#ffffff"> SMOVE </text><text x="224" textLength="8" fill="#000000"> </text><text x="232" textLength="296" class="foreground"> </text></g><g id="g86"><text x="8" textLength="136" class="foreground"> </text><text x="144" textLength="80" fill="#ffffff"> SMEMBERS </text><text x="224" textLength="8" fill="#000000"> </text><text x="232" textLength="296" class="foreground"> </text></g><g id="g87"><text x="0" textLength="152" class="foreground">127.0.0.1:6379> sme</text><text x="152" textLength="8" class="background">m</text><text x="160" textLength="88" fill="#626262">bers fooset</text><text x="248" textLength="280" class="foreground"> </text></g><g id="g88"><text x="8" textLength="144" class="foreground"> </text><text x="152" textLength="80" fill="#ffffff"> SMEMBERS </text><text x="232" textLength="8" fill="#000000"> </text><text x="240" textLength="288" class="foreground"> </text></g><g id="g89"><text x="0" textLength="160" class="foreground">127.0.0.1:6379> smem</text><text x="160" textLength="8" class="background">b</text><text x="168" textLength="80" fill="#626262">ers fooset</text><text x="248" textLength="280" class="foreground"> </text></g><g id="g90"><text x="8" textLength="152" class="foreground"> </text><text x="160" textLength="80" fill="#ffffff"> SMEMBERS </text><text x="240" textLength="8" fill="#000000"> </text><text x="248" textLength="280" class="foreground"> </text></g><g id="g91"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="64" font-weight="bold" fill="#008700">SMEMBERS</text><text x="192" textLength="8" class="background"> </text><text x="200" textLength="328" class="foreground"> </text></g><g id="g92"><text x="8" textLength="152" class="foreground"> </text><text x="160" textLength="88" fill="#000000"> SMEMBERS </text><text x="248" textLength="280" class="foreground"> </text></g><g id="g93"><text x="0" textLength="48" font-weight="bold" fill="#d75f5f">(set) </text><text x="48" textLength="64" font-weight="bold" fill="#008700">SMEMBERS</text><text x="112" textLength="32" fill="#5faf5f"> key</text><text x="144" textLength="120" fill="#af8700"> since: 1.0.0</text><text x="264" textLength="264" fill="#626262"> complexity:O(N) where N is the s</text></g><g id="g94"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="64" font-weight="bold" fill="#008700">SMEMBERS</text><text x="192" textLength="8" class="foreground"> </text><text x="200" textLength="8" class="background">m</text><text x="208" textLength="32" fill="#626262">yset</text><text x="240" textLength="288" class="foreground"> </text></g><g id="g95"><text x="8" textLength="192" class="foreground"> </text><text x="200" textLength="136" fill="#ffffff"> myset </text><text x="336" textLength="8" fill="#000000"> </text><text x="344" textLength="184" class="foreground"> </text></g><g id="g96"><text x="8" textLength="192" class="foreground"> </text><text x="200" textLength="136" fill="#ffffff"> kkk </text><text x="336" textLength="8" fill="#000000"> </text><text x="344" textLength="184" class="foreground"> </text></g><g id="g97"><text x="8" textLength="192" class="foreground"> </text><text x="200" textLength="136" fill="#ffffff"> somestream </text><text x="336" textLength="8" fill="#000000"> </text><text x="344" textLength="184" class="foreground"> </text></g><g id="g98"><text x="8" textLength="192" class="foreground"> </text><text x="200" textLength="136" fill="#ffffff"> af </text><text x="336" textLength="8" fill="#000000"> </text><text x="344" textLength="184" class="foreground"> </text></g><g id="g99"><text x="0" textLength="200" class="foreground"> </text><text x="200" textLength="136" fill="#ffffff"> list:animals </text><text x="336" textLength="8" fill="#000000"> </text><text x="344" textLength="184" class="foreground"> </text></g><g id="g100"><text x="0" textLength="200" class="foreground"> </text><text x="200" textLength="136" fill="#ffffff"> hash2 </text><text x="336" textLength="8" fill="#000000"> </text><text x="344" textLength="184" class="foreground"> </text></g><g id="g101"><text x="0" textLength="200" class="foreground"> </text><text x="200" textLength="136" fill="#ffffff"> foo </text><text x="336" textLength="8" fill="#000000"> </text><text x="344" textLength="184" class="foreground"> </text></g><g id="g102"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="64" font-weight="bold" fill="#008700">SMEMBERS</text><text x="192" textLength="8" class="foreground"> </text><text x="200" textLength="8" fill="#5faf5f">m</text><text x="208" textLength="8" class="background">y</text><text x="216" textLength="24" fill="#626262">set</text><text x="240" textLength="288" class="foreground"> </text></g><g id="g103"><text x="8" textLength="200" class="foreground"> </text><text x="208" textLength="8" fill="#ffffff"> </text><text x="216" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">m</text><text x="224" textLength="32" fill="#444444">yset</text><text x="256" textLength="64" fill="#ffffff"> </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g104"><text x="8" textLength="200" class="foreground"> </text><text x="208" textLength="8" fill="#ffffff"> </text><text x="216" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">m</text><text x="224" textLength="48" fill="#444444">stream</text><text x="272" textLength="48" fill="#ffffff"> </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g105"><text x="8" textLength="200" class="foreground"> </text><text x="208" textLength="8" fill="#ffffff"> </text><text x="216" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">m</text><text x="224" textLength="48" fill="#444444">ylist1</text><text x="272" textLength="48" fill="#ffffff"> </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g106"><text x="8" textLength="200" class="foreground"> </text><text x="208" textLength="8" fill="#ffffff"> </text><text x="216" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">m</text><text x="224" textLength="40" fill="#444444">yzset</text><text x="264" textLength="56" fill="#ffffff"> </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g107"><text x="0" textLength="208" class="foreground"> </text><text x="208" textLength="8" fill="#ffffff"> </text><text x="216" textLength="16" fill="#444444">so</text><text x="232" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">m</text><text x="240" textLength="56" fill="#444444">estream</text><text x="296" textLength="24" fill="#ffffff"> </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g108"><text x="0" textLength="208" class="foreground"> </text><text x="208" textLength="8" fill="#ffffff"> </text><text x="216" textLength="64" fill="#444444">list:ani</text><text x="280" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">m</text><text x="288" textLength="24" fill="#444444">als</text><text x="312" textLength="8" fill="#ffffff"> </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g109"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="64" font-weight="bold" fill="#008700">SMEMBERS</text><text x="192" textLength="8" class="foreground"> </text><text x="200" textLength="16" fill="#5faf5f">my</text><text x="216" textLength="8" class="background">s</text><text x="224" textLength="16" fill="#626262">et</text><text x="240" textLength="288" class="foreground"> </text></g><g id="g110"><text x="8" textLength="208" class="foreground"> </text><text x="216" textLength="8" fill="#ffffff"> </text><text x="224" textLength="16" font-weight="bold" text-decoration="underline" fill="#ffffff">my</text><text x="240" textLength="24" fill="#444444">set</text><text x="264" textLength="24" fill="#ffffff"> </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g111"><text x="8" textLength="208" class="foreground"> </text><text x="216" textLength="8" fill="#ffffff"> </text><text x="224" textLength="16" font-weight="bold" text-decoration="underline" fill="#ffffff">my</text><text x="240" textLength="40" fill="#444444">list1</text><text x="280" textLength="8" fill="#ffffff"> </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g112"><text x="8" textLength="208" class="foreground"> </text><text x="216" textLength="8" fill="#ffffff"> </text><text x="224" textLength="16" font-weight="bold" text-decoration="underline" fill="#ffffff">my</text><text x="240" textLength="32" fill="#444444">zset</text><text x="272" textLength="16" fill="#ffffff"> </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g113"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="64" font-weight="bold" fill="#008700">SMEMBERS</text><text x="192" textLength="8" class="foreground"> </text><text x="200" textLength="40" fill="#5faf5f">myset</text><text x="240" textLength="8" class="background"> </text><text x="248" textLength="280" class="foreground"> </text></g><g id="g114"><text x="8" textLength="208" class="foreground"> </text><text x="216" textLength="8" fill="#000000"> </text><text x="224" textLength="16" text-decoration="underline" fill="#000000">my</text><text x="240" textLength="24" class="foreground">set</text><text x="264" textLength="32" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g115"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="64" font-weight="bold" fill="#008700">SMEMBERS</text><text x="192" textLength="8" class="foreground"> </text><text x="200" textLength="40" fill="#5faf5f">myset</text><text x="240" textLength="288" class="foreground"> </text></g><g id="g116"><text x="0" textLength="24" class="foreground">1) </text><text x="24" textLength="40" fill="#ff8700">"foo"</text><text x="64" textLength="464" class="foreground"> </text></g><g id="g117"><text x="0" textLength="24" class="foreground">2) </text><text x="24" textLength="56" fill="#ff8700">"world"</text><text x="80" textLength="448" class="foreground"> </text></g><g id="g118"><text x="0" textLength="24" class="foreground">3) </text><text x="24" textLength="40" fill="#ff8700">"bar"</text><text x="64" textLength="464" class="foreground"> </text></g><g id="g119"><text x="0" textLength="24" class="foreground">4) </text><text x="24" textLength="56" fill="#ff8700">"hello"</text><text x="80" textLength="448" class="foreground"> </text></g><g id="g120"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="56" fill="#ffffff"> TTL </text><text x="192" textLength="8" fill="#000000"> </text><text x="200" textLength="328" class="foreground"> </text></g><g id="g121"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="56" fill="#ffffff"> TIME </text><text x="192" textLength="8" fill="#000000"> </text><text x="200" textLength="328" class="foreground"> </text></g><g id="g122"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="56" fill="#ffffff"> TYPE </text><text x="192" textLength="8" fill="#000000"> </text><text x="200" textLength="328" class="foreground"> </text></g><g id="g123"><text x="0" textLength="144" class="foreground"> </text><text x="144" textLength="56" fill="#ffffff"> TYPE </text><text x="200" textLength="8" fill="#000000"> </text><text x="208" textLength="320" class="foreground"> </text></g><g id="g124"><text x="0" textLength="152" class="foreground"> </text><text x="152" textLength="56" fill="#ffffff"> TYPE </text><text x="208" textLength="8" fill="#000000"> </text><text x="216" textLength="312" class="foreground"> </text></g><g id="g125"><text x="0" textLength="160" class="foreground"> </text><text x="160" textLength="56" fill="#ffffff"> TYPE </text><text x="216" textLength="8" fill="#000000"> </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g126"><text x="0" textLength="168" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> myset </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g127"><text x="0" textLength="168" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> kkk </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g128"><text x="0" textLength="168" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> somestream </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g129"><text x="168" textLength="136" fill="#ffffff"> list:animals </text><text x="304" textLength="8" fill="#000000"> </text></g><g id="g130"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="8" fill="#5faf5f">c</text><text x="176" textLength="8" class="background">a</text><text x="184" textLength="16" fill="#626262">rs</text><text x="200" textLength="328" class="foreground"> </text></g><g id="g131"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">c</text><text x="192" textLength="24" fill="#444444">ars</text><text x="216" textLength="24" fill="#ffffff"> </text><text x="240" textLength="8" fill="#000000"> </text><text x="248" textLength="280" class="foreground"> </text></g><g id="g132"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="16" fill="#444444">Si</text><text x="200" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">c</text><text x="208" textLength="24" fill="#444444">ily</text><text x="232" textLength="8" fill="#ffffff"> </text><text x="240" textLength="8" fill="#000000"> </text><text x="248" textLength="280" class="foreground"> </text></g><g id="g133"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="16" fill="#444444">ab</text><text x="200" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">c</text><text x="208" textLength="32" fill="#ffffff"> </text><text x="240" textLength="8" fill="#000000"> </text><text x="248" textLength="280" class="foreground"> </text></g><g id="g134"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="16" fill="#5faf5f">ca</text><text x="184" textLength="8" class="background">r</text><text x="192" textLength="8" fill="#626262">s</text><text x="200" textLength="328" class="foreground"> </text></g><g id="g135"><text x="0" textLength="184" class="foreground"> </text><text x="184" textLength="8" fill="#ffffff"> </text><text x="192" textLength="16" font-weight="bold" text-decoration="underline" fill="#ffffff">ca</text><text x="208" textLength="16" fill="#444444">rs</text><text x="224" textLength="16" fill="#ffffff"> </text><text x="240" textLength="8" fill="#000000"> </text><text x="248" textLength="280" class="foreground"> </text></g><g id="g136"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="32" fill="#5faf5f">cars</text><text x="200" textLength="8" class="background"> </text><text x="208" textLength="320" class="foreground"> </text></g><g id="g137"><text x="0" textLength="184" class="foreground"> </text><text x="184" textLength="8" fill="#000000"> </text><text x="192" textLength="16" text-decoration="underline" fill="#000000">ca</text><text x="208" textLength="16" class="foreground">rs</text><text x="224" textLength="24" fill="#000000"> </text><text x="248" textLength="280" class="foreground"> </text></g><g id="g138"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="32" fill="#5faf5f">cars</text><text x="200" textLength="328" class="foreground"> </text></g><g id="g139"><text x="0" textLength="528" class="foreground">"zset" </text></g><g id="g140"><text x="0" textLength="136" class="foreground">127.0.0.1:6379> z</text><text x="136" textLength="8" class="background">s</text><text x="144" textLength="72" fill="#626262">can kkk 0</text><text x="216" textLength="312" class="foreground"> </text></g><g id="g141"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="144" fill="#ffffff"> ZREM </text><text x="280" textLength="8" fill="#000000"> </text><text x="288" textLength="240" class="foreground"> </text></g><g id="g142"><text x="8" textLength="128" class="foreground"> </text><text x="136" textLength="144" fill="#ffffff"> ZADD </text><text x="280" textLength="8" fill="#000000"> </text><text x="288" textLength="240" class="foreground"> </text></g><g id="g143"><text x="8" textLength="128" class="foreground"> </text><text x="136" textLength="144" fill="#ffffff"> ZSCAN </text><text x="280" textLength="8" fill="#000000"> </text><text x="288" textLength="240" class="foreground"> </text></g><g id="g144"><text x="8" textLength="128" class="foreground"> </text><text x="136" textLength="144" fill="#ffffff"> ZRANK </text><text x="280" textLength="8" fill="#000000"> </text><text x="288" textLength="240" class="foreground"> </text></g><g id="g145"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="144" fill="#ffffff"> ZCARD </text><text x="280" textLength="8" fill="#000000"> </text><text x="288" textLength="240" class="foreground"> </text></g><g id="g146"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="144" fill="#ffffff"> ZSCORE </text><text x="280" textLength="8" fill="#000000"> </text><text x="288" textLength="240" class="foreground"> </text></g><g id="g147"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="144" fill="#ffffff"> ZRANGE </text><text x="280" textLength="8" fill="#000000"> </text><text x="288" textLength="240" class="foreground"> </text></g><g id="g148"><text x="0" textLength="144" class="foreground">127.0.0.1:6379> zs</text><text x="144" textLength="8" class="background">c</text><text x="152" textLength="64" fill="#626262">an kkk 0</text><text x="216" textLength="312" class="foreground"> </text></g><g id="g149"><text x="0" textLength="144" class="foreground"> </text><text x="144" textLength="64" fill="#ffffff"> ZSCAN </text><text x="208" textLength="8" fill="#000000"> </text><text x="216" textLength="312" class="foreground"> </text></g><g id="g150"><text x="8" textLength="136" class="foreground"> </text><text x="144" textLength="64" fill="#ffffff"> ZSCORE </text><text x="208" textLength="8" fill="#000000"> </text><text x="216" textLength="312" class="foreground"> </text></g><g id="g151"><text x="0" textLength="152" class="foreground">127.0.0.1:6379> zsc</text><text x="152" textLength="8" class="background">a</text><text x="160" textLength="56" fill="#626262">n kkk 0</text><text x="216" textLength="312" class="foreground"> </text></g><g id="g152"><text x="0" textLength="152" class="foreground"> </text><text x="152" textLength="64" fill="#ffffff"> ZSCAN </text><text x="216" textLength="8" fill="#000000"> </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g153"><text x="8" textLength="144" class="foreground"> </text><text x="152" textLength="64" fill="#ffffff"> ZSCORE </text><text x="216" textLength="8" fill="#000000"> </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g154"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="40" font-weight="bold" fill="#008700">ZSCAN</text><text x="168" textLength="8" class="background"> </text><text x="176" textLength="352" class="foreground"> </text></g><g id="g155"><text x="0" textLength="152" class="foreground"> </text><text x="152" textLength="72" fill="#000000"> ZSCAN </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g156"><text x="0" textLength="104" font-weight="bold" fill="#d75f5f">(sorted_set) </text><text x="104" textLength="40" font-weight="bold" fill="#008700">ZSCAN</text><text x="144" textLength="32" fill="#5faf5f"> key</text><text x="176" textLength="56" fill="#af87ff"> cursor</text><text x="232" textLength="56" font-weight="bold" fill="#008700"> [MATCH</text><text x="288" textLength="64" font-weight="bold" fill="#5faf5f"> pattern</text><text x="352" textLength="64" font-weight="bold" fill="#008700">] [COUNT</text><text x="416" textLength="48" fill="#af87ff"> count</text><text x="464" textLength="8" font-weight="bold" fill="#008700">]</text><text x="472" textLength="56" fill="#af8700"> sinc</text></g><g id="g157"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="40" font-weight="bold" fill="#008700">ZSCAN</text><text x="168" textLength="8" class="foreground"> </text><text x="176" textLength="8" class="background">c</text><text x="184" textLength="40" fill="#626262">ars 0</text><text x="224" textLength="304" class="foreground"> </text></g><g id="g158"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="136" fill="#ffffff"> cars </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g159"><text x="8" textLength="168" class="foreground"> </text><text x="176" textLength="136" fill="#ffffff"> myset </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g160"><text x="8" textLength="168" class="foreground"> </text><text x="176" textLength="136" fill="#ffffff"> kkk </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g161"><text x="8" textLength="168" class="foreground"> </text><text x="176" textLength="136" fill="#ffffff"> somestream </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g162"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="136" fill="#ffffff"> af </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g163"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="136" fill="#ffffff"> list:animals </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g164"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="136" fill="#ffffff"> hash2 </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g165"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="40" font-weight="bold" fill="#008700">ZSCAN</text><text x="168" textLength="8" class="foreground"> </text><text x="176" textLength="32" fill="#5faf5f">cars</text><text x="208" textLength="8" class="background"> </text><text x="216" textLength="312" class="foreground"> </text></g><g id="g166"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="144" fill="#000000"> cars </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g167"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="40" font-weight="bold" fill="#008700">ZSCAN</text><text x="168" textLength="8" class="foreground"> </text><text x="176" textLength="32" fill="#5faf5f">cars</text><text x="208" textLength="320" class="foreground"> </text></g><g id="g168"><text x="0" textLength="64" fill="#878787">(error) </text><text x="64" textLength="360" font-weight="bold" fill="#ff005f">wrong number of arguments for 'zscan' command</text><text x="424" textLength="104" class="foreground"> </text></g><g id="g169"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="40" font-weight="bold" fill="#008700">ZSCAN</text><text x="168" textLength="8" class="foreground"> </text><text x="176" textLength="32" fill="#5faf5f">cars</text><text x="208" textLength="8" class="foreground"> </text><text x="216" textLength="8" class="background">0</text><text x="224" textLength="304" class="foreground"> </text></g><g id="g170"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="40" font-weight="bold" fill="#008700">ZSCAN</text><text x="168" textLength="8" class="foreground"> </text><text x="176" textLength="32" fill="#5faf5f">cars</text><text x="208" textLength="8" class="foreground"> </text><text x="216" textLength="8" fill="#af87ff">0</text><text x="224" textLength="8" class="background"> </text><text x="232" textLength="296" class="foreground"> </text></g><g id="g171"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="40" font-weight="bold" fill="#008700">ZSCAN</text><text x="168" textLength="8" class="foreground"> </text><text x="176" textLength="32" fill="#5faf5f">cars</text><text x="208" textLength="8" class="foreground"> </text><text x="216" textLength="8" fill="#af87ff">0</text><text x="224" textLength="304" class="foreground"> </text></g><g id="g172"><text x="0" textLength="72" fill="#878787">(cursor) </text><text x="72" textLength="8" fill="#af87ff">0</text><text x="80" textLength="448" class="foreground"> </text></g><g id="g173"><text x="0" textLength="24" class="foreground">1) </text><text x="24" textLength="136" fill="#af87ff">1367522908124000 </text><text x="160" textLength="96" fill="#ff8700">"robins-car"</text><text x="256" textLength="272" class="foreground"> </text></g><g id="g174"><text x="0" textLength="136" class="foreground">127.0.0.1:6379> t</text><text x="136" textLength="8" class="background">y</text><text x="144" textLength="56" fill="#626262">pe cars</text><text x="200" textLength="328" class="foreground"> </text></g><g id="g175"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="56" fill="#ffffff"> TOUCH </text><text x="192" textLength="8" fill="#000000"> </text><text x="200" textLength="328" class="foreground"> </text></g><g id="g176"><text x="0" textLength="144" class="foreground">127.0.0.1:6379> ty</text><text x="144" textLength="8" class="background">p</text><text x="152" textLength="48" fill="#626262">e cars</text><text x="200" textLength="328" class="foreground"> </text></g><g id="g177"><text x="0" textLength="152" class="foreground">127.0.0.1:6379> typ</text><text x="152" textLength="8" class="background">e</text><text x="160" textLength="40" fill="#626262"> cars</text><text x="200" textLength="328" class="foreground"> </text></g><g id="g178"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="background"> </text><text x="168" textLength="32" fill="#626262">cars</text><text x="200" textLength="328" class="foreground"> </text></g><g id="g179"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="8" class="background">c</text><text x="176" textLength="24" fill="#626262">ars</text><text x="200" textLength="328" class="foreground"> </text></g><g id="g180"><text x="0" textLength="168" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> cars </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g181"><text x="0" textLength="168" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> af </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g182"><text x="0" textLength="168" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> hash2 </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g183"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="8" fill="#5faf5f">l</text><text x="176" textLength="8" class="background">i</text><text x="184" textLength="80" fill="#626262">st:animals</text><text x="264" textLength="264" class="foreground"> </text></g><g id="g184"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">l</text><text x="192" textLength="88" fill="#444444">ist:animals</text><text x="280" textLength="32" fill="#ffffff"> </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g185"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">l</text><text x="192" textLength="104" fill="#444444">ist:buildings</text><text x="296" textLength="16" fill="#ffffff"> </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g186"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">l</text><text x="192" textLength="112" fill="#444444">ist:restaurant</text><text x="304" textLength="8" fill="#ffffff"> </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g187"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="16" fill="#444444">my</text><text x="200" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">l</text><text x="208" textLength="32" fill="#444444">ist1</text><text x="240" textLength="72" fill="#ffffff"> </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g188"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="32" fill="#444444">Sici</text><text x="216" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">l</text><text x="224" textLength="8" fill="#444444">y</text><text x="232" textLength="80" fill="#ffffff"> </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g189"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="16" fill="#5faf5f">li</text><text x="184" textLength="8" class="background">s</text><text x="192" textLength="72" fill="#626262">t:animals</text><text x="264" textLength="264" class="foreground"> </text></g><g id="g190"><text x="0" textLength="184" class="foreground"> </text><text x="184" textLength="8" fill="#ffffff"> </text><text x="192" textLength="16" font-weight="bold" text-decoration="underline" fill="#ffffff">li</text><text x="208" textLength="80" fill="#444444">st:animals</text><text x="288" textLength="32" fill="#ffffff"> </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g191"><text x="0" textLength="184" class="foreground"> </text><text x="184" textLength="8" fill="#ffffff"> </text><text x="192" textLength="16" font-weight="bold" text-decoration="underline" fill="#ffffff">li</text><text x="208" textLength="96" fill="#444444">st:buildings</text><text x="304" textLength="16" fill="#ffffff"> </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g192"><text x="0" textLength="184" class="foreground"> </text><text x="184" textLength="8" fill="#ffffff"> </text><text x="192" textLength="16" font-weight="bold" text-decoration="underline" fill="#ffffff">li</text><text x="208" textLength="104" fill="#444444">st:restaurant</text><text x="312" textLength="8" fill="#ffffff"> </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g193"><text x="0" textLength="184" class="foreground"> </text><text x="184" textLength="8" fill="#ffffff"> </text><text x="192" textLength="16" fill="#444444">my</text><text x="208" textLength="16" font-weight="bold" text-decoration="underline" fill="#ffffff">li</text><text x="224" textLength="24" fill="#444444">st1</text><text x="248" textLength="72" fill="#ffffff"> </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g194"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="96" fill="#5faf5f">list:animals</text><text x="264" textLength="8" class="background"> </text><text x="272" textLength="256" class="foreground"> </text></g><g id="g195"><text x="0" textLength="184" class="foreground"> </text><text x="184" textLength="8" fill="#000000"> </text><text x="192" textLength="16" text-decoration="underline" fill="#000000">li</text><text x="208" textLength="80" class="foreground">st:animals</text><text x="288" textLength="40" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g196"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="96" fill="#5faf5f">list:animals</text><text x="264" textLength="264" class="foreground"> </text></g><g id="g197"><text x="0" textLength="528" class="foreground">"list" </text></g><g id="g198"><text x="0" textLength="136" class="foreground">127.0.0.1:6379> l</text><text x="136" textLength="8" class="background">r</text><text x="144" textLength="168" fill="#626262">ange list:animals 0 6</text><text x="312" textLength="216" class="foreground"> </text></g><g id="g199"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="80" fill="#ffffff"> LSET </text><text x="216" textLength="8" fill="#000000"> </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g200"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="80" fill="#ffffff"> LREM </text><text x="216" textLength="8" fill="#000000"> </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g201"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="80" fill="#ffffff"> LPOP </text><text x="216" textLength="8" fill="#000000"> </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g202"><text x="8" textLength="128" class="foreground"> </text><text x="136" textLength="80" fill="#ffffff"> LLEN </text><text x="216" textLength="8" fill="#000000"> </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g203"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="80" fill="#ffffff"> LTRIM </text><text x="216" textLength="8" fill="#000000"> </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g204"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="80" fill="#ffffff"> LPUSH </text><text x="216" textLength="8" fill="#000000"> </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g205"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="80" fill="#ffffff"> LRANGE </text><text x="216" textLength="8" fill="#000000"> </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g206"><text x="0" textLength="144" class="foreground">127.0.0.1:6379> ll</text><text x="144" textLength="8" class="background">e</text><text x="152" textLength="96" fill="#626262">n testKeyDB2</text><text x="248" textLength="280" class="foreground"> </text></g><g id="g207"><text x="0" textLength="144" class="foreground"> </text><text x="144" textLength="56" fill="#ffffff"> LLEN </text><text x="200" textLength="8" fill="#000000"> </text><text x="208" textLength="320" class="foreground"> </text></g><g id="g208"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">LLEN</text><text x="160" textLength="8" class="background"> </text><text x="168" textLength="360" class="foreground"> </text></g><g id="g209"><text x="0" textLength="144" class="foreground"> </text><text x="144" textLength="64" fill="#000000"> LLEN </text><text x="208" textLength="320" class="foreground"> </text></g><g id="g210"><text x="0" textLength="56" font-weight="bold" fill="#d75f5f">(list) </text><text x="56" textLength="32" font-weight="bold" fill="#008700">LLEN</text><text x="88" textLength="32" fill="#5faf5f"> key</text><text x="120" textLength="120" fill="#af8700"> since: 1.0.0</text><text x="240" textLength="128" fill="#626262"> complexity:O(1)</text><text x="368" textLength="160" fill="#a8a8a8"> </text></g><g id="g211"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">LLEN</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="8" class="background">l</text><text x="176" textLength="88" fill="#626262">ist:animals</text><text x="264" textLength="264" class="foreground"> </text></g><g id="g212"><text x="0" textLength="168" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> list:animals </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g213"><text x="8" textLength="160" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> kkk </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g214"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">LLEN</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="8" fill="#5faf5f">i</text><text x="176" textLength="8" class="background"> </text><text x="184" textLength="344" class="foreground"> </text></g><g id="g215"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="8" fill="#444444">l</text><text x="192" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">i</text><text x="200" textLength="80" fill="#444444">st:animals</text><text x="280" textLength="32" fill="#ffffff"> </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g216"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="8" fill="#444444">l</text><text x="192" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">i</text><text x="200" textLength="96" fill="#444444">st:buildings</text><text x="296" textLength="16" fill="#ffffff"> </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g217"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="8" fill="#444444">S</text><text x="192" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">i</text><text x="200" textLength="32" fill="#444444">cily</text><text x="232" textLength="80" fill="#ffffff"> </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g218"><text x="8" textLength="168" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="8" fill="#444444">l</text><text x="192" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">i</text><text x="200" textLength="104" fill="#444444">st:restaurant</text><text x="304" textLength="8" fill="#ffffff"> </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g219"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="24" fill="#444444">myl</text><text x="208" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">i</text><text x="216" textLength="24" fill="#444444">st1</text><text x="240" textLength="72" fill="#ffffff"> </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g220"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">LLEN</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="8" class="background"> </text><text x="176" textLength="352" class="foreground"> </text></g><g id="g221"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">LLEN</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="8" fill="#5faf5f">l</text><text x="176" textLength="8" class="background">i</text><text x="184" textLength="80" fill="#626262">st:animals</text><text x="264" textLength="264" class="foreground"> </text></g><g id="g222"><text x="8" textLength="168" class="foreground"> </text><text x="176" textLength="8" fill="#ffffff"> </text><text x="184" textLength="16" fill="#444444">my</text><text x="200" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">l</text><text x="208" textLength="32" fill="#444444">ist1</text><text x="240" textLength="72" fill="#ffffff"> </text><text x="312" textLength="8" fill="#000000"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g223"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">LLEN</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="16" fill="#5faf5f">li</text><text x="184" textLength="8" class="background">s</text><text x="192" textLength="72" fill="#626262">t:animals</text><text x="264" textLength="264" class="foreground"> </text></g><g id="g224"><text x="8" textLength="176" class="foreground"> </text><text x="184" textLength="8" fill="#ffffff"> </text><text x="192" textLength="16" fill="#444444">my</text><text x="208" textLength="16" font-weight="bold" text-decoration="underline" fill="#ffffff">li</text><text x="224" textLength="24" fill="#444444">st1</text><text x="248" textLength="72" fill="#ffffff"> </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g225"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">LLEN</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="96" fill="#5faf5f">list:animals</text><text x="264" textLength="8" class="background"> </text><text x="272" textLength="256" class="foreground"> </text></g><g id="g226"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">LLEN</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="96" fill="#5faf5f">list:animals</text><text x="264" textLength="264" class="foreground"> </text></g><g id="g227"><text x="0" textLength="80" fill="#878787">(integer) </text><text x="80" textLength="448" class="foreground">51 </text></g><g id="g228"><text x="8" textLength="128" class="foreground"> </text><text x="136" textLength="80" fill="#ffffff"> LREM </text><text x="216" textLength="8" fill="#000000"> </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g229"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="80" fill="#ffffff"> LLEN </text><text x="216" textLength="8" fill="#000000"> </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g230"><text x="0" textLength="144" class="foreground">127.0.0.1:6379> lr</text><text x="144" textLength="8" class="background">a</text><text x="152" textLength="160" fill="#626262">nge list:animals 0 6</text><text x="312" textLength="216" class="foreground"> </text></g><g id="g231"><text x="0" textLength="144" class="foreground"> </text><text x="144" textLength="64" fill="#ffffff"> LREM </text><text x="208" textLength="8" fill="#000000"> </text><text x="216" textLength="312" class="foreground"> </text></g><g id="g232"><text x="8" textLength="136" class="foreground"> </text><text x="144" textLength="64" fill="#ffffff"> LRANGE </text><text x="208" textLength="8" fill="#000000"> </text><text x="216" textLength="312" class="foreground"> </text></g><g id="g233"><text x="0" textLength="152" class="foreground">127.0.0.1:6379> lra</text><text x="152" textLength="8" class="background">n</text><text x="160" textLength="152" fill="#626262">ge list:animals 0 6</text><text x="312" textLength="216" class="foreground"> </text></g><g id="g234"><text x="0" textLength="152" class="foreground"> </text><text x="152" textLength="64" fill="#ffffff"> LRANGE </text><text x="216" textLength="8" fill="#000000"> </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g235"><text x="0" textLength="160" class="foreground">127.0.0.1:6379> lran</text><text x="160" textLength="8" class="background">g</text><text x="168" textLength="144" fill="#626262">e list:animals 0 6</text><text x="312" textLength="216" class="foreground"> </text></g><g id="g236"><text x="0" textLength="160" class="foreground"> </text><text x="160" textLength="64" fill="#ffffff"> LRANGE </text><text x="224" textLength="8" fill="#000000"> </text><text x="232" textLength="296" class="foreground"> </text></g><g id="g237"><text x="0" textLength="168" class="foreground">127.0.0.1:6379> lrang</text><text x="168" textLength="8" class="background">e</text><text x="176" textLength="136" fill="#626262"> list:animals 0 6</text><text x="312" textLength="216" class="foreground"> </text></g><g id="g238"><text x="0" textLength="168" class="foreground"> </text><text x="168" textLength="64" fill="#ffffff"> LRANGE </text><text x="232" textLength="8" fill="#000000"> </text><text x="240" textLength="288" class="foreground"> </text></g><g id="g239"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="48" font-weight="bold" fill="#008700">lrange</text><text x="176" textLength="8" class="background"> </text><text x="184" textLength="128" fill="#626262">list:animals 0 6</text><text x="312" textLength="216" class="foreground"> </text></g><g id="g240"><text x="0" textLength="176" class="foreground"> </text><text x="176" textLength="64" fill="#ffffff"> LRANGE </text><text x="240" textLength="8" fill="#000000"> </text><text x="248" textLength="280" class="foreground"> </text></g><g id="g241"><text x="0" textLength="56" font-weight="bold" fill="#d75f5f">(list) </text><text x="56" textLength="48" font-weight="bold" fill="#008700">LRANGE</text><text x="104" textLength="32" fill="#5faf5f"> key</text><text x="136" textLength="88" fill="#af87ff"> start stop</text><text x="224" textLength="120" fill="#af8700"> since: 1.0.0</text><text x="344" textLength="184" fill="#626262"> complexity:O(S+N) wher</text></g><g id="g242"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="48" font-weight="bold" fill="#008700">lrange</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="8" class="background">l</text><text x="192" textLength="120" fill="#626262">ist:animals 0 6</text><text x="312" textLength="216" class="foreground"> </text></g><g id="g243"><text x="0" textLength="184" class="foreground"> </text><text x="184" textLength="136" fill="#ffffff"> list:animals </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g244"><text x="8" textLength="176" class="foreground"> </text><text x="184" textLength="136" fill="#ffffff"> cars </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g245"><text x="0" textLength="184" class="foreground"> </text><text x="184" textLength="136" fill="#ffffff"> myset </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g246"><text x="0" textLength="184" class="foreground"> </text><text x="184" textLength="136" fill="#ffffff"> kkk </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g247"><text x="0" textLength="184" class="foreground"> </text><text x="184" textLength="136" fill="#ffffff"> somestream </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g248"><text x="0" textLength="184" class="foreground"> </text><text x="184" textLength="136" fill="#ffffff"> af </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g249"><text x="0" textLength="184" class="foreground"> </text><text x="184" textLength="136" fill="#ffffff"> hash2 </text><text x="320" textLength="8" fill="#000000"> </text><text x="328" textLength="200" class="foreground"> </text></g><g id="g250"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="48" font-weight="bold" fill="#008700">lrange</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="8" fill="#5faf5f">l</text><text x="192" textLength="8" class="background">i</text><text x="200" textLength="112" fill="#626262">st:animals 0 6</text><text x="312" textLength="216" class="foreground"> </text></g><g id="g251"><text x="0" textLength="192" class="foreground"> </text><text x="192" textLength="8" fill="#ffffff"> </text><text x="200" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">l</text><text x="208" textLength="88" fill="#444444">ist:animals</text><text x="296" textLength="32" fill="#ffffff"> </text><text x="328" textLength="8" fill="#000000"> </text><text x="336" textLength="192" class="foreground"> </text></g><g id="g252"><text x="8" textLength="184" class="foreground"> </text><text x="192" textLength="8" fill="#ffffff"> </text><text x="200" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">l</text><text x="208" textLength="104" fill="#444444">ist:buildings</text><text x="312" textLength="16" fill="#ffffff"> </text><text x="328" textLength="8" fill="#000000"> </text><text x="336" textLength="192" class="foreground"> </text></g><g id="g253"><text x="0" textLength="192" class="foreground"> </text><text x="192" textLength="8" fill="#ffffff"> </text><text x="200" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">l</text><text x="208" textLength="112" fill="#444444">ist:restaurant</text><text x="320" textLength="8" fill="#ffffff"> </text><text x="328" textLength="8" fill="#000000"> </text><text x="336" textLength="192" class="foreground"> </text></g><g id="g254"><text x="0" textLength="192" class="foreground"> </text><text x="192" textLength="8" fill="#ffffff"> </text><text x="200" textLength="16" fill="#444444">my</text><text x="216" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">l</text><text x="224" textLength="32" fill="#444444">ist1</text><text x="256" textLength="72" fill="#ffffff"> </text><text x="328" textLength="8" fill="#000000"> </text><text x="336" textLength="192" class="foreground"> </text></g><g id="g255"><text x="0" textLength="192" class="foreground"> </text><text x="192" textLength="8" fill="#ffffff"> </text><text x="200" textLength="32" fill="#444444">Sici</text><text x="232" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">l</text><text x="240" textLength="8" fill="#444444">y</text><text x="248" textLength="80" fill="#ffffff"> </text><text x="328" textLength="8" fill="#000000"> </text><text x="336" textLength="192" class="foreground"> </text></g><g id="g256"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="48" font-weight="bold" fill="#008700">lrange</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="96" fill="#5faf5f">list:animals</text><text x="280" textLength="8" class="background"> </text><text x="288" textLength="240" class="foreground"> </text></g><g id="g257"><text x="0" textLength="192" class="foreground"> </text><text x="192" textLength="8" fill="#000000"> </text><text x="200" textLength="8" text-decoration="underline" fill="#000000">l</text><text x="208" textLength="88" class="foreground">ist:animals</text><text x="296" textLength="40" fill="#000000"> </text><text x="336" textLength="192" class="foreground"> </text></g><g id="g258"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="48" font-weight="bold" fill="#008700">lrange</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="96" fill="#5faf5f">list:animals</text><text x="280" textLength="8" class="foreground"> </text><text x="288" textLength="8" class="background">0</text><text x="296" textLength="16" fill="#626262"> 6</text><text x="312" textLength="216" class="foreground"> </text></g><g id="g259"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="48" font-weight="bold" fill="#008700">lrange</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="96" fill="#5faf5f">list:animals</text><text x="280" textLength="8" class="foreground"> </text><text x="288" textLength="8" fill="#af87ff">0</text><text x="296" textLength="8" class="background"> </text><text x="304" textLength="8" fill="#626262">6</text><text x="312" textLength="216" class="foreground"> </text></g><g id="g260"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="48" font-weight="bold" fill="#008700">lrange</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="96" fill="#5faf5f">list:animals</text><text x="280" textLength="8" class="foreground"> </text><text x="288" textLength="8" fill="#af87ff">0</text><text x="296" textLength="8" class="foreground"> </text><text x="304" textLength="8" class="background">6</text><text x="312" textLength="216" class="foreground"> </text></g><g id="g261"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="48" font-weight="bold" fill="#008700">lrange</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="96" fill="#5faf5f">list:animals</text><text x="280" textLength="8" class="foreground"> </text><text x="288" textLength="8" fill="#af87ff">0</text><text x="296" textLength="8" class="foreground"> </text><text x="304" textLength="8" fill="#af87ff">6</text><text x="312" textLength="8" class="background"> </text><text x="320" textLength="208" class="foreground"> </text></g><g id="g262"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="48" font-weight="bold" fill="#008700">lrange</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="96" fill="#5faf5f">list:animals</text><text x="280" textLength="8" class="foreground"> </text><text x="288" textLength="8" fill="#af87ff">0</text><text x="296" textLength="8" class="foreground"> </text><text x="304" textLength="8" fill="#af87ff">6</text><text x="312" textLength="216" class="foreground"> </text></g><g id="g263"><text x="0" textLength="24" class="foreground">1) </text><text x="24" textLength="48" fill="#ff8700">"wolf"</text><text x="72" textLength="456" class="foreground"> </text></g><g id="g264"><text x="0" textLength="24" class="foreground">2) </text><text x="24" textLength="64" fill="#ff8700">"turtle"</text><text x="88" textLength="440" class="foreground"> </text></g><g id="g265"><text x="0" textLength="24" class="foreground">3) </text><text x="24" textLength="56" fill="#ff8700">"tiger"</text><text x="80" textLength="448" class="foreground"> </text></g><g id="g266"><text x="0" textLength="24" class="foreground">4) </text><text x="24" textLength="80" fill="#ff8700">"squirrel"</text><text x="104" textLength="424" class="foreground"> </text></g><g id="g267"><text x="0" textLength="24" class="foreground">5) </text><text x="24" textLength="64" fill="#ff8700">"spider"</text><text x="88" textLength="440" class="foreground"> </text></g><g id="g268"><text x="0" textLength="24" class="foreground">6) </text><text x="24" textLength="56" fill="#ff8700">"snake"</text><text x="80" textLength="448" class="foreground"> </text></g><g id="g269"><text x="0" textLength="24" class="foreground">7) </text><text x="24" textLength="56" fill="#ff8700">"snail"</text><text x="80" textLength="448" class="foreground"> </text></g><g id="g270"><text x="0" textLength="136" class="foreground">127.0.0.1:6379> t</text><text x="136" textLength="8" class="background">y</text><text x="144" textLength="120" fill="#626262">pe list:animals</text><text x="264" textLength="264" class="foreground"> </text></g><g id="g271"><text x="0" textLength="144" class="foreground">127.0.0.1:6379> ty</text><text x="144" textLength="8" class="background">p</text><text x="152" textLength="112" fill="#626262">e list:animals</text><text x="264" textLength="264" class="foreground"> </text></g><g id="g272"><text x="0" textLength="152" class="foreground">127.0.0.1:6379> typ</text><text x="152" textLength="8" class="background">e</text><text x="160" textLength="104" fill="#626262"> list:animals</text><text x="264" textLength="264" class="foreground"> </text></g><g id="g273"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="background"> </text><text x="168" textLength="96" fill="#626262">list:animals</text><text x="264" textLength="264" class="foreground"> </text></g><g id="g274"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="8" class="background">l</text><text x="176" textLength="88" fill="#626262">ist:animals</text><text x="264" textLength="264" class="foreground"> </text></g><g id="g275"><text x="136" textLength="32" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> list:animals </text><text x="304" textLength="8" fill="#000000"> </text></g><g id="g276"><text x="8" textLength="160" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> cars </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g277"><text x="8" textLength="160" class="foreground"> </text><text x="168" textLength="136" fill="#ffffff"> myset </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g278"><text x="168" textLength="136" fill="#ffffff"> somestream </text><text x="304" textLength="8" fill="#000000"> </text></g><g id="g279"><text x="168" textLength="136" fill="#ffffff"> af </text><text x="304" textLength="8" fill="#000000"> </text></g><g id="g280"><text x="136" textLength="32" class="foreground"> </text><text x="168" textLength="144" fill="#000000"> list:animals </text></g><g id="g281"><text x="8" textLength="160" class="foreground"> </text><text x="168" textLength="144" fill="#000000"> cars </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g282"><text x="8" textLength="160" class="foreground"> </text><text x="168" textLength="144" fill="#000000"> myset </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g283"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="24" fill="#5faf5f">kkk</text><text x="192" textLength="8" class="background"> </text><text x="200" textLength="328" class="foreground"> </text></g><g id="g284"><text x="8" textLength="160" class="foreground"> </text><text x="168" textLength="144" fill="#000000"> kkk </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g285"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="80" fill="#5faf5f">somestream</text><text x="248" textLength="8" class="background"> </text><text x="256" textLength="272" class="foreground"> </text></g><g id="g286"><text x="168" textLength="144" fill="#000000"> somestream </text></g><g id="g287"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="16" fill="#5faf5f">af</text><text x="184" textLength="8" class="background"> </text><text x="192" textLength="336" class="foreground"> </text></g><g id="g288"><text x="168" textLength="144" fill="#000000"> af </text></g><g id="g289"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="32" font-weight="bold" fill="#008700">type</text><text x="160" textLength="8" class="foreground"> </text><text x="168" textLength="16" fill="#5faf5f">af</text><text x="184" textLength="344" class="foreground"> </text></g><g id="g290"><text x="0" textLength="528" class="foreground">"string" </text></g><g id="g291"><text x="168" textLength="144" class="foreground"> </text></g><g id="g292"><text x="0" textLength="136" class="foreground">127.0.0.1:6379> g</text><text x="136" textLength="8" class="background">e</text><text x="144" textLength="40" fill="#626262">t foo</text><text x="184" textLength="344" class="foreground"> </text></g><g id="g293"><text x="8" textLength="128" class="foreground"> </text><text x="136" textLength="152" fill="#ffffff"> GET </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g294"><text x="8" textLength="128" class="foreground"> </text><text x="136" textLength="152" fill="#ffffff"> GETSET </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g295"><text x="136" textLength="152" fill="#ffffff"> GETBIT </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="16" class="foreground"> </text></g><g id="g296"><text x="136" textLength="152" fill="#ffffff"> GEOPOS </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="16" class="foreground"> </text></g><g id="g297"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="152" fill="#ffffff"> GEOADD </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g298"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="152" fill="#ffffff"> GEOHASH </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g299"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="152" fill="#ffffff"> GEODIST </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g300"><text x="0" textLength="144" class="foreground">127.0.0.1:6379> ge</text><text x="144" textLength="8" class="background">t</text><text x="152" textLength="32" fill="#626262"> foo</text><text x="184" textLength="344" class="foreground"> </text></g><g id="g301"><text x="8" textLength="136" class="foreground"> </text><text x="144" textLength="152" fill="#ffffff"> GET </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g302"><text x="8" textLength="136" class="foreground"> </text><text x="144" textLength="152" fill="#ffffff"> GETSET </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g303"><text x="136" textLength="8" class="foreground"> </text><text x="144" textLength="152" fill="#ffffff"> GETBIT </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="8" class="foreground"> </text></g><g id="g304"><text x="136" textLength="8" class="foreground"> </text><text x="144" textLength="152" fill="#ffffff"> GEOPOS </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="8" class="foreground"> </text></g><g id="g305"><text x="0" textLength="144" class="foreground"> </text><text x="144" textLength="152" fill="#ffffff"> GEOADD </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g306"><text x="0" textLength="144" class="foreground"> </text><text x="144" textLength="152" fill="#ffffff"> GEOHASH </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g307"><text x="0" textLength="144" class="foreground"> </text><text x="144" textLength="152" fill="#ffffff"> GEODIST </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g308"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="background"> </text><text x="160" textLength="24" fill="#626262">foo</text><text x="184" textLength="344" class="foreground"> </text></g><g id="g309"><text x="8" textLength="144" class="foreground"> </text><text x="152" textLength="80" fill="#ffffff"> GET </text><text x="232" textLength="8" fill="#000000"> </text><text x="240" textLength="288" class="foreground"> </text></g><g id="g310"><text x="8" textLength="144" class="foreground"> </text><text x="152" textLength="80" fill="#ffffff"> GETSET </text><text x="232" textLength="8" fill="#000000"> </text><text x="240" textLength="288" class="foreground"> </text></g><g id="g311"><text x="136" textLength="16" class="foreground"> </text><text x="152" textLength="80" fill="#ffffff"> GETBIT </text><text x="232" textLength="8" fill="#000000"> </text><text x="240" textLength="288" class="foreground"> </text></g><g id="g312"><text x="136" textLength="16" class="foreground"> </text><text x="152" textLength="80" fill="#ffffff"> GETRANGE </text><text x="232" textLength="8" fill="#000000"> </text><text x="240" textLength="288" class="foreground"> </text></g><g id="g313"><text x="0" textLength="72" font-weight="bold" fill="#d75f5f">(string) </text><text x="72" textLength="24" font-weight="bold" fill="#008700">GET</text><text x="96" textLength="32" fill="#5faf5f"> key</text><text x="128" textLength="120" fill="#af8700"> since: 1.0.0</text><text x="248" textLength="128" fill="#626262"> complexity:O(1)</text><text x="376" textLength="152" fill="#a8a8a8"> </text></g><g id="g314"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="8" class="background">f</text><text x="168" textLength="16" fill="#626262">oo</text><text x="184" textLength="344" class="foreground"> </text></g><g id="g315"><text x="8" textLength="152" class="foreground"> </text><text x="160" textLength="136" fill="#ffffff"> af </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g316"><text x="8" textLength="152" class="foreground"> </text><text x="160" textLength="136" fill="#ffffff"> list:animals </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g317"><text x="136" textLength="24" class="foreground"> </text><text x="160" textLength="136" fill="#ffffff"> cars </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g318"><text x="136" textLength="24" class="foreground"> </text><text x="160" textLength="136" fill="#ffffff"> myset </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g319"><text x="0" textLength="160" class="foreground"> </text><text x="160" textLength="136" fill="#ffffff"> kkk </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g320"><text x="0" textLength="160" class="foreground"> </text><text x="160" textLength="136" fill="#ffffff"> somestream </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g321"><text x="0" textLength="160" class="foreground"> </text><text x="160" textLength="136" fill="#ffffff"> hash2 </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g322"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="8" fill="#5faf5f">a</text><text x="168" textLength="8" class="background"> </text><text x="176" textLength="352" class="foreground"> </text></g><g id="g323"><text x="8" textLength="160" class="foreground"> </text><text x="168" textLength="8" fill="#ffffff"> </text><text x="176" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">a</text><text x="184" textLength="8" fill="#444444">f</text><text x="192" textLength="112" fill="#ffffff"> </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g324"><text x="8" textLength="160" class="foreground"> </text><text x="168" textLength="8" fill="#ffffff"> </text><text x="176" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">a</text><text x="184" textLength="120" fill="#ffffff"> </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g325"><text x="136" textLength="32" class="foreground"> </text><text x="168" textLength="8" fill="#ffffff"> </text><text x="176" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">a</text><text x="184" textLength="16" fill="#444444">bc</text><text x="200" textLength="104" fill="#ffffff"> </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g326"><text x="136" textLength="32" class="foreground"> </text><text x="168" textLength="8" fill="#ffffff"> </text><text x="176" textLength="8" fill="#444444">c</text><text x="184" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">a</text><text x="192" textLength="16" fill="#444444">rs</text><text x="208" textLength="96" fill="#ffffff"> </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g327"><text x="0" textLength="168" class="foreground"> </text><text x="168" textLength="8" fill="#ffffff"> </text><text x="176" textLength="8" fill="#444444">h</text><text x="184" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">a</text><text x="192" textLength="24" fill="#444444">sh2</text><text x="216" textLength="88" fill="#ffffff"> </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g328"><text x="0" textLength="168" class="foreground"> </text><text x="168" textLength="8" fill="#ffffff"> </text><text x="176" textLength="8" fill="#444444">h</text><text x="184" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">a</text><text x="192" textLength="24" fill="#444444">sh3</text><text x="216" textLength="88" fill="#ffffff"> </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g329"><text x="0" textLength="168" class="foreground"> </text><text x="168" textLength="8" fill="#ffffff"> </text><text x="176" textLength="8" fill="#444444">h</text><text x="184" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">a</text><text x="192" textLength="24" fill="#444444">sh1</text><text x="216" textLength="88" fill="#ffffff"> </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g330"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="background"> </text><text x="184" textLength="344" class="foreground"> </text></g><g id="g331"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="352" class="foreground"> </text></g><g id="g332"><text x="0" textLength="528" class="foreground">"asdf" </text></g><g id="g333"><text x="0" textLength="136" class="foreground">127.0.0.1:6379> g</text><text x="136" textLength="8" class="background">e</text><text x="144" textLength="32" fill="#626262">t af</text><text x="176" textLength="352" class="foreground"> </text></g><g id="g334"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="152" fill="#ffffff"> GETBIT </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g335"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="152" fill="#ffffff"> GEOPOS </text><text x="288" textLength="8" fill="#000000"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g336"><text x="0" textLength="144" class="foreground">127.0.0.1:6379> ge</text><text x="144" textLength="8" class="background">t</text><text x="152" textLength="24" fill="#626262"> af</text><text x="176" textLength="352" class="foreground"> </text></g><g id="g337"><text x="0" textLength="144" class="foreground"> </text><text x="144" textLength="152" fill="#ffffff"> GETBIT </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g338"><text x="0" textLength="144" class="foreground"> </text><text x="144" textLength="152" fill="#ffffff"> GEOPOS </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g339"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="background"> </text><text x="160" textLength="16" fill="#626262">af</text><text x="176" textLength="352" class="foreground"> </text></g><g id="g340"><text x="0" textLength="152" class="foreground"> </text><text x="152" textLength="80" fill="#ffffff"> GETBIT </text><text x="232" textLength="8" fill="#000000"> </text><text x="240" textLength="288" class="foreground"> </text></g><g id="g341"><text x="0" textLength="152" class="foreground"> </text><text x="152" textLength="80" fill="#ffffff"> GETRANGE </text><text x="232" textLength="8" fill="#000000"> </text><text x="240" textLength="288" class="foreground"> </text></g><g id="g342"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="8" class="background">a</text><text x="168" textLength="8" fill="#626262">f</text><text x="176" textLength="352" class="foreground"> </text></g><g id="g343"><text x="0" textLength="160" class="foreground"> </text><text x="160" textLength="136" fill="#ffffff"> cars </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g344"><text x="0" textLength="160" class="foreground"> </text><text x="160" textLength="136" fill="#ffffff"> myset </text><text x="296" textLength="8" fill="#000000"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g345"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="8" fill="#5faf5f">a</text><text x="168" textLength="8" class="background">f</text><text x="176" textLength="352" class="foreground"> </text></g><g id="g346"><text x="0" textLength="168" class="foreground"> </text><text x="168" textLength="8" fill="#ffffff"> </text><text x="176" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">a</text><text x="184" textLength="16" fill="#444444">bc</text><text x="200" textLength="104" fill="#ffffff"> </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g347"><text x="0" textLength="168" class="foreground"> </text><text x="168" textLength="8" fill="#ffffff"> </text><text x="176" textLength="8" fill="#444444">c</text><text x="184" textLength="8" font-weight="bold" text-decoration="underline" fill="#ffffff">a</text><text x="192" textLength="16" fill="#444444">rs</text><text x="208" textLength="96" fill="#ffffff"> </text><text x="304" textLength="8" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g348"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="8" class="background"> </text><text x="192" textLength="336" class="foreground"> </text></g><g id="g349"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="8" fill="#000000">i</text><text x="192" textLength="8" class="background"> </text><text x="200" textLength="328" class="foreground"> </text></g><g id="g350"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="16" fill="#000000">in</text><text x="200" textLength="8" class="background"> </text><text x="208" textLength="320" class="foreground"> </text></g><g id="g351"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="24" fill="#000000">inv</text><text x="208" textLength="8" class="background"> </text><text x="216" textLength="312" class="foreground"> </text></g><g id="g352"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="32" fill="#000000">inva</text><text x="216" textLength="8" class="background"> </text><text x="224" textLength="304" class="foreground"> </text></g><g id="g353"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="40" fill="#000000">inval</text><text x="224" textLength="8" class="background"> </text><text x="232" textLength="296" class="foreground"> </text></g><g id="g354"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="48" fill="#000000">invali</text><text x="232" textLength="8" class="background"> </text><text x="240" textLength="288" class="foreground"> </text></g><g id="g355"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="56" fill="#000000">invalid</text><text x="240" textLength="8" class="background"> </text><text x="248" textLength="280" class="foreground"> </text></g><g id="g356"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="64" fill="#000000">invalide</text><text x="248" textLength="8" class="background"> </text><text x="256" textLength="272" class="foreground"> </text></g><g id="g357"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="72" fill="#000000">invalide </text><text x="256" textLength="8" class="background"> </text><text x="264" textLength="264" class="foreground"> </text></g><g id="g358"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="80" fill="#000000">invalide i</text><text x="264" textLength="8" class="background"> </text><text x="272" textLength="256" class="foreground"> </text></g><g id="g359"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="88" fill="#000000">invalide in</text><text x="272" textLength="8" class="background"> </text><text x="280" textLength="248" class="foreground"> </text></g><g id="g360"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="96" fill="#000000">invalide inp</text><text x="280" textLength="8" class="background"> </text><text x="288" textLength="240" class="foreground"> </text></g><g id="g361"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="104" fill="#000000">invalide inpu</text><text x="288" textLength="8" class="background"> </text><text x="296" textLength="232" class="foreground"> </text></g><g id="g362"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="foreground"> </text><text x="184" textLength="112" fill="#000000">invalide input</text><text x="296" textLength="8" class="background"> </text><text x="304" textLength="224" class="foreground"> </text></g><g id="g363"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">get</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="8" class="background"> </text><text x="168" textLength="360" class="foreground"> </text></g><g id="g364"><text x="0" textLength="144" class="foreground">127.0.0.1:6379> ge</text><text x="144" textLength="8" class="background"> </text><text x="152" textLength="376" class="foreground"> </text></g><g id="g365"><text x="0" textLength="136" class="foreground">127.0.0.1:6379> g</text><text x="136" textLength="8" class="background"> </text><text x="144" textLength="384" class="foreground"> </text></g><g id="g366"><text x="0" textLength="136" class="foreground">127.0.0.1:6379> d</text><text x="136" textLength="8" class="background">e</text><text x="144" textLength="200" fill="#626262">l _kombu.binding.celeryev</text><text x="344" textLength="184" class="foreground"> </text></g><g id="g367"><text x="8" textLength="128" class="foreground"> </text><text x="136" textLength="128" fill="#ffffff"> DEL </text><text x="264" textLength="8" fill="#000000"> </text><text x="272" textLength="256" class="foreground"> </text></g><g id="g368"><text x="8" textLength="128" class="foreground"> </text><text x="136" textLength="128" fill="#ffffff"> DECR </text><text x="264" textLength="8" fill="#000000"> </text><text x="272" textLength="256" class="foreground"> </text></g><g id="g369"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="128" fill="#ffffff"> DUMP </text><text x="264" textLength="8" fill="#000000"> </text><text x="272" textLength="256" class="foreground"> </text></g><g id="g370"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="128" fill="#ffffff"> DECRBY </text><text x="264" textLength="8" fill="#000000"> </text><text x="272" textLength="256" class="foreground"> </text></g><g id="g371"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="128" fill="#ffffff"> DBSIZE </text><text x="264" textLength="8" fill="#000000"> </text><text x="272" textLength="256" class="foreground"> </text></g><g id="g372"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="128" fill="#ffffff"> DISCARD </text><text x="264" textLength="8" fill="#000000"> </text><text x="272" textLength="256" class="foreground"> </text></g><g id="g373"><text x="0" textLength="136" class="foreground"> </text><text x="136" textLength="128" fill="#ffffff"> DEBUG OBJECT </text><text x="264" textLength="8" fill="#000000"> </text><text x="272" textLength="256" class="foreground"> </text></g><g id="g374"><text x="0" textLength="144" class="foreground">127.0.0.1:6379> de</text><text x="144" textLength="8" class="background">l</text><text x="152" textLength="192" fill="#626262"> _kombu.binding.celeryev</text><text x="344" textLength="184" class="foreground"> </text></g><g id="g375"><text x="8" textLength="136" class="foreground"> </text><text x="144" textLength="128" fill="#ffffff"> DEL </text><text x="272" textLength="8" fill="#000000"> </text><text x="280" textLength="248" class="foreground"> </text></g><g id="g376"><text x="8" textLength="136" class="foreground"> </text><text x="144" textLength="128" fill="#ffffff"> DECR </text><text x="272" textLength="8" fill="#000000"> </text><text x="280" textLength="248" class="foreground"> </text></g><g id="g377"><text x="0" textLength="144" class="foreground"> </text><text x="144" textLength="128" fill="#ffffff"> DECRBY </text><text x="272" textLength="8" fill="#000000"> </text><text x="280" textLength="248" class="foreground"> </text></g><g id="g378"><text x="0" textLength="144" class="foreground"> </text><text x="144" textLength="128" fill="#ffffff"> DEBUG OBJECT </text><text x="272" textLength="8" fill="#000000"> </text><text x="280" textLength="248" class="foreground"> </text></g><g id="g379"><text x="0" textLength="144" class="foreground"> </text><text x="144" textLength="128" fill="#ffffff"> DEBUG SEGFAULT </text><text x="272" textLength="8" fill="#000000"> </text><text x="280" textLength="248" class="foreground"> </text></g><g id="g380"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">del</text><text x="152" textLength="8" class="background"> </text><text x="160" textLength="184" fill="#626262">_kombu.binding.celeryev</text><text x="344" textLength="184" class="foreground"> </text></g><g id="g381"><text x="8" textLength="144" class="foreground"> </text><text x="152" textLength="56" fill="#ffffff"> DEL </text><text x="208" textLength="8" fill="#000000"> </text><text x="216" textLength="312" class="foreground"> </text></g><g id="g382"><text x="0" textLength="80" font-weight="bold" fill="#d75f5f">(generic) </text><text x="80" textLength="24" font-weight="bold" fill="#008700">DEL</text><text x="104" textLength="32" fill="#5faf5f"> key</text><text x="136" textLength="120" fill="#af8700"> since: 1.0.0</text><text x="256" textLength="272" fill="#626262"> complexity:O(N) where N is the nu</text></g><g id="g383"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">del</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="8" class="background">_</text><text x="168" textLength="176" fill="#626262">kombu.binding.celeryev</text><text x="344" textLength="184" class="foreground"> </text></g><g id="g384"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">del</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="8" fill="#5faf5f">a</text><text x="168" textLength="8" class="background">\</text><text x="176" textLength="24" fill="#626262">"bc</text><text x="200" textLength="328" class="foreground"> </text></g><g id="g385"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">del</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="8" class="background"> </text><text x="184" textLength="344" class="foreground"> </text></g><g id="g386"><text x="8" textLength="160" class="foreground"> </text><text x="168" textLength="8" fill="#000000"> </text><text x="176" textLength="8" text-decoration="underline" fill="#000000">a</text><text x="184" textLength="8" class="foreground">f</text><text x="192" textLength="120" fill="#000000"> </text><text x="312" textLength="216" class="foreground"> </text></g><g id="g387"><text x="0" textLength="128" class="foreground">127.0.0.1:6379> </text><text x="128" textLength="24" font-weight="bold" fill="#008700">del</text><text x="152" textLength="8" class="foreground"> </text><text x="160" textLength="16" fill="#5faf5f">af</text><text x="176" textLength="352" class="foreground"> </text></g><g id="g388"><text x="0" textLength="80" fill="#878787">(integer) </text><text x="80" textLength="448" class="foreground">1 </text></g><g id="g389"><text x="0" textLength="528" fill="#878787">127.0.0.1:6379> </text></g><g id="g390"><text x="0" textLength="528" class="foreground">Goodbye! </text></g><g id="g391"><text x="0" textLength="528" class="foreground">$ </text></g></defs><g id="screen_view"><g><rect x="0" y="0" width="8" height="17" class="foreground"/><use xlink:href="#g1" y="0"/></g><g><rect x="16" y="374" width="8" height="17" class="foreground"/><use xlink:href="#g2" y="374"/></g><g><rect x="24" y="748" width="8" height="17" class="foreground"/><use xlink:href="#g3" y="748"/></g><g><rect x="32" y="1122" width="8" height="17" class="foreground"/><use xlink:href="#g4" y="1122"/></g><g><rect x="40" y="1496" width="8" height="17" class="foreground"/><use xlink:href="#g5" y="1496"/></g><g><rect x="48" y="1870" width="8" height="17" class="foreground"/><use xlink:href="#g6" y="1870"/></g><g><rect x="56" y="2244" width="8" height="17" class="foreground"/><use xlink:href="#g7" y="2244"/></g><g><rect x="64" y="2618" width="8" height="17" class="foreground"/><use xlink:href="#g8" y="2618"/></g><g><use xlink:href="#g9" y="2992"/><rect x="0" y="3009" width="8" height="17" class="foreground"/><use xlink:href="#g10" y="3009"/></g><g><use xlink:href="#g9" y="3366"/><use xlink:href="#g11" y="3383"/><use xlink:href="#g12" y="3400"/><use xlink:href="#g13" y="3417"/><use xlink:href="#g14" y="3434"/><rect x="128" y="3451" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="3451"/><use xlink:href="#g16" y="3570"/><use xlink:href="#g16" y="3672"/><rect x="0" y="3689" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="3689"/></g><g><use xlink:href="#g9" y="3740"/><use xlink:href="#g11" y="3757"/><use xlink:href="#g12" y="3774"/><use xlink:href="#g13" y="3791"/><use xlink:href="#g14" y="3808"/><rect x="136" y="3825" width="8" height="17" class="foreground"/><use xlink:href="#g18" y="3825"/><rect x="136" y="3842" width="56" height="17" fill="#008787"/><rect x="192" y="3842" width="8" height="17" fill="#444444"/><use xlink:href="#g19" y="3842"/><use xlink:href="#g16" y="3944"/><use xlink:href="#g16" y="4046"/><rect x="0" y="4063" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="4063"/></g><g><use xlink:href="#g9" y="4114"/><use xlink:href="#g11" y="4131"/><use xlink:href="#g12" y="4148"/><use xlink:href="#g13" y="4165"/><use xlink:href="#g14" y="4182"/><rect x="144" y="4199" width="8" height="17" class="foreground"/><use xlink:href="#g20" y="4199"/><rect x="144" y="4216" width="56" height="17" fill="#008787"/><rect x="200" y="4216" width="8" height="17" fill="#444444"/><use xlink:href="#g21" y="4216"/><use xlink:href="#g16" y="4318"/><use xlink:href="#g16" y="4420"/><rect x="0" y="4437" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="4437"/></g><g><use xlink:href="#g9" y="4488"/><use xlink:href="#g11" y="4505"/><use xlink:href="#g12" y="4522"/><use xlink:href="#g13" y="4539"/><use xlink:href="#g14" y="4556"/><rect x="152" y="4573" width="8" height="17" class="foreground"/><use xlink:href="#g22" y="4573"/><rect x="152" y="4590" width="56" height="17" fill="#008787"/><rect x="208" y="4590" width="8" height="17" fill="#444444"/><use xlink:href="#g23" y="4590"/><use xlink:href="#g16" y="4692"/><use xlink:href="#g16" y="4794"/><rect x="0" y="4811" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="4811"/></g><g><use xlink:href="#g9" y="4862"/><use xlink:href="#g11" y="4879"/><use xlink:href="#g12" y="4896"/><use xlink:href="#g13" y="4913"/><use xlink:href="#g14" y="4930"/><rect x="160" y="4947" width="8" height="17" class="foreground"/><use xlink:href="#g24" y="4947"/><rect x="160" y="4964" width="56" height="17" fill="#008787"/><rect x="216" y="4964" width="8" height="17" fill="#444444"/><use xlink:href="#g25" y="4964"/><use xlink:href="#g16" y="5066"/><use xlink:href="#g16" y="5168"/><rect x="0" y="5185" width="528" height="17" fill="#262626"/><use xlink:href="#g26" y="5185"/></g><g><use xlink:href="#g9" y="5236"/><use xlink:href="#g11" y="5253"/><use xlink:href="#g12" y="5270"/><use xlink:href="#g13" y="5287"/><use xlink:href="#g14" y="5304"/><rect x="168" y="5321" width="8" height="17" class="foreground"/><use xlink:href="#g27" y="5321"/><use xlink:href="#g28" y="5338"/><use xlink:href="#g16" y="5440"/><use xlink:href="#g16" y="5542"/><rect x="0" y="5559" width="528" height="17" fill="#262626"/><use xlink:href="#g26" y="5559"/></g><g><use xlink:href="#g9" y="5610"/><use xlink:href="#g11" y="5627"/><use xlink:href="#g12" y="5644"/><use xlink:href="#g13" y="5661"/><use xlink:href="#g14" y="5678"/><rect x="176" y="5695" width="8" height="17" class="foreground"/><use xlink:href="#g29" y="5695"/><use xlink:href="#g28" y="5712"/><use xlink:href="#g16" y="5814"/><use xlink:href="#g16" y="5916"/><rect x="0" y="5933" width="528" height="17" fill="#262626"/><use xlink:href="#g26" y="5933"/></g><g><use xlink:href="#g30" y="5984"/><use xlink:href="#g31" y="6001"/><use xlink:href="#g32" y="6018"/><use xlink:href="#g33" y="6035"/><use xlink:href="#g34" y="6052"/><use xlink:href="#g35" y="6069"/><use xlink:href="#g36" y="6086"/><use xlink:href="#g37" y="6103"/><use xlink:href="#g38" y="6120"/><use xlink:href="#g39" y="6137"/><use xlink:href="#g40" y="6154"/><rect x="128" y="6171" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="6171"/><use xlink:href="#g16" y="6290"/><rect x="0" y="6307" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="6307"/></g><g><use xlink:href="#g30" y="6358"/><use xlink:href="#g31" y="6375"/><use xlink:href="#g32" y="6392"/><use xlink:href="#g33" y="6409"/><use xlink:href="#g34" y="6426"/><use xlink:href="#g35" y="6443"/><use xlink:href="#g36" y="6460"/><use xlink:href="#g37" y="6477"/><use xlink:href="#g38" y="6494"/><use xlink:href="#g39" y="6511"/><use xlink:href="#g40" y="6528"/><rect x="136" y="6545" width="8" height="17" class="foreground"/><use xlink:href="#g41" y="6545"/><rect x="136" y="6562" width="56" height="17" fill="#008787"/><rect x="192" y="6562" width="8" height="17" fill="#444444"/><use xlink:href="#g42" y="6562"/><rect x="136" y="6579" width="56" height="17" fill="#008787"/><rect x="192" y="6579" width="8" height="17" fill="#444444"/><use xlink:href="#g43" y="6579"/><rect x="136" y="6596" width="56" height="17" fill="#008787"/><rect x="192" y="6596" width="8" height="17" fill="#444444"/><use xlink:href="#g44" y="6596"/><rect x="136" y="6613" width="56" height="17" fill="#008787"/><rect x="192" y="6613" width="8" height="17" fill="#444444"/><use xlink:href="#g45" y="6613"/><use xlink:href="#g16" y="6664"/><rect x="0" y="6681" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="6681"/></g><g><use xlink:href="#g30" y="6732"/><use xlink:href="#g31" y="6749"/><use xlink:href="#g32" y="6766"/><use xlink:href="#g33" y="6783"/><use xlink:href="#g34" y="6800"/><use xlink:href="#g35" y="6817"/><use xlink:href="#g36" y="6834"/><use xlink:href="#g37" y="6851"/><use xlink:href="#g38" y="6868"/><use xlink:href="#g39" y="6885"/><use xlink:href="#g40" y="6902"/><rect x="144" y="6919" width="8" height="17" class="foreground"/><use xlink:href="#g46" y="6919"/><rect x="144" y="6936" width="56" height="17" fill="#008787"/><rect x="200" y="6936" width="8" height="17" fill="#444444"/><use xlink:href="#g47" y="6936"/><use xlink:href="#g28" y="6953"/><use xlink:href="#g28" y="6970"/><use xlink:href="#g28" y="6987"/><use xlink:href="#g16" y="7038"/><rect x="0" y="7055" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="7055"/></g><g><use xlink:href="#g30" y="7106"/><use xlink:href="#g31" y="7123"/><use xlink:href="#g32" y="7140"/><use xlink:href="#g33" y="7157"/><use xlink:href="#g34" y="7174"/><use xlink:href="#g35" y="7191"/><use xlink:href="#g36" y="7208"/><use xlink:href="#g37" y="7225"/><use xlink:href="#g38" y="7242"/><use xlink:href="#g39" y="7259"/><use xlink:href="#g40" y="7276"/><rect x="152" y="7293" width="8" height="17" class="foreground"/><use xlink:href="#g48" y="7293"/><rect x="152" y="7310" width="56" height="17" fill="#008787"/><rect x="208" y="7310" width="8" height="17" fill="#444444"/><use xlink:href="#g49" y="7310"/><use xlink:href="#g28" y="7327"/><use xlink:href="#g28" y="7344"/><use xlink:href="#g28" y="7361"/><use xlink:href="#g16" y="7412"/><rect x="0" y="7429" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="7429"/></g><g><use xlink:href="#g30" y="7480"/><use xlink:href="#g31" y="7497"/><use xlink:href="#g32" y="7514"/><use xlink:href="#g33" y="7531"/><use xlink:href="#g34" y="7548"/><use xlink:href="#g35" y="7565"/><use xlink:href="#g36" y="7582"/><use xlink:href="#g37" y="7599"/><use xlink:href="#g38" y="7616"/><use xlink:href="#g39" y="7633"/><use xlink:href="#g40" y="7650"/><rect x="160" y="7667" width="8" height="17" class="foreground"/><use xlink:href="#g50" y="7667"/><rect x="160" y="7684" width="56" height="17" fill="#008787"/><rect x="216" y="7684" width="8" height="17" fill="#444444"/><use xlink:href="#g51" y="7684"/><use xlink:href="#g28" y="7701"/><use xlink:href="#g28" y="7718"/><use xlink:href="#g28" y="7735"/><use xlink:href="#g16" y="7786"/><rect x="0" y="7803" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="7803"/></g><g><use xlink:href="#g30" y="7854"/><use xlink:href="#g31" y="7871"/><use xlink:href="#g32" y="7888"/><use xlink:href="#g33" y="7905"/><use xlink:href="#g34" y="7922"/><use xlink:href="#g35" y="7939"/><use xlink:href="#g36" y="7956"/><use xlink:href="#g37" y="7973"/><use xlink:href="#g38" y="7990"/><use xlink:href="#g39" y="8007"/><use xlink:href="#g40" y="8024"/><rect x="168" y="8041" width="8" height="17" class="foreground"/><use xlink:href="#g53" y="8041"/><rect x="168" y="8058" width="136" height="17" fill="#008787"/><rect x="304" y="8058" width="8" height="17" fill="#444444"/><use xlink:href="#g54" y="8058"/><rect x="168" y="8075" width="136" height="17" fill="#008787"/><rect x="304" y="8075" width="8" height="17" fill="#444444"/><use xlink:href="#g55" y="8075"/><rect x="168" y="8092" width="136" height="17" fill="#008787"/><rect x="304" y="8092" width="8" height="17" fill="#00afaf"/><use xlink:href="#g56" y="8092"/><rect x="168" y="8109" width="136" height="17" fill="#008787"/><rect x="304" y="8109" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g57" y="8109"/><rect x="168" y="8126" width="136" height="17" fill="#008787"/><rect x="304" y="8126" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g58" y="8126"/><rect x="168" y="8143" width="136" height="17" fill="#008787"/><rect x="304" y="8143" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g59" y="8143"/><rect x="168" y="8160" width="136" height="17" fill="#008787"/><rect x="304" y="8160" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g60" y="8160"/><rect x="0" y="8177" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="8177"/></g><g><use xlink:href="#g30" y="8228"/><use xlink:href="#g31" y="8245"/><use xlink:href="#g32" y="8262"/><use xlink:href="#g33" y="8279"/><use xlink:href="#g34" y="8296"/><use xlink:href="#g35" y="8313"/><use xlink:href="#g36" y="8330"/><use xlink:href="#g37" y="8347"/><use xlink:href="#g38" y="8364"/><use xlink:href="#g39" y="8381"/><use xlink:href="#g40" y="8398"/><rect x="176" y="8415" width="8" height="17" class="foreground"/><use xlink:href="#g61" y="8415"/><rect x="176" y="8432" width="112" height="17" fill="#008787"/><rect x="288" y="8432" width="8" height="17" fill="#444444"/><use xlink:href="#g62" y="8432"/><rect x="176" y="8449" width="112" height="17" fill="#008787"/><rect x="288" y="8449" width="8" height="17" fill="#444444"/><use xlink:href="#g63" y="8449"/><rect x="176" y="8466" width="112" height="17" fill="#008787"/><rect x="288" y="8466" width="8" height="17" fill="#444444"/><use xlink:href="#g64" y="8466"/><rect x="176" y="8483" width="112" height="17" fill="#008787"/><rect x="288" y="8483" width="8" height="17" fill="#444444"/><use xlink:href="#g65" y="8483"/><rect x="176" y="8500" width="112" height="17" fill="#008787"/><rect x="288" y="8500" width="8" height="17" fill="#444444"/><use xlink:href="#g66" y="8500"/><rect x="176" y="8517" width="112" height="17" fill="#008787"/><rect x="288" y="8517" width="8" height="17" fill="#444444"/><use xlink:href="#g67" y="8517"/><use xlink:href="#g16" y="8534"/><rect x="0" y="8551" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="8551"/></g><g><use xlink:href="#g30" y="8602"/><use xlink:href="#g31" y="8619"/><use xlink:href="#g32" y="8636"/><use xlink:href="#g33" y="8653"/><use xlink:href="#g34" y="8670"/><use xlink:href="#g35" y="8687"/><use xlink:href="#g36" y="8704"/><use xlink:href="#g37" y="8721"/><use xlink:href="#g38" y="8738"/><use xlink:href="#g39" y="8755"/><use xlink:href="#g40" y="8772"/><rect x="184" y="8789" width="8" height="17" class="foreground"/><use xlink:href="#g68" y="8789"/><rect x="184" y="8806" width="72" height="17" fill="#008787"/><rect x="256" y="8806" width="8" height="17" fill="#444444"/><use xlink:href="#g69" y="8806"/><rect x="184" y="8823" width="72" height="17" fill="#008787"/><rect x="256" y="8823" width="8" height="17" fill="#444444"/><use xlink:href="#g70" y="8823"/><rect x="184" y="8840" width="72" height="17" fill="#008787"/><rect x="256" y="8840" width="8" height="17" fill="#444444"/><use xlink:href="#g71" y="8840"/><use xlink:href="#g28" y="8857"/><use xlink:href="#g28" y="8874"/><use xlink:href="#g28" y="8891"/><use xlink:href="#g16" y="8908"/><rect x="0" y="8925" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="8925"/></g><g><use xlink:href="#g30" y="8976"/><use xlink:href="#g31" y="8993"/><use xlink:href="#g32" y="9010"/><use xlink:href="#g33" y="9027"/><use xlink:href="#g34" y="9044"/><use xlink:href="#g35" y="9061"/><use xlink:href="#g36" y="9078"/><use xlink:href="#g37" y="9095"/><use xlink:href="#g38" y="9112"/><use xlink:href="#g39" y="9129"/><use xlink:href="#g40" y="9146"/><rect x="208" y="9163" width="8" height="17" class="foreground"/><use xlink:href="#g72" y="9163"/><rect x="184" y="9180" width="72" height="17" fill="#ffffff"/><rect x="256" y="9180" width="8" height="17" fill="#444444"/><use xlink:href="#g73" y="9180"/><rect x="184" y="9197" width="72" height="17" fill="#008787"/><rect x="256" y="9197" width="8" height="17" fill="#444444"/><use xlink:href="#g70" y="9197"/><rect x="184" y="9214" width="72" height="17" fill="#008787"/><rect x="256" y="9214" width="8" height="17" fill="#444444"/><use xlink:href="#g71" y="9214"/><use xlink:href="#g28" y="9231"/><use xlink:href="#g28" y="9248"/><use xlink:href="#g28" y="9265"/><use xlink:href="#g16" y="9282"/><rect x="0" y="9299" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="9299"/></g><g><use xlink:href="#g32" y="9350"/><use xlink:href="#g33" y="9367"/><use xlink:href="#g34" y="9384"/><use xlink:href="#g35" y="9401"/><use xlink:href="#g36" y="9418"/><use xlink:href="#g37" y="9435"/><use xlink:href="#g38" y="9452"/><use xlink:href="#g39" y="9469"/><use xlink:href="#g40" y="9486"/><use xlink:href="#g74" y="9503"/><use xlink:href="#g75" y="9520"/><rect x="128" y="9537" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="9537"/><use xlink:href="#g28" y="9554"/><use xlink:href="#g28" y="9571"/><use xlink:href="#g28" y="9588"/><use xlink:href="#g28" y="9605"/><use xlink:href="#g16" y="9622"/><use xlink:href="#g16" y="9639"/><use xlink:href="#g16" y="9656"/><rect x="0" y="9673" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="9673"/></g><g><use xlink:href="#g32" y="9724"/><use xlink:href="#g33" y="9741"/><use xlink:href="#g34" y="9758"/><use xlink:href="#g35" y="9775"/><use xlink:href="#g36" y="9792"/><use xlink:href="#g37" y="9809"/><use xlink:href="#g38" y="9826"/><use xlink:href="#g39" y="9843"/><use xlink:href="#g40" y="9860"/><use xlink:href="#g74" y="9877"/><use xlink:href="#g75" y="9894"/><rect x="136" y="9911" width="8" height="17" class="foreground"/><use xlink:href="#g76" y="9911"/><rect x="136" y="9928" width="120" height="17" fill="#008787"/><rect x="256" y="9928" width="8" height="17" fill="#444444"/><use xlink:href="#g77" y="9928"/><rect x="136" y="9945" width="120" height="17" fill="#008787"/><rect x="256" y="9945" width="8" height="17" fill="#00afaf"/><use xlink:href="#g78" y="9945"/><rect x="136" y="9962" width="120" height="17" fill="#008787"/><rect x="256" y="9962" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g79" y="9962"/><rect x="136" y="9979" width="120" height="17" fill="#008787"/><rect x="256" y="9979" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g80" y="9979"/><rect x="136" y="9996" width="120" height="17" fill="#008787"/><rect x="256" y="9996" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g81" y="9996"/><rect x="136" y="10013" width="120" height="17" fill="#008787"/><rect x="256" y="10013" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g82" y="10013"/><rect x="136" y="10030" width="120" height="17" fill="#008787"/><rect x="256" y="10030" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g83" y="10030"/><rect x="0" y="10047" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="10047"/></g><g><use xlink:href="#g32" y="10098"/><use xlink:href="#g33" y="10115"/><use xlink:href="#g34" y="10132"/><use xlink:href="#g35" y="10149"/><use xlink:href="#g36" y="10166"/><use xlink:href="#g37" y="10183"/><use xlink:href="#g38" y="10200"/><use xlink:href="#g39" y="10217"/><use xlink:href="#g40" y="10234"/><use xlink:href="#g74" y="10251"/><use xlink:href="#g75" y="10268"/><rect x="144" y="10285" width="8" height="17" class="foreground"/><use xlink:href="#g84" y="10285"/><rect x="144" y="10302" width="80" height="17" fill="#008787"/><rect x="224" y="10302" width="8" height="17" fill="#444444"/><use xlink:href="#g85" y="10302"/><rect x="144" y="10319" width="80" height="17" fill="#008787"/><rect x="224" y="10319" width="8" height="17" fill="#444444"/><use xlink:href="#g86" y="10319"/><use xlink:href="#g28" y="10336"/><use xlink:href="#g28" y="10353"/><use xlink:href="#g16" y="10370"/><use xlink:href="#g16" y="10387"/><use xlink:href="#g16" y="10404"/><rect x="0" y="10421" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="10421"/></g><g><use xlink:href="#g32" y="10472"/><use xlink:href="#g33" y="10489"/><use xlink:href="#g34" y="10506"/><use xlink:href="#g35" y="10523"/><use xlink:href="#g36" y="10540"/><use xlink:href="#g37" y="10557"/><use xlink:href="#g38" y="10574"/><use xlink:href="#g39" y="10591"/><use xlink:href="#g40" y="10608"/><use xlink:href="#g74" y="10625"/><use xlink:href="#g75" y="10642"/><rect x="152" y="10659" width="8" height="17" class="foreground"/><use xlink:href="#g87" y="10659"/><rect x="152" y="10676" width="80" height="17" fill="#008787"/><rect x="232" y="10676" width="8" height="17" fill="#444444"/><use xlink:href="#g88" y="10676"/><use xlink:href="#g28" y="10693"/><use xlink:href="#g28" y="10710"/><use xlink:href="#g28" y="10727"/><use xlink:href="#g16" y="10744"/><use xlink:href="#g16" y="10761"/><use xlink:href="#g16" y="10778"/><rect x="0" y="10795" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="10795"/></g><g><use xlink:href="#g32" y="10846"/><use xlink:href="#g33" y="10863"/><use xlink:href="#g34" y="10880"/><use xlink:href="#g35" y="10897"/><use xlink:href="#g36" y="10914"/><use xlink:href="#g37" y="10931"/><use xlink:href="#g38" y="10948"/><use xlink:href="#g39" y="10965"/><use xlink:href="#g40" y="10982"/><use xlink:href="#g74" y="10999"/><use xlink:href="#g75" y="11016"/><rect x="160" y="11033" width="8" height="17" class="foreground"/><use xlink:href="#g89" y="11033"/><rect x="160" y="11050" width="80" height="17" fill="#008787"/><rect x="240" y="11050" width="8" height="17" fill="#444444"/><use xlink:href="#g90" y="11050"/><use xlink:href="#g28" y="11067"/><use xlink:href="#g28" y="11084"/><use xlink:href="#g28" y="11101"/><use xlink:href="#g16" y="11118"/><use xlink:href="#g16" y="11135"/><use xlink:href="#g16" y="11152"/><rect x="0" y="11169" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="11169"/></g><g><use xlink:href="#g32" y="11220"/><use xlink:href="#g33" y="11237"/><use xlink:href="#g34" y="11254"/><use xlink:href="#g35" y="11271"/><use xlink:href="#g36" y="11288"/><use xlink:href="#g37" y="11305"/><use xlink:href="#g38" y="11322"/><use xlink:href="#g39" y="11339"/><use xlink:href="#g40" y="11356"/><use xlink:href="#g74" y="11373"/><use xlink:href="#g75" y="11390"/><rect x="192" y="11407" width="8" height="17" class="foreground"/><use xlink:href="#g91" y="11407"/><rect x="160" y="11424" width="80" height="17" fill="#ffffff"/><rect x="240" y="11424" width="8" height="17" fill="#444444"/><use xlink:href="#g92" y="11424"/><use xlink:href="#g28" y="11441"/><use xlink:href="#g28" y="11458"/><use xlink:href="#g28" y="11475"/><use xlink:href="#g16" y="11492"/><use xlink:href="#g16" y="11509"/><use xlink:href="#g16" y="11526"/><rect x="0" y="11543" width="528" height="17" fill="#262626"/><use xlink:href="#g93" y="11543"/></g><g><use xlink:href="#g32" y="11594"/><use xlink:href="#g33" y="11611"/><use xlink:href="#g34" y="11628"/><use xlink:href="#g35" y="11645"/><use xlink:href="#g36" y="11662"/><use xlink:href="#g37" y="11679"/><use xlink:href="#g38" y="11696"/><use xlink:href="#g39" y="11713"/><use xlink:href="#g40" y="11730"/><use xlink:href="#g74" y="11747"/><use xlink:href="#g75" y="11764"/><rect x="200" y="11781" width="8" height="17" class="foreground"/><use xlink:href="#g94" y="11781"/><rect x="200" y="11798" width="136" height="17" fill="#008787"/><rect x="336" y="11798" width="8" height="17" fill="#444444"/><use xlink:href="#g95" y="11798"/><rect x="200" y="11815" width="136" height="17" fill="#008787"/><rect x="336" y="11815" width="8" height="17" fill="#444444"/><use xlink:href="#g96" y="11815"/><rect x="200" y="11832" width="136" height="17" fill="#008787"/><rect x="336" y="11832" width="8" height="17" fill="#00afaf"/><use xlink:href="#g97" y="11832"/><rect x="200" y="11849" width="136" height="17" fill="#008787"/><rect x="336" y="11849" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g98" y="11849"/><rect x="200" y="11866" width="136" height="17" fill="#008787"/><rect x="336" y="11866" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g99" y="11866"/><rect x="200" y="11883" width="136" height="17" fill="#008787"/><rect x="336" y="11883" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g100" y="11883"/><rect x="200" y="11900" width="136" height="17" fill="#008787"/><rect x="336" y="11900" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g101" y="11900"/><rect x="0" y="11917" width="528" height="17" fill="#262626"/><use xlink:href="#g93" y="11917"/></g><g><use xlink:href="#g32" y="11968"/><use xlink:href="#g33" y="11985"/><use xlink:href="#g34" y="12002"/><use xlink:href="#g35" y="12019"/><use xlink:href="#g36" y="12036"/><use xlink:href="#g37" y="12053"/><use xlink:href="#g38" y="12070"/><use xlink:href="#g39" y="12087"/><use xlink:href="#g40" y="12104"/><use xlink:href="#g74" y="12121"/><use xlink:href="#g75" y="12138"/><rect x="208" y="12155" width="8" height="17" class="foreground"/><use xlink:href="#g102" y="12155"/><rect x="208" y="12172" width="112" height="17" fill="#008787"/><rect x="320" y="12172" width="8" height="17" fill="#444444"/><use xlink:href="#g103" y="12172"/><rect x="208" y="12189" width="112" height="17" fill="#008787"/><rect x="320" y="12189" width="8" height="17" fill="#444444"/><use xlink:href="#g104" y="12189"/><rect x="208" y="12206" width="112" height="17" fill="#008787"/><rect x="320" y="12206" width="8" height="17" fill="#444444"/><use xlink:href="#g105" y="12206"/><rect x="208" y="12223" width="112" height="17" fill="#008787"/><rect x="320" y="12223" width="8" height="17" fill="#444444"/><use xlink:href="#g106" y="12223"/><rect x="208" y="12240" width="112" height="17" fill="#008787"/><rect x="320" y="12240" width="8" height="17" fill="#444444"/><use xlink:href="#g107" y="12240"/><rect x="208" y="12257" width="112" height="17" fill="#008787"/><rect x="320" y="12257" width="8" height="17" fill="#444444"/><use xlink:href="#g108" y="12257"/><use xlink:href="#g16" y="12274"/><rect x="0" y="12291" width="528" height="17" fill="#262626"/><use xlink:href="#g93" y="12291"/></g><g><use xlink:href="#g32" y="12342"/><use xlink:href="#g33" y="12359"/><use xlink:href="#g34" y="12376"/><use xlink:href="#g35" y="12393"/><use xlink:href="#g36" y="12410"/><use xlink:href="#g37" y="12427"/><use xlink:href="#g38" y="12444"/><use xlink:href="#g39" y="12461"/><use xlink:href="#g40" y="12478"/><use xlink:href="#g74" y="12495"/><use xlink:href="#g75" y="12512"/><rect x="216" y="12529" width="8" height="17" class="foreground"/><use xlink:href="#g109" y="12529"/><rect x="216" y="12546" width="72" height="17" fill="#008787"/><rect x="288" y="12546" width="8" height="17" fill="#444444"/><use xlink:href="#g110" y="12546"/><rect x="216" y="12563" width="72" height="17" fill="#008787"/><rect x="288" y="12563" width="8" height="17" fill="#444444"/><use xlink:href="#g111" y="12563"/><rect x="216" y="12580" width="72" height="17" fill="#008787"/><rect x="288" y="12580" width="8" height="17" fill="#444444"/><use xlink:href="#g112" y="12580"/><use xlink:href="#g28" y="12597"/><use xlink:href="#g16" y="12614"/><use xlink:href="#g16" y="12631"/><use xlink:href="#g16" y="12648"/><rect x="0" y="12665" width="528" height="17" fill="#262626"/><use xlink:href="#g93" y="12665"/></g><g><use xlink:href="#g32" y="12716"/><use xlink:href="#g33" y="12733"/><use xlink:href="#g34" y="12750"/><use xlink:href="#g35" y="12767"/><use xlink:href="#g36" y="12784"/><use xlink:href="#g37" y="12801"/><use xlink:href="#g38" y="12818"/><use xlink:href="#g39" y="12835"/><use xlink:href="#g40" y="12852"/><use xlink:href="#g74" y="12869"/><use xlink:href="#g75" y="12886"/><rect x="240" y="12903" width="8" height="17" class="foreground"/><use xlink:href="#g113" y="12903"/><rect x="216" y="12920" width="72" height="17" fill="#ffffff"/><rect x="288" y="12920" width="8" height="17" fill="#444444"/><use xlink:href="#g114" y="12920"/><rect x="216" y="12937" width="72" height="17" fill="#008787"/><rect x="288" y="12937" width="8" height="17" fill="#444444"/><use xlink:href="#g111" y="12937"/><rect x="216" y="12954" width="72" height="17" fill="#008787"/><rect x="288" y="12954" width="8" height="17" fill="#444444"/><use xlink:href="#g112" y="12954"/><use xlink:href="#g28" y="12971"/><use xlink:href="#g16" y="12988"/><use xlink:href="#g16" y="13005"/><use xlink:href="#g16" y="13022"/><rect x="0" y="13039" width="528" height="17" fill="#262626"/><use xlink:href="#g93" y="13039"/></g><g><use xlink:href="#g37" y="13090"/><use xlink:href="#g38" y="13107"/><use xlink:href="#g39" y="13124"/><use xlink:href="#g40" y="13141"/><use xlink:href="#g74" y="13158"/><use xlink:href="#g75" y="13175"/><use xlink:href="#g115" y="13192"/><use xlink:href="#g116" y="13209"/><use xlink:href="#g117" y="13226"/><use xlink:href="#g118" y="13243"/><use xlink:href="#g119" y="13260"/><rect x="128" y="13277" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="13277"/><use xlink:href="#g16" y="13294"/><use xlink:href="#g16" y="13311"/><use xlink:href="#g16" y="13328"/><use xlink:href="#g16" y="13396"/><rect x="0" y="13413" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="13413"/></g><g><use xlink:href="#g37" y="13464"/><use xlink:href="#g38" y="13481"/><use xlink:href="#g39" y="13498"/><use xlink:href="#g40" y="13515"/><use xlink:href="#g74" y="13532"/><use xlink:href="#g75" y="13549"/><use xlink:href="#g115" y="13566"/><use xlink:href="#g116" y="13583"/><use xlink:href="#g117" y="13600"/><use xlink:href="#g118" y="13617"/><use xlink:href="#g119" y="13634"/><rect x="136" y="13651" width="8" height="17" class="foreground"/><use xlink:href="#g41" y="13651"/><rect x="136" y="13668" width="56" height="17" fill="#008787"/><rect x="192" y="13668" width="8" height="17" fill="#444444"/><use xlink:href="#g120" y="13668"/><rect x="136" y="13685" width="56" height="17" fill="#008787"/><rect x="192" y="13685" width="8" height="17" fill="#444444"/><use xlink:href="#g121" y="13685"/><rect x="136" y="13702" width="56" height="17" fill="#008787"/><rect x="192" y="13702" width="8" height="17" fill="#444444"/><use xlink:href="#g122" y="13702"/><rect x="136" y="13719" width="56" height="17" fill="#008787"/><rect x="192" y="13719" width="8" height="17" fill="#444444"/><use xlink:href="#g45" y="13719"/><use xlink:href="#g16" y="13770"/><rect x="0" y="13787" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="13787"/></g><g><use xlink:href="#g37" y="13838"/><use xlink:href="#g38" y="13855"/><use xlink:href="#g39" y="13872"/><use xlink:href="#g40" y="13889"/><use xlink:href="#g74" y="13906"/><use xlink:href="#g75" y="13923"/><use xlink:href="#g115" y="13940"/><use xlink:href="#g116" y="13957"/><use xlink:href="#g117" y="13974"/><use xlink:href="#g118" y="13991"/><use xlink:href="#g119" y="14008"/><rect x="144" y="14025" width="8" height="17" class="foreground"/><use xlink:href="#g46" y="14025"/><rect x="144" y="14042" width="56" height="17" fill="#008787"/><rect x="200" y="14042" width="8" height="17" fill="#444444"/><use xlink:href="#g123" y="14042"/><use xlink:href="#g16" y="14059"/><use xlink:href="#g16" y="14076"/><use xlink:href="#g28" y="14093"/><use xlink:href="#g16" y="14144"/><rect x="0" y="14161" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="14161"/></g><g><use xlink:href="#g37" y="14212"/><use xlink:href="#g38" y="14229"/><use xlink:href="#g39" y="14246"/><use xlink:href="#g40" y="14263"/><use xlink:href="#g74" y="14280"/><use xlink:href="#g75" y="14297"/><use xlink:href="#g115" y="14314"/><use xlink:href="#g116" y="14331"/><use xlink:href="#g117" y="14348"/><use xlink:href="#g118" y="14365"/><use xlink:href="#g119" y="14382"/><rect x="152" y="14399" width="8" height="17" class="foreground"/><use xlink:href="#g48" y="14399"/><rect x="152" y="14416" width="56" height="17" fill="#008787"/><rect x="208" y="14416" width="8" height="17" fill="#444444"/><use xlink:href="#g124" y="14416"/><use xlink:href="#g16" y="14433"/><use xlink:href="#g16" y="14450"/><use xlink:href="#g28" y="14467"/><use xlink:href="#g16" y="14518"/><rect x="0" y="14535" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="14535"/></g><g><use xlink:href="#g37" y="14586"/><use xlink:href="#g38" y="14603"/><use xlink:href="#g39" y="14620"/><use xlink:href="#g40" y="14637"/><use xlink:href="#g74" y="14654"/><use xlink:href="#g75" y="14671"/><use xlink:href="#g115" y="14688"/><use xlink:href="#g116" y="14705"/><use xlink:href="#g117" y="14722"/><use xlink:href="#g118" y="14739"/><use xlink:href="#g119" y="14756"/><rect x="160" y="14773" width="8" height="17" class="foreground"/><use xlink:href="#g50" y="14773"/><rect x="160" y="14790" width="56" height="17" fill="#008787"/><rect x="216" y="14790" width="8" height="17" fill="#444444"/><use xlink:href="#g125" y="14790"/><use xlink:href="#g16" y="14807"/><use xlink:href="#g16" y="14824"/><use xlink:href="#g28" y="14841"/><use xlink:href="#g16" y="14892"/><rect x="0" y="14909" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="14909"/></g><g><use xlink:href="#g37" y="14960"/><use xlink:href="#g38" y="14977"/><use xlink:href="#g39" y="14994"/><use xlink:href="#g40" y="15011"/><use xlink:href="#g74" y="15028"/><use xlink:href="#g75" y="15045"/><use xlink:href="#g115" y="15062"/><use xlink:href="#g116" y="15079"/><use xlink:href="#g117" y="15096"/><use xlink:href="#g118" y="15113"/><use xlink:href="#g119" y="15130"/><rect x="168" y="15147" width="8" height="17" class="foreground"/><use xlink:href="#g53" y="15147"/><rect x="168" y="15164" width="136" height="17" fill="#008787"/><rect x="304" y="15164" width="8" height="17" fill="#444444"/><use xlink:href="#g126" y="15164"/><rect x="168" y="15181" width="136" height="17" fill="#008787"/><rect x="304" y="15181" width="8" height="17" fill="#444444"/><use xlink:href="#g127" y="15181"/><rect x="168" y="15198" width="136" height="17" fill="#008787"/><rect x="304" y="15198" width="8" height="17" fill="#00afaf"/><use xlink:href="#g128" y="15198"/><rect x="168" y="15215" width="136" height="17" fill="#008787"/><rect x="304" y="15215" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g56" y="15215"/><rect x="168" y="15232" width="136" height="17" fill="#008787"/><rect x="304" y="15232" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g129" y="15232"/><rect x="168" y="15249" width="136" height="17" fill="#008787"/><rect x="304" y="15249" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g58" y="15249"/><rect x="168" y="15266" width="136" height="17" fill="#008787"/><rect x="304" y="15266" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g60" y="15266"/><rect x="0" y="15283" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="15283"/></g><g><use xlink:href="#g37" y="15334"/><use xlink:href="#g38" y="15351"/><use xlink:href="#g39" y="15368"/><use xlink:href="#g40" y="15385"/><use xlink:href="#g74" y="15402"/><use xlink:href="#g75" y="15419"/><use xlink:href="#g115" y="15436"/><use xlink:href="#g116" y="15453"/><use xlink:href="#g117" y="15470"/><use xlink:href="#g118" y="15487"/><use xlink:href="#g119" y="15504"/><rect x="176" y="15521" width="8" height="17" class="foreground"/><use xlink:href="#g130" y="15521"/><rect x="176" y="15538" width="64" height="17" fill="#008787"/><rect x="240" y="15538" width="8" height="17" fill="#444444"/><use xlink:href="#g131" y="15538"/><rect x="176" y="15555" width="64" height="17" fill="#008787"/><rect x="240" y="15555" width="8" height="17" fill="#444444"/><use xlink:href="#g132" y="15555"/><rect x="176" y="15572" width="64" height="17" fill="#008787"/><rect x="240" y="15572" width="8" height="17" fill="#444444"/><use xlink:href="#g133" y="15572"/><use xlink:href="#g28" y="15589"/><use xlink:href="#g28" y="15606"/><use xlink:href="#g28" y="15623"/><use xlink:href="#g16" y="15640"/><rect x="0" y="15657" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="15657"/></g><g><use xlink:href="#g37" y="15708"/><use xlink:href="#g38" y="15725"/><use xlink:href="#g39" y="15742"/><use xlink:href="#g40" y="15759"/><use xlink:href="#g74" y="15776"/><use xlink:href="#g75" y="15793"/><use xlink:href="#g115" y="15810"/><use xlink:href="#g116" y="15827"/><use xlink:href="#g117" y="15844"/><use xlink:href="#g118" y="15861"/><use xlink:href="#g119" y="15878"/><rect x="184" y="15895" width="8" height="17" class="foreground"/><use xlink:href="#g134" y="15895"/><rect x="184" y="15912" width="56" height="17" fill="#008787"/><rect x="240" y="15912" width="8" height="17" fill="#444444"/><use xlink:href="#g135" y="15912"/><use xlink:href="#g16" y="15929"/><use xlink:href="#g16" y="15946"/><use xlink:href="#g28" y="15963"/><use xlink:href="#g28" y="15980"/><use xlink:href="#g28" y="15997"/><use xlink:href="#g16" y="16014"/><rect x="0" y="16031" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="16031"/></g><g><use xlink:href="#g37" y="16082"/><use xlink:href="#g38" y="16099"/><use xlink:href="#g39" y="16116"/><use xlink:href="#g40" y="16133"/><use xlink:href="#g74" y="16150"/><use xlink:href="#g75" y="16167"/><use xlink:href="#g115" y="16184"/><use xlink:href="#g116" y="16201"/><use xlink:href="#g117" y="16218"/><use xlink:href="#g118" y="16235"/><use xlink:href="#g119" y="16252"/><rect x="200" y="16269" width="8" height="17" class="foreground"/><use xlink:href="#g136" y="16269"/><rect x="184" y="16286" width="56" height="17" fill="#ffffff"/><rect x="240" y="16286" width="8" height="17" fill="#444444"/><use xlink:href="#g137" y="16286"/><use xlink:href="#g16" y="16303"/><use xlink:href="#g16" y="16320"/><use xlink:href="#g28" y="16337"/><use xlink:href="#g28" y="16354"/><use xlink:href="#g28" y="16371"/><use xlink:href="#g16" y="16388"/><rect x="0" y="16405" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="16405"/></g><g><use xlink:href="#g39" y="16456"/><use xlink:href="#g40" y="16473"/><use xlink:href="#g74" y="16490"/><use xlink:href="#g75" y="16507"/><use xlink:href="#g115" y="16524"/><use xlink:href="#g116" y="16541"/><use xlink:href="#g117" y="16558"/><use xlink:href="#g118" y="16575"/><use xlink:href="#g119" y="16592"/><use xlink:href="#g138" y="16609"/><use xlink:href="#g139" y="16626"/><rect x="128" y="16643" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="16643"/><use xlink:href="#g16" y="16660"/><use xlink:href="#g28" y="16677"/><use xlink:href="#g28" y="16694"/><use xlink:href="#g28" y="16711"/><use xlink:href="#g16" y="16728"/><use xlink:href="#g16" y="16745"/><use xlink:href="#g16" y="16762"/><rect x="0" y="16779" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="16779"/></g><g><use xlink:href="#g39" y="16830"/><use xlink:href="#g40" y="16847"/><use xlink:href="#g74" y="16864"/><use xlink:href="#g75" y="16881"/><use xlink:href="#g115" y="16898"/><use xlink:href="#g116" y="16915"/><use xlink:href="#g117" y="16932"/><use xlink:href="#g118" y="16949"/><use xlink:href="#g119" y="16966"/><use xlink:href="#g138" y="16983"/><use xlink:href="#g139" y="17000"/><rect x="136" y="17017" width="8" height="17" class="foreground"/><use xlink:href="#g140" y="17017"/><rect x="136" y="17034" width="144" height="17" fill="#008787"/><rect x="280" y="17034" width="8" height="17" fill="#444444"/><use xlink:href="#g141" y="17034"/><rect x="136" y="17051" width="144" height="17" fill="#008787"/><rect x="280" y="17051" width="8" height="17" fill="#444444"/><use xlink:href="#g142" y="17051"/><rect x="136" y="17068" width="144" height="17" fill="#008787"/><rect x="280" y="17068" width="8" height="17" fill="#00afaf"/><use xlink:href="#g143" y="17068"/><rect x="136" y="17085" width="144" height="17" fill="#008787"/><rect x="280" y="17085" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g144" y="17085"/><rect x="136" y="17102" width="144" height="17" fill="#008787"/><rect x="280" y="17102" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g145" y="17102"/><rect x="136" y="17119" width="144" height="17" fill="#008787"/><rect x="280" y="17119" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g146" y="17119"/><rect x="136" y="17136" width="144" height="17" fill="#008787"/><rect x="280" y="17136" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g147" y="17136"/><rect x="0" y="17153" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="17153"/></g><g><use xlink:href="#g39" y="17204"/><use xlink:href="#g40" y="17221"/><use xlink:href="#g74" y="17238"/><use xlink:href="#g75" y="17255"/><use xlink:href="#g115" y="17272"/><use xlink:href="#g116" y="17289"/><use xlink:href="#g117" y="17306"/><use xlink:href="#g118" y="17323"/><use xlink:href="#g119" y="17340"/><use xlink:href="#g138" y="17357"/><use xlink:href="#g139" y="17374"/><rect x="144" y="17391" width="8" height="17" class="foreground"/><use xlink:href="#g148" y="17391"/><rect x="144" y="17408" width="64" height="17" fill="#008787"/><rect x="208" y="17408" width="8" height="17" fill="#444444"/><use xlink:href="#g149" y="17408"/><rect x="144" y="17425" width="64" height="17" fill="#008787"/><rect x="208" y="17425" width="8" height="17" fill="#444444"/><use xlink:href="#g150" y="17425"/><use xlink:href="#g28" y="17442"/><use xlink:href="#g28" y="17459"/><use xlink:href="#g16" y="17476"/><use xlink:href="#g16" y="17493"/><use xlink:href="#g16" y="17510"/><rect x="0" y="17527" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="17527"/></g><g><use xlink:href="#g39" y="17578"/><use xlink:href="#g40" y="17595"/><use xlink:href="#g74" y="17612"/><use xlink:href="#g75" y="17629"/><use xlink:href="#g115" y="17646"/><use xlink:href="#g116" y="17663"/><use xlink:href="#g117" y="17680"/><use xlink:href="#g118" y="17697"/><use xlink:href="#g119" y="17714"/><use xlink:href="#g138" y="17731"/><use xlink:href="#g139" y="17748"/><rect x="152" y="17765" width="8" height="17" class="foreground"/><use xlink:href="#g151" y="17765"/><rect x="152" y="17782" width="64" height="17" fill="#008787"/><rect x="216" y="17782" width="8" height="17" fill="#444444"/><use xlink:href="#g152" y="17782"/><rect x="152" y="17799" width="64" height="17" fill="#008787"/><rect x="216" y="17799" width="8" height="17" fill="#444444"/><use xlink:href="#g153" y="17799"/><use xlink:href="#g28" y="17816"/><use xlink:href="#g28" y="17833"/><use xlink:href="#g16" y="17850"/><use xlink:href="#g16" y="17867"/><use xlink:href="#g16" y="17884"/><rect x="0" y="17901" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="17901"/></g><g><use xlink:href="#g39" y="17952"/><use xlink:href="#g40" y="17969"/><use xlink:href="#g74" y="17986"/><use xlink:href="#g75" y="18003"/><use xlink:href="#g115" y="18020"/><use xlink:href="#g116" y="18037"/><use xlink:href="#g117" y="18054"/><use xlink:href="#g118" y="18071"/><use xlink:href="#g119" y="18088"/><use xlink:href="#g138" y="18105"/><use xlink:href="#g139" y="18122"/><rect x="168" y="18139" width="8" height="17" class="foreground"/><use xlink:href="#g154" y="18139"/><rect x="152" y="18156" width="64" height="17" fill="#ffffff"/><rect x="216" y="18156" width="8" height="17" fill="#444444"/><use xlink:href="#g155" y="18156"/><rect x="152" y="18173" width="64" height="17" fill="#008787"/><rect x="216" y="18173" width="8" height="17" fill="#444444"/><use xlink:href="#g153" y="18173"/><use xlink:href="#g28" y="18190"/><use xlink:href="#g28" y="18207"/><use xlink:href="#g16" y="18224"/><use xlink:href="#g16" y="18241"/><use xlink:href="#g16" y="18258"/><rect x="0" y="18275" width="528" height="17" fill="#262626"/><use xlink:href="#g156" y="18275"/></g><g><use xlink:href="#g39" y="18326"/><use xlink:href="#g40" y="18343"/><use xlink:href="#g74" y="18360"/><use xlink:href="#g75" y="18377"/><use xlink:href="#g115" y="18394"/><use xlink:href="#g116" y="18411"/><use xlink:href="#g117" y="18428"/><use xlink:href="#g118" y="18445"/><use xlink:href="#g119" y="18462"/><use xlink:href="#g138" y="18479"/><use xlink:href="#g139" y="18496"/><rect x="176" y="18513" width="8" height="17" class="foreground"/><use xlink:href="#g157" y="18513"/><rect x="176" y="18530" width="136" height="17" fill="#008787"/><rect x="312" y="18530" width="8" height="17" fill="#444444"/><use xlink:href="#g158" y="18530"/><rect x="176" y="18547" width="136" height="17" fill="#008787"/><rect x="312" y="18547" width="8" height="17" fill="#444444"/><use xlink:href="#g159" y="18547"/><rect x="176" y="18564" width="136" height="17" fill="#008787"/><rect x="312" y="18564" width="8" height="17" fill="#00afaf"/><use xlink:href="#g160" y="18564"/><rect x="176" y="18581" width="136" height="17" fill="#008787"/><rect x="312" y="18581" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g161" y="18581"/><rect x="176" y="18598" width="136" height="17" fill="#008787"/><rect x="312" y="18598" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g162" y="18598"/><rect x="176" y="18615" width="136" height="17" fill="#008787"/><rect x="312" y="18615" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g163" y="18615"/><rect x="176" y="18632" width="136" height="17" fill="#008787"/><rect x="312" y="18632" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g164" y="18632"/><rect x="0" y="18649" width="528" height="17" fill="#262626"/><use xlink:href="#g156" y="18649"/></g><g><use xlink:href="#g39" y="18700"/><use xlink:href="#g40" y="18717"/><use xlink:href="#g74" y="18734"/><use xlink:href="#g75" y="18751"/><use xlink:href="#g115" y="18768"/><use xlink:href="#g116" y="18785"/><use xlink:href="#g117" y="18802"/><use xlink:href="#g118" y="18819"/><use xlink:href="#g119" y="18836"/><use xlink:href="#g138" y="18853"/><use xlink:href="#g139" y="18870"/><rect x="208" y="18887" width="8" height="17" class="foreground"/><use xlink:href="#g165" y="18887"/><rect x="176" y="18904" width="136" height="17" fill="#ffffff"/><rect x="312" y="18904" width="8" height="17" fill="#444444"/><use xlink:href="#g166" y="18904"/><rect x="176" y="18921" width="136" height="17" fill="#008787"/><rect x="312" y="18921" width="8" height="17" fill="#444444"/><use xlink:href="#g159" y="18921"/><rect x="176" y="18938" width="136" height="17" fill="#008787"/><rect x="312" y="18938" width="8" height="17" fill="#00afaf"/><use xlink:href="#g160" y="18938"/><rect x="176" y="18955" width="136" height="17" fill="#008787"/><rect x="312" y="18955" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g161" y="18955"/><rect x="176" y="18972" width="136" height="17" fill="#008787"/><rect x="312" y="18972" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g162" y="18972"/><rect x="176" y="18989" width="136" height="17" fill="#008787"/><rect x="312" y="18989" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g163" y="18989"/><rect x="176" y="19006" width="136" height="17" fill="#008787"/><rect x="312" y="19006" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g164" y="19006"/><rect x="0" y="19023" width="528" height="17" fill="#262626"/><use xlink:href="#g156" y="19023"/></g><g><use xlink:href="#g74" y="19074"/><use xlink:href="#g75" y="19091"/><use xlink:href="#g115" y="19108"/><use xlink:href="#g116" y="19125"/><use xlink:href="#g117" y="19142"/><use xlink:href="#g118" y="19159"/><use xlink:href="#g119" y="19176"/><use xlink:href="#g138" y="19193"/><use xlink:href="#g139" y="19210"/><use xlink:href="#g167" y="19227"/><use xlink:href="#g168" y="19244"/><rect x="128" y="19261" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="19261"/><use xlink:href="#g28" y="19278"/><use xlink:href="#g28" y="19295"/><use xlink:href="#g16" y="19312"/><use xlink:href="#g16" y="19329"/><use xlink:href="#g16" y="19346"/><use xlink:href="#g16" y="19363"/><use xlink:href="#g16" y="19380"/><rect x="0" y="19397" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="19397"/></g><g><use xlink:href="#g74" y="19448"/><use xlink:href="#g75" y="19465"/><use xlink:href="#g115" y="19482"/><use xlink:href="#g116" y="19499"/><use xlink:href="#g117" y="19516"/><use xlink:href="#g118" y="19533"/><use xlink:href="#g119" y="19550"/><use xlink:href="#g138" y="19567"/><use xlink:href="#g139" y="19584"/><use xlink:href="#g167" y="19601"/><use xlink:href="#g168" y="19618"/><rect x="208" y="19635" width="8" height="17" class="foreground"/><use xlink:href="#g165" y="19635"/><use xlink:href="#g28" y="19652"/><use xlink:href="#g28" y="19669"/><use xlink:href="#g16" y="19686"/><use xlink:href="#g16" y="19703"/><use xlink:href="#g16" y="19720"/><use xlink:href="#g16" y="19737"/><use xlink:href="#g16" y="19754"/><rect x="0" y="19771" width="528" height="17" fill="#262626"/><use xlink:href="#g156" y="19771"/></g><g><use xlink:href="#g74" y="19822"/><use xlink:href="#g75" y="19839"/><use xlink:href="#g115" y="19856"/><use xlink:href="#g116" y="19873"/><use xlink:href="#g117" y="19890"/><use xlink:href="#g118" y="19907"/><use xlink:href="#g119" y="19924"/><use xlink:href="#g138" y="19941"/><use xlink:href="#g139" y="19958"/><use xlink:href="#g167" y="19975"/><use xlink:href="#g168" y="19992"/><rect x="216" y="20009" width="8" height="17" class="foreground"/><use xlink:href="#g169" y="20009"/><use xlink:href="#g28" y="20026"/><use xlink:href="#g28" y="20043"/><use xlink:href="#g16" y="20060"/><use xlink:href="#g16" y="20077"/><use xlink:href="#g16" y="20094"/><use xlink:href="#g16" y="20111"/><use xlink:href="#g16" y="20128"/><rect x="0" y="20145" width="528" height="17" fill="#262626"/><use xlink:href="#g156" y="20145"/></g><g><use xlink:href="#g74" y="20196"/><use xlink:href="#g75" y="20213"/><use xlink:href="#g115" y="20230"/><use xlink:href="#g116" y="20247"/><use xlink:href="#g117" y="20264"/><use xlink:href="#g118" y="20281"/><use xlink:href="#g119" y="20298"/><use xlink:href="#g138" y="20315"/><use xlink:href="#g139" y="20332"/><use xlink:href="#g167" y="20349"/><use xlink:href="#g168" y="20366"/><rect x="224" y="20383" width="8" height="17" class="foreground"/><use xlink:href="#g170" y="20383"/><use xlink:href="#g28" y="20400"/><use xlink:href="#g28" y="20417"/><use xlink:href="#g16" y="20434"/><use xlink:href="#g16" y="20451"/><use xlink:href="#g16" y="20468"/><use xlink:href="#g16" y="20485"/><use xlink:href="#g16" y="20502"/><rect x="0" y="20519" width="528" height="17" fill="#262626"/><use xlink:href="#g156" y="20519"/></g><g><use xlink:href="#g116" y="20570"/><use xlink:href="#g117" y="20587"/><use xlink:href="#g118" y="20604"/><use xlink:href="#g119" y="20621"/><use xlink:href="#g138" y="20638"/><use xlink:href="#g139" y="20655"/><use xlink:href="#g167" y="20672"/><use xlink:href="#g168" y="20689"/><use xlink:href="#g171" y="20706"/><use xlink:href="#g172" y="20723"/><use xlink:href="#g173" y="20740"/><rect x="128" y="20757" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="20757"/><use xlink:href="#g16" y="20774"/><use xlink:href="#g16" y="20791"/><use xlink:href="#g16" y="20808"/><use xlink:href="#g16" y="20825"/><use xlink:href="#g16" y="20842"/><use xlink:href="#g16" y="20876"/><rect x="0" y="20893" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="20893"/></g><g><use xlink:href="#g116" y="20944"/><use xlink:href="#g117" y="20961"/><use xlink:href="#g118" y="20978"/><use xlink:href="#g119" y="20995"/><use xlink:href="#g138" y="21012"/><use xlink:href="#g139" y="21029"/><use xlink:href="#g167" y="21046"/><use xlink:href="#g168" y="21063"/><use xlink:href="#g171" y="21080"/><use xlink:href="#g172" y="21097"/><use xlink:href="#g173" y="21114"/><rect x="136" y="21131" width="8" height="17" class="foreground"/><use xlink:href="#g174" y="21131"/><rect x="136" y="21148" width="56" height="17" fill="#008787"/><rect x="192" y="21148" width="8" height="17" fill="#444444"/><use xlink:href="#g120" y="21148"/><rect x="136" y="21165" width="56" height="17" fill="#008787"/><rect x="192" y="21165" width="8" height="17" fill="#444444"/><use xlink:href="#g121" y="21165"/><rect x="136" y="21182" width="56" height="17" fill="#008787"/><rect x="192" y="21182" width="8" height="17" fill="#444444"/><use xlink:href="#g122" y="21182"/><rect x="136" y="21199" width="56" height="17" fill="#008787"/><rect x="192" y="21199" width="8" height="17" fill="#444444"/><use xlink:href="#g175" y="21199"/><use xlink:href="#g16" y="21216"/><use xlink:href="#g16" y="21250"/><rect x="0" y="21267" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="21267"/></g><g><use xlink:href="#g116" y="21318"/><use xlink:href="#g117" y="21335"/><use xlink:href="#g118" y="21352"/><use xlink:href="#g119" y="21369"/><use xlink:href="#g138" y="21386"/><use xlink:href="#g139" y="21403"/><use xlink:href="#g167" y="21420"/><use xlink:href="#g168" y="21437"/><use xlink:href="#g171" y="21454"/><use xlink:href="#g172" y="21471"/><use xlink:href="#g173" y="21488"/><rect x="144" y="21505" width="8" height="17" class="foreground"/><use xlink:href="#g176" y="21505"/><rect x="144" y="21522" width="56" height="17" fill="#008787"/><rect x="200" y="21522" width="8" height="17" fill="#444444"/><use xlink:href="#g123" y="21522"/><use xlink:href="#g16" y="21539"/><use xlink:href="#g16" y="21556"/><use xlink:href="#g16" y="21573"/><use xlink:href="#g16" y="21590"/><use xlink:href="#g16" y="21624"/><rect x="0" y="21641" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="21641"/></g><g><use xlink:href="#g116" y="21692"/><use xlink:href="#g117" y="21709"/><use xlink:href="#g118" y="21726"/><use xlink:href="#g119" y="21743"/><use xlink:href="#g138" y="21760"/><use xlink:href="#g139" y="21777"/><use xlink:href="#g167" y="21794"/><use xlink:href="#g168" y="21811"/><use xlink:href="#g171" y="21828"/><use xlink:href="#g172" y="21845"/><use xlink:href="#g173" y="21862"/><rect x="152" y="21879" width="8" height="17" class="foreground"/><use xlink:href="#g177" y="21879"/><rect x="152" y="21896" width="56" height="17" fill="#008787"/><rect x="208" y="21896" width="8" height="17" fill="#444444"/><use xlink:href="#g124" y="21896"/><use xlink:href="#g16" y="21913"/><use xlink:href="#g16" y="21930"/><use xlink:href="#g16" y="21947"/><use xlink:href="#g16" y="21964"/><use xlink:href="#g16" y="21998"/><rect x="0" y="22015" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="22015"/></g><g><use xlink:href="#g116" y="22066"/><use xlink:href="#g117" y="22083"/><use xlink:href="#g118" y="22100"/><use xlink:href="#g119" y="22117"/><use xlink:href="#g138" y="22134"/><use xlink:href="#g139" y="22151"/><use xlink:href="#g167" y="22168"/><use xlink:href="#g168" y="22185"/><use xlink:href="#g171" y="22202"/><use xlink:href="#g172" y="22219"/><use xlink:href="#g173" y="22236"/><rect x="160" y="22253" width="8" height="17" class="foreground"/><use xlink:href="#g178" y="22253"/><rect x="160" y="22270" width="56" height="17" fill="#008787"/><rect x="216" y="22270" width="8" height="17" fill="#444444"/><use xlink:href="#g125" y="22270"/><use xlink:href="#g16" y="22287"/><use xlink:href="#g16" y="22304"/><use xlink:href="#g16" y="22321"/><use xlink:href="#g16" y="22338"/><use xlink:href="#g16" y="22372"/><rect x="0" y="22389" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="22389"/></g><g><use xlink:href="#g116" y="22440"/><use xlink:href="#g117" y="22457"/><use xlink:href="#g118" y="22474"/><use xlink:href="#g119" y="22491"/><use xlink:href="#g138" y="22508"/><use xlink:href="#g139" y="22525"/><use xlink:href="#g167" y="22542"/><use xlink:href="#g168" y="22559"/><use xlink:href="#g171" y="22576"/><use xlink:href="#g172" y="22593"/><use xlink:href="#g173" y="22610"/><rect x="168" y="22627" width="8" height="17" class="foreground"/><use xlink:href="#g179" y="22627"/><rect x="168" y="22644" width="136" height="17" fill="#008787"/><rect x="304" y="22644" width="8" height="17" fill="#444444"/><use xlink:href="#g180" y="22644"/><rect x="168" y="22661" width="136" height="17" fill="#008787"/><rect x="304" y="22661" width="8" height="17" fill="#444444"/><use xlink:href="#g126" y="22661"/><rect x="168" y="22678" width="136" height="17" fill="#008787"/><rect x="304" y="22678" width="8" height="17" fill="#00afaf"/><use xlink:href="#g127" y="22678"/><rect x="168" y="22695" width="136" height="17" fill="#008787"/><rect x="304" y="22695" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g128" y="22695"/><rect x="168" y="22712" width="136" height="17" fill="#008787"/><rect x="304" y="22712" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g181" y="22712"/><rect x="168" y="22729" width="136" height="17" fill="#008787"/><rect x="304" y="22729" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g129" y="22729"/><rect x="168" y="22746" width="136" height="17" fill="#008787"/><rect x="304" y="22746" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g182" y="22746"/><rect x="0" y="22763" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="22763"/></g><g><use xlink:href="#g116" y="22814"/><use xlink:href="#g117" y="22831"/><use xlink:href="#g118" y="22848"/><use xlink:href="#g119" y="22865"/><use xlink:href="#g138" y="22882"/><use xlink:href="#g139" y="22899"/><use xlink:href="#g167" y="22916"/><use xlink:href="#g168" y="22933"/><use xlink:href="#g171" y="22950"/><use xlink:href="#g172" y="22967"/><use xlink:href="#g173" y="22984"/><rect x="176" y="23001" width="8" height="17" class="foreground"/><use xlink:href="#g183" y="23001"/><rect x="176" y="23018" width="136" height="17" fill="#008787"/><rect x="312" y="23018" width="8" height="17" fill="#444444"/><use xlink:href="#g184" y="23018"/><rect x="176" y="23035" width="136" height="17" fill="#008787"/><rect x="312" y="23035" width="8" height="17" fill="#444444"/><use xlink:href="#g185" y="23035"/><rect x="176" y="23052" width="136" height="17" fill="#008787"/><rect x="312" y="23052" width="8" height="17" fill="#444444"/><use xlink:href="#g186" y="23052"/><rect x="176" y="23069" width="136" height="17" fill="#008787"/><rect x="312" y="23069" width="8" height="17" fill="#444444"/><use xlink:href="#g187" y="23069"/><rect x="176" y="23086" width="136" height="17" fill="#008787"/><rect x="312" y="23086" width="8" height="17" fill="#444444"/><use xlink:href="#g188" y="23086"/><use xlink:href="#g28" y="23103"/><use xlink:href="#g16" y="23120"/><rect x="0" y="23137" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="23137"/></g><g><use xlink:href="#g116" y="23188"/><use xlink:href="#g117" y="23205"/><use xlink:href="#g118" y="23222"/><use xlink:href="#g119" y="23239"/><use xlink:href="#g138" y="23256"/><use xlink:href="#g139" y="23273"/><use xlink:href="#g167" y="23290"/><use xlink:href="#g168" y="23307"/><use xlink:href="#g171" y="23324"/><use xlink:href="#g172" y="23341"/><use xlink:href="#g173" y="23358"/><rect x="184" y="23375" width="8" height="17" class="foreground"/><use xlink:href="#g189" y="23375"/><rect x="184" y="23392" width="136" height="17" fill="#008787"/><rect x="320" y="23392" width="8" height="17" fill="#444444"/><use xlink:href="#g190" y="23392"/><rect x="184" y="23409" width="136" height="17" fill="#008787"/><rect x="320" y="23409" width="8" height="17" fill="#444444"/><use xlink:href="#g191" y="23409"/><rect x="184" y="23426" width="136" height="17" fill="#008787"/><rect x="320" y="23426" width="8" height="17" fill="#444444"/><use xlink:href="#g192" y="23426"/><rect x="184" y="23443" width="136" height="17" fill="#008787"/><rect x="320" y="23443" width="8" height="17" fill="#444444"/><use xlink:href="#g193" y="23443"/><use xlink:href="#g16" y="23460"/><use xlink:href="#g28" y="23477"/><use xlink:href="#g16" y="23494"/><rect x="0" y="23511" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="23511"/></g><g><use xlink:href="#g116" y="23562"/><use xlink:href="#g117" y="23579"/><use xlink:href="#g118" y="23596"/><use xlink:href="#g119" y="23613"/><use xlink:href="#g138" y="23630"/><use xlink:href="#g139" y="23647"/><use xlink:href="#g167" y="23664"/><use xlink:href="#g168" y="23681"/><use xlink:href="#g171" y="23698"/><use xlink:href="#g172" y="23715"/><use xlink:href="#g173" y="23732"/><rect x="264" y="23749" width="8" height="17" class="foreground"/><use xlink:href="#g194" y="23749"/><rect x="184" y="23766" width="136" height="17" fill="#ffffff"/><rect x="320" y="23766" width="8" height="17" fill="#444444"/><use xlink:href="#g195" y="23766"/><rect x="184" y="23783" width="136" height="17" fill="#008787"/><rect x="320" y="23783" width="8" height="17" fill="#444444"/><use xlink:href="#g191" y="23783"/><rect x="184" y="23800" width="136" height="17" fill="#008787"/><rect x="320" y="23800" width="8" height="17" fill="#444444"/><use xlink:href="#g192" y="23800"/><rect x="184" y="23817" width="136" height="17" fill="#008787"/><rect x="320" y="23817" width="8" height="17" fill="#444444"/><use xlink:href="#g193" y="23817"/><use xlink:href="#g16" y="23834"/><use xlink:href="#g28" y="23851"/><use xlink:href="#g16" y="23868"/><rect x="0" y="23885" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="23885"/></g><g><use xlink:href="#g118" y="23936"/><use xlink:href="#g119" y="23953"/><use xlink:href="#g138" y="23970"/><use xlink:href="#g139" y="23987"/><use xlink:href="#g167" y="24004"/><use xlink:href="#g168" y="24021"/><use xlink:href="#g171" y="24038"/><use xlink:href="#g172" y="24055"/><use xlink:href="#g173" y="24072"/><use xlink:href="#g196" y="24089"/><use xlink:href="#g197" y="24106"/><rect x="128" y="24123" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="24123"/><use xlink:href="#g16" y="24140"/><use xlink:href="#g16" y="24157"/><use xlink:href="#g16" y="24174"/><use xlink:href="#g28" y="24191"/><use xlink:href="#g16" y="24208"/><use xlink:href="#g16" y="24225"/><use xlink:href="#g16" y="24242"/><rect x="0" y="24259" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="24259"/></g><g><use xlink:href="#g118" y="24310"/><use xlink:href="#g119" y="24327"/><use xlink:href="#g138" y="24344"/><use xlink:href="#g139" y="24361"/><use xlink:href="#g167" y="24378"/><use xlink:href="#g168" y="24395"/><use xlink:href="#g171" y="24412"/><use xlink:href="#g172" y="24429"/><use xlink:href="#g173" y="24446"/><use xlink:href="#g196" y="24463"/><use xlink:href="#g197" y="24480"/><rect x="136" y="24497" width="8" height="17" class="foreground"/><use xlink:href="#g198" y="24497"/><rect x="136" y="24514" width="80" height="17" fill="#008787"/><rect x="216" y="24514" width="8" height="17" fill="#444444"/><use xlink:href="#g199" y="24514"/><rect x="136" y="24531" width="80" height="17" fill="#008787"/><rect x="216" y="24531" width="8" height="17" fill="#444444"/><use xlink:href="#g200" y="24531"/><rect x="136" y="24548" width="80" height="17" fill="#008787"/><rect x="216" y="24548" width="8" height="17" fill="#444444"/><use xlink:href="#g201" y="24548"/><rect x="136" y="24565" width="80" height="17" fill="#008787"/><rect x="216" y="24565" width="8" height="17" fill="#444444"/><use xlink:href="#g202" y="24565"/><rect x="136" y="24582" width="80" height="17" fill="#008787"/><rect x="216" y="24582" width="8" height="17" fill="#00afaf"/><use xlink:href="#g203" y="24582"/><rect x="136" y="24599" width="80" height="17" fill="#008787"/><rect x="216" y="24599" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g204" y="24599"/><rect x="136" y="24616" width="80" height="17" fill="#008787"/><rect x="216" y="24616" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g205" y="24616"/><rect x="0" y="24633" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="24633"/></g><g><use xlink:href="#g118" y="24684"/><use xlink:href="#g119" y="24701"/><use xlink:href="#g138" y="24718"/><use xlink:href="#g139" y="24735"/><use xlink:href="#g167" y="24752"/><use xlink:href="#g168" y="24769"/><use xlink:href="#g171" y="24786"/><use xlink:href="#g172" y="24803"/><use xlink:href="#g173" y="24820"/><use xlink:href="#g196" y="24837"/><use xlink:href="#g197" y="24854"/><rect x="144" y="24871" width="8" height="17" class="foreground"/><use xlink:href="#g206" y="24871"/><rect x="144" y="24888" width="56" height="17" fill="#008787"/><rect x="200" y="24888" width="8" height="17" fill="#444444"/><use xlink:href="#g207" y="24888"/><use xlink:href="#g16" y="24905"/><use xlink:href="#g16" y="24922"/><use xlink:href="#g28" y="24939"/><use xlink:href="#g16" y="24956"/><use xlink:href="#g16" y="24973"/><use xlink:href="#g16" y="24990"/><rect x="0" y="25007" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="25007"/></g><g><use xlink:href="#g118" y="25058"/><use xlink:href="#g119" y="25075"/><use xlink:href="#g138" y="25092"/><use xlink:href="#g139" y="25109"/><use xlink:href="#g167" y="25126"/><use xlink:href="#g168" y="25143"/><use xlink:href="#g171" y="25160"/><use xlink:href="#g172" y="25177"/><use xlink:href="#g173" y="25194"/><use xlink:href="#g196" y="25211"/><use xlink:href="#g197" y="25228"/><rect x="160" y="25245" width="8" height="17" class="foreground"/><use xlink:href="#g208" y="25245"/><rect x="144" y="25262" width="56" height="17" fill="#ffffff"/><rect x="200" y="25262" width="8" height="17" fill="#444444"/><use xlink:href="#g209" y="25262"/><use xlink:href="#g16" y="25279"/><use xlink:href="#g16" y="25296"/><use xlink:href="#g28" y="25313"/><use xlink:href="#g16" y="25330"/><use xlink:href="#g16" y="25347"/><use xlink:href="#g16" y="25364"/><rect x="0" y="25381" width="528" height="17" fill="#262626"/><use xlink:href="#g210" y="25381"/></g><g><use xlink:href="#g118" y="25432"/><use xlink:href="#g119" y="25449"/><use xlink:href="#g138" y="25466"/><use xlink:href="#g139" y="25483"/><use xlink:href="#g167" y="25500"/><use xlink:href="#g168" y="25517"/><use xlink:href="#g171" y="25534"/><use xlink:href="#g172" y="25551"/><use xlink:href="#g173" y="25568"/><use xlink:href="#g196" y="25585"/><use xlink:href="#g197" y="25602"/><rect x="168" y="25619" width="8" height="17" class="foreground"/><use xlink:href="#g211" y="25619"/><rect x="168" y="25636" width="136" height="17" fill="#008787"/><rect x="304" y="25636" width="8" height="17" fill="#444444"/><use xlink:href="#g212" y="25636"/><rect x="168" y="25653" width="136" height="17" fill="#008787"/><rect x="304" y="25653" width="8" height="17" fill="#444444"/><use xlink:href="#g180" y="25653"/><rect x="168" y="25670" width="136" height="17" fill="#008787"/><rect x="304" y="25670" width="8" height="17" fill="#00afaf"/><use xlink:href="#g126" y="25670"/><rect x="168" y="25687" width="136" height="17" fill="#008787"/><rect x="304" y="25687" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g213" y="25687"/><rect x="168" y="25704" width="136" height="17" fill="#008787"/><rect x="304" y="25704" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g128" y="25704"/><rect x="168" y="25721" width="136" height="17" fill="#008787"/><rect x="304" y="25721" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g181" y="25721"/><rect x="168" y="25738" width="136" height="17" fill="#008787"/><rect x="304" y="25738" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g182" y="25738"/><rect x="0" y="25755" width="528" height="17" fill="#262626"/><use xlink:href="#g210" y="25755"/></g><g><use xlink:href="#g118" y="25806"/><use xlink:href="#g119" y="25823"/><use xlink:href="#g138" y="25840"/><use xlink:href="#g139" y="25857"/><use xlink:href="#g167" y="25874"/><use xlink:href="#g168" y="25891"/><use xlink:href="#g171" y="25908"/><use xlink:href="#g172" y="25925"/><use xlink:href="#g173" y="25942"/><use xlink:href="#g196" y="25959"/><use xlink:href="#g197" y="25976"/><rect x="176" y="25993" width="8" height="17" class="foreground"/><use xlink:href="#g214" y="25993"/><rect x="176" y="26010" width="136" height="17" fill="#008787"/><rect x="312" y="26010" width="8" height="17" fill="#444444"/><use xlink:href="#g215" y="26010"/><rect x="176" y="26027" width="136" height="17" fill="#008787"/><rect x="312" y="26027" width="8" height="17" fill="#444444"/><use xlink:href="#g216" y="26027"/><rect x="176" y="26044" width="136" height="17" fill="#008787"/><rect x="312" y="26044" width="8" height="17" fill="#444444"/><use xlink:href="#g217" y="26044"/><rect x="176" y="26061" width="136" height="17" fill="#008787"/><rect x="312" y="26061" width="8" height="17" fill="#444444"/><use xlink:href="#g218" y="26061"/><rect x="176" y="26078" width="136" height="17" fill="#008787"/><rect x="312" y="26078" width="8" height="17" fill="#444444"/><use xlink:href="#g219" y="26078"/><use xlink:href="#g16" y="26095"/><use xlink:href="#g16" y="26112"/><rect x="0" y="26129" width="528" height="17" fill="#262626"/><use xlink:href="#g210" y="26129"/></g><g><use xlink:href="#g118" y="26180"/><use xlink:href="#g119" y="26197"/><use xlink:href="#g138" y="26214"/><use xlink:href="#g139" y="26231"/><use xlink:href="#g167" y="26248"/><use xlink:href="#g168" y="26265"/><use xlink:href="#g171" y="26282"/><use xlink:href="#g172" y="26299"/><use xlink:href="#g173" y="26316"/><use xlink:href="#g196" y="26333"/><use xlink:href="#g197" y="26350"/><rect x="168" y="26367" width="8" height="17" class="foreground"/><use xlink:href="#g220" y="26367"/><use xlink:href="#g16" y="26384"/><use xlink:href="#g16" y="26401"/><use xlink:href="#g16" y="26418"/><use xlink:href="#g28" y="26435"/><use xlink:href="#g16" y="26452"/><use xlink:href="#g16" y="26469"/><use xlink:href="#g16" y="26486"/><rect x="0" y="26503" width="528" height="17" fill="#262626"/><use xlink:href="#g210" y="26503"/></g><g><use xlink:href="#g118" y="26554"/><use xlink:href="#g119" y="26571"/><use xlink:href="#g138" y="26588"/><use xlink:href="#g139" y="26605"/><use xlink:href="#g167" y="26622"/><use xlink:href="#g168" y="26639"/><use xlink:href="#g171" y="26656"/><use xlink:href="#g172" y="26673"/><use xlink:href="#g173" y="26690"/><use xlink:href="#g196" y="26707"/><use xlink:href="#g197" y="26724"/><rect x="176" y="26741" width="8" height="17" class="foreground"/><use xlink:href="#g221" y="26741"/><rect x="176" y="26758" width="136" height="17" fill="#008787"/><rect x="312" y="26758" width="8" height="17" fill="#444444"/><use xlink:href="#g184" y="26758"/><rect x="176" y="26775" width="136" height="17" fill="#008787"/><rect x="312" y="26775" width="8" height="17" fill="#444444"/><use xlink:href="#g185" y="26775"/><rect x="176" y="26792" width="136" height="17" fill="#008787"/><rect x="312" y="26792" width="8" height="17" fill="#444444"/><use xlink:href="#g186" y="26792"/><rect x="176" y="26809" width="136" height="17" fill="#008787"/><rect x="312" y="26809" width="8" height="17" fill="#444444"/><use xlink:href="#g222" y="26809"/><rect x="176" y="26826" width="136" height="17" fill="#008787"/><rect x="312" y="26826" width="8" height="17" fill="#444444"/><use xlink:href="#g188" y="26826"/><use xlink:href="#g16" y="26843"/><use xlink:href="#g16" y="26860"/><rect x="0" y="26877" width="528" height="17" fill="#262626"/><use xlink:href="#g210" y="26877"/></g><g><use xlink:href="#g118" y="26928"/><use xlink:href="#g119" y="26945"/><use xlink:href="#g138" y="26962"/><use xlink:href="#g139" y="26979"/><use xlink:href="#g167" y="26996"/><use xlink:href="#g168" y="27013"/><use xlink:href="#g171" y="27030"/><use xlink:href="#g172" y="27047"/><use xlink:href="#g173" y="27064"/><use xlink:href="#g196" y="27081"/><use xlink:href="#g197" y="27098"/><rect x="184" y="27115" width="8" height="17" class="foreground"/><use xlink:href="#g223" y="27115"/><rect x="184" y="27132" width="136" height="17" fill="#008787"/><rect x="320" y="27132" width="8" height="17" fill="#444444"/><use xlink:href="#g190" y="27132"/><rect x="184" y="27149" width="136" height="17" fill="#008787"/><rect x="320" y="27149" width="8" height="17" fill="#444444"/><use xlink:href="#g191" y="27149"/><rect x="184" y="27166" width="136" height="17" fill="#008787"/><rect x="320" y="27166" width="8" height="17" fill="#444444"/><use xlink:href="#g192" y="27166"/><rect x="184" y="27183" width="136" height="17" fill="#008787"/><rect x="320" y="27183" width="8" height="17" fill="#444444"/><use xlink:href="#g224" y="27183"/><use xlink:href="#g16" y="27200"/><use xlink:href="#g16" y="27217"/><use xlink:href="#g16" y="27234"/><rect x="0" y="27251" width="528" height="17" fill="#262626"/><use xlink:href="#g210" y="27251"/></g><g><use xlink:href="#g118" y="27302"/><use xlink:href="#g119" y="27319"/><use xlink:href="#g138" y="27336"/><use xlink:href="#g139" y="27353"/><use xlink:href="#g167" y="27370"/><use xlink:href="#g168" y="27387"/><use xlink:href="#g171" y="27404"/><use xlink:href="#g172" y="27421"/><use xlink:href="#g173" y="27438"/><use xlink:href="#g196" y="27455"/><use xlink:href="#g197" y="27472"/><rect x="264" y="27489" width="8" height="17" class="foreground"/><use xlink:href="#g225" y="27489"/><rect x="184" y="27506" width="136" height="17" fill="#ffffff"/><rect x="320" y="27506" width="8" height="17" fill="#444444"/><use xlink:href="#g195" y="27506"/><rect x="184" y="27523" width="136" height="17" fill="#008787"/><rect x="320" y="27523" width="8" height="17" fill="#444444"/><use xlink:href="#g191" y="27523"/><rect x="184" y="27540" width="136" height="17" fill="#008787"/><rect x="320" y="27540" width="8" height="17" fill="#444444"/><use xlink:href="#g192" y="27540"/><rect x="184" y="27557" width="136" height="17" fill="#008787"/><rect x="320" y="27557" width="8" height="17" fill="#444444"/><use xlink:href="#g224" y="27557"/><use xlink:href="#g16" y="27574"/><use xlink:href="#g16" y="27591"/><use xlink:href="#g16" y="27608"/><rect x="0" y="27625" width="528" height="17" fill="#262626"/><use xlink:href="#g210" y="27625"/></g><g><use xlink:href="#g138" y="27676"/><use xlink:href="#g139" y="27693"/><use xlink:href="#g167" y="27710"/><use xlink:href="#g168" y="27727"/><use xlink:href="#g171" y="27744"/><use xlink:href="#g172" y="27761"/><use xlink:href="#g173" y="27778"/><use xlink:href="#g196" y="27795"/><use xlink:href="#g197" y="27812"/><use xlink:href="#g226" y="27829"/><use xlink:href="#g227" y="27846"/><rect x="128" y="27863" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="27863"/><use xlink:href="#g16" y="27880"/><use xlink:href="#g28" y="27897"/><use xlink:href="#g16" y="27914"/><use xlink:href="#g16" y="27931"/><use xlink:href="#g16" y="27948"/><use xlink:href="#g16" y="27965"/><use xlink:href="#g16" y="27982"/><rect x="0" y="27999" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="27999"/></g><g><use xlink:href="#g138" y="28050"/><use xlink:href="#g139" y="28067"/><use xlink:href="#g167" y="28084"/><use xlink:href="#g168" y="28101"/><use xlink:href="#g171" y="28118"/><use xlink:href="#g172" y="28135"/><use xlink:href="#g173" y="28152"/><use xlink:href="#g196" y="28169"/><use xlink:href="#g197" y="28186"/><use xlink:href="#g226" y="28203"/><use xlink:href="#g227" y="28220"/><rect x="136" y="28237" width="8" height="17" class="foreground"/><use xlink:href="#g198" y="28237"/><rect x="136" y="28254" width="80" height="17" fill="#008787"/><rect x="216" y="28254" width="8" height="17" fill="#444444"/><use xlink:href="#g199" y="28254"/><rect x="136" y="28271" width="80" height="17" fill="#008787"/><rect x="216" y="28271" width="8" height="17" fill="#444444"/><use xlink:href="#g228" y="28271"/><rect x="136" y="28288" width="80" height="17" fill="#008787"/><rect x="216" y="28288" width="8" height="17" fill="#444444"/><use xlink:href="#g201" y="28288"/><rect x="136" y="28305" width="80" height="17" fill="#008787"/><rect x="216" y="28305" width="8" height="17" fill="#444444"/><use xlink:href="#g229" y="28305"/><rect x="136" y="28322" width="80" height="17" fill="#008787"/><rect x="216" y="28322" width="8" height="17" fill="#00afaf"/><use xlink:href="#g203" y="28322"/><rect x="136" y="28339" width="80" height="17" fill="#008787"/><rect x="216" y="28339" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g204" y="28339"/><rect x="136" y="28356" width="80" height="17" fill="#008787"/><rect x="216" y="28356" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g205" y="28356"/><rect x="0" y="28373" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="28373"/></g><g><use xlink:href="#g138" y="28424"/><use xlink:href="#g139" y="28441"/><use xlink:href="#g167" y="28458"/><use xlink:href="#g168" y="28475"/><use xlink:href="#g171" y="28492"/><use xlink:href="#g172" y="28509"/><use xlink:href="#g173" y="28526"/><use xlink:href="#g196" y="28543"/><use xlink:href="#g197" y="28560"/><use xlink:href="#g226" y="28577"/><use xlink:href="#g227" y="28594"/><rect x="144" y="28611" width="8" height="17" class="foreground"/><use xlink:href="#g230" y="28611"/><rect x="144" y="28628" width="64" height="17" fill="#008787"/><rect x="208" y="28628" width="8" height="17" fill="#444444"/><use xlink:href="#g231" y="28628"/><rect x="144" y="28645" width="64" height="17" fill="#008787"/><rect x="208" y="28645" width="8" height="17" fill="#444444"/><use xlink:href="#g232" y="28645"/><use xlink:href="#g16" y="28662"/><use xlink:href="#g16" y="28679"/><use xlink:href="#g16" y="28696"/><use xlink:href="#g16" y="28713"/><use xlink:href="#g16" y="28730"/><rect x="0" y="28747" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="28747"/></g><g><use xlink:href="#g138" y="28798"/><use xlink:href="#g139" y="28815"/><use xlink:href="#g167" y="28832"/><use xlink:href="#g168" y="28849"/><use xlink:href="#g171" y="28866"/><use xlink:href="#g172" y="28883"/><use xlink:href="#g173" y="28900"/><use xlink:href="#g196" y="28917"/><use xlink:href="#g197" y="28934"/><use xlink:href="#g226" y="28951"/><use xlink:href="#g227" y="28968"/><rect x="152" y="28985" width="8" height="17" class="foreground"/><use xlink:href="#g233" y="28985"/><rect x="152" y="29002" width="64" height="17" fill="#008787"/><rect x="216" y="29002" width="8" height="17" fill="#444444"/><use xlink:href="#g234" y="29002"/><use xlink:href="#g28" y="29019"/><use xlink:href="#g16" y="29036"/><use xlink:href="#g16" y="29053"/><use xlink:href="#g16" y="29070"/><use xlink:href="#g16" y="29087"/><use xlink:href="#g16" y="29104"/><rect x="0" y="29121" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="29121"/></g><g><use xlink:href="#g138" y="29172"/><use xlink:href="#g139" y="29189"/><use xlink:href="#g167" y="29206"/><use xlink:href="#g168" y="29223"/><use xlink:href="#g171" y="29240"/><use xlink:href="#g172" y="29257"/><use xlink:href="#g173" y="29274"/><use xlink:href="#g196" y="29291"/><use xlink:href="#g197" y="29308"/><use xlink:href="#g226" y="29325"/><use xlink:href="#g227" y="29342"/><rect x="160" y="29359" width="8" height="17" class="foreground"/><use xlink:href="#g235" y="29359"/><rect x="160" y="29376" width="64" height="17" fill="#008787"/><rect x="224" y="29376" width="8" height="17" fill="#444444"/><use xlink:href="#g236" y="29376"/><use xlink:href="#g28" y="29393"/><use xlink:href="#g16" y="29410"/><use xlink:href="#g16" y="29427"/><use xlink:href="#g16" y="29444"/><use xlink:href="#g16" y="29461"/><use xlink:href="#g16" y="29478"/><rect x="0" y="29495" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="29495"/></g><g><use xlink:href="#g138" y="29546"/><use xlink:href="#g139" y="29563"/><use xlink:href="#g167" y="29580"/><use xlink:href="#g168" y="29597"/><use xlink:href="#g171" y="29614"/><use xlink:href="#g172" y="29631"/><use xlink:href="#g173" y="29648"/><use xlink:href="#g196" y="29665"/><use xlink:href="#g197" y="29682"/><use xlink:href="#g226" y="29699"/><use xlink:href="#g227" y="29716"/><rect x="168" y="29733" width="8" height="17" class="foreground"/><use xlink:href="#g237" y="29733"/><rect x="168" y="29750" width="64" height="17" fill="#008787"/><rect x="232" y="29750" width="8" height="17" fill="#444444"/><use xlink:href="#g238" y="29750"/><use xlink:href="#g28" y="29767"/><use xlink:href="#g16" y="29784"/><use xlink:href="#g16" y="29801"/><use xlink:href="#g16" y="29818"/><use xlink:href="#g16" y="29835"/><use xlink:href="#g16" y="29852"/><rect x="0" y="29869" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="29869"/></g><g><use xlink:href="#g138" y="29920"/><use xlink:href="#g139" y="29937"/><use xlink:href="#g167" y="29954"/><use xlink:href="#g168" y="29971"/><use xlink:href="#g171" y="29988"/><use xlink:href="#g172" y="30005"/><use xlink:href="#g173" y="30022"/><use xlink:href="#g196" y="30039"/><use xlink:href="#g197" y="30056"/><use xlink:href="#g226" y="30073"/><use xlink:href="#g227" y="30090"/><rect x="176" y="30107" width="8" height="17" class="foreground"/><use xlink:href="#g239" y="30107"/><rect x="176" y="30124" width="64" height="17" fill="#008787"/><rect x="240" y="30124" width="8" height="17" fill="#444444"/><use xlink:href="#g240" y="30124"/><use xlink:href="#g28" y="30141"/><use xlink:href="#g16" y="30158"/><use xlink:href="#g16" y="30175"/><use xlink:href="#g16" y="30192"/><use xlink:href="#g16" y="30209"/><use xlink:href="#g16" y="30226"/><rect x="0" y="30243" width="528" height="17" fill="#262626"/><use xlink:href="#g241" y="30243"/></g><g><use xlink:href="#g138" y="30294"/><use xlink:href="#g139" y="30311"/><use xlink:href="#g167" y="30328"/><use xlink:href="#g168" y="30345"/><use xlink:href="#g171" y="30362"/><use xlink:href="#g172" y="30379"/><use xlink:href="#g173" y="30396"/><use xlink:href="#g196" y="30413"/><use xlink:href="#g197" y="30430"/><use xlink:href="#g226" y="30447"/><use xlink:href="#g227" y="30464"/><rect x="184" y="30481" width="8" height="17" class="foreground"/><use xlink:href="#g242" y="30481"/><rect x="184" y="30498" width="136" height="17" fill="#008787"/><rect x="320" y="30498" width="8" height="17" fill="#444444"/><use xlink:href="#g243" y="30498"/><rect x="184" y="30515" width="136" height="17" fill="#008787"/><rect x="320" y="30515" width="8" height="17" fill="#444444"/><use xlink:href="#g244" y="30515"/><rect x="184" y="30532" width="136" height="17" fill="#008787"/><rect x="320" y="30532" width="8" height="17" fill="#00afaf"/><use xlink:href="#g245" y="30532"/><rect x="184" y="30549" width="136" height="17" fill="#008787"/><rect x="320" y="30549" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g246" y="30549"/><rect x="184" y="30566" width="136" height="17" fill="#008787"/><rect x="320" y="30566" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g247" y="30566"/><rect x="184" y="30583" width="136" height="17" fill="#008787"/><rect x="320" y="30583" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g248" y="30583"/><rect x="184" y="30600" width="136" height="17" fill="#008787"/><rect x="320" y="30600" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g249" y="30600"/><rect x="0" y="30617" width="528" height="17" fill="#262626"/><use xlink:href="#g241" y="30617"/></g><g><use xlink:href="#g138" y="30668"/><use xlink:href="#g139" y="30685"/><use xlink:href="#g167" y="30702"/><use xlink:href="#g168" y="30719"/><use xlink:href="#g171" y="30736"/><use xlink:href="#g172" y="30753"/><use xlink:href="#g173" y="30770"/><use xlink:href="#g196" y="30787"/><use xlink:href="#g197" y="30804"/><use xlink:href="#g226" y="30821"/><use xlink:href="#g227" y="30838"/><rect x="192" y="30855" width="8" height="17" class="foreground"/><use xlink:href="#g250" y="30855"/><rect x="192" y="30872" width="136" height="17" fill="#008787"/><rect x="328" y="30872" width="8" height="17" fill="#444444"/><use xlink:href="#g251" y="30872"/><rect x="192" y="30889" width="136" height="17" fill="#008787"/><rect x="328" y="30889" width="8" height="17" fill="#444444"/><use xlink:href="#g252" y="30889"/><rect x="192" y="30906" width="136" height="17" fill="#008787"/><rect x="328" y="30906" width="8" height="17" fill="#444444"/><use xlink:href="#g253" y="30906"/><rect x="192" y="30923" width="136" height="17" fill="#008787"/><rect x="328" y="30923" width="8" height="17" fill="#444444"/><use xlink:href="#g254" y="30923"/><rect x="192" y="30940" width="136" height="17" fill="#008787"/><rect x="328" y="30940" width="8" height="17" fill="#444444"/><use xlink:href="#g255" y="30940"/><use xlink:href="#g16" y="30957"/><use xlink:href="#g16" y="30974"/><rect x="0" y="30991" width="528" height="17" fill="#262626"/><use xlink:href="#g241" y="30991"/></g><g><use xlink:href="#g138" y="31042"/><use xlink:href="#g139" y="31059"/><use xlink:href="#g167" y="31076"/><use xlink:href="#g168" y="31093"/><use xlink:href="#g171" y="31110"/><use xlink:href="#g172" y="31127"/><use xlink:href="#g173" y="31144"/><use xlink:href="#g196" y="31161"/><use xlink:href="#g197" y="31178"/><use xlink:href="#g226" y="31195"/><use xlink:href="#g227" y="31212"/><rect x="280" y="31229" width="8" height="17" class="foreground"/><use xlink:href="#g256" y="31229"/><rect x="192" y="31246" width="136" height="17" fill="#ffffff"/><rect x="328" y="31246" width="8" height="17" fill="#444444"/><use xlink:href="#g257" y="31246"/><rect x="192" y="31263" width="136" height="17" fill="#008787"/><rect x="328" y="31263" width="8" height="17" fill="#444444"/><use xlink:href="#g252" y="31263"/><rect x="192" y="31280" width="136" height="17" fill="#008787"/><rect x="328" y="31280" width="8" height="17" fill="#444444"/><use xlink:href="#g253" y="31280"/><rect x="192" y="31297" width="136" height="17" fill="#008787"/><rect x="328" y="31297" width="8" height="17" fill="#444444"/><use xlink:href="#g254" y="31297"/><rect x="192" y="31314" width="136" height="17" fill="#008787"/><rect x="328" y="31314" width="8" height="17" fill="#444444"/><use xlink:href="#g255" y="31314"/><use xlink:href="#g16" y="31331"/><use xlink:href="#g16" y="31348"/><rect x="0" y="31365" width="528" height="17" fill="#262626"/><use xlink:href="#g241" y="31365"/></g><g><use xlink:href="#g138" y="31416"/><use xlink:href="#g139" y="31433"/><use xlink:href="#g167" y="31450"/><use xlink:href="#g168" y="31467"/><use xlink:href="#g171" y="31484"/><use xlink:href="#g172" y="31501"/><use xlink:href="#g173" y="31518"/><use xlink:href="#g196" y="31535"/><use xlink:href="#g197" y="31552"/><use xlink:href="#g226" y="31569"/><use xlink:href="#g227" y="31586"/><rect x="288" y="31603" width="8" height="17" class="foreground"/><use xlink:href="#g258" y="31603"/><use xlink:href="#g16" y="31620"/><use xlink:href="#g28" y="31637"/><use xlink:href="#g16" y="31654"/><use xlink:href="#g16" y="31671"/><use xlink:href="#g16" y="31688"/><use xlink:href="#g16" y="31705"/><use xlink:href="#g16" y="31722"/><rect x="0" y="31739" width="528" height="17" fill="#262626"/><use xlink:href="#g241" y="31739"/></g><g><use xlink:href="#g138" y="31790"/><use xlink:href="#g139" y="31807"/><use xlink:href="#g167" y="31824"/><use xlink:href="#g168" y="31841"/><use xlink:href="#g171" y="31858"/><use xlink:href="#g172" y="31875"/><use xlink:href="#g173" y="31892"/><use xlink:href="#g196" y="31909"/><use xlink:href="#g197" y="31926"/><use xlink:href="#g226" y="31943"/><use xlink:href="#g227" y="31960"/><rect x="296" y="31977" width="8" height="17" class="foreground"/><use xlink:href="#g259" y="31977"/><use xlink:href="#g16" y="31994"/><use xlink:href="#g28" y="32011"/><use xlink:href="#g16" y="32028"/><use xlink:href="#g16" y="32045"/><use xlink:href="#g16" y="32062"/><use xlink:href="#g16" y="32079"/><use xlink:href="#g16" y="32096"/><rect x="0" y="32113" width="528" height="17" fill="#262626"/><use xlink:href="#g241" y="32113"/></g><g><use xlink:href="#g138" y="32164"/><use xlink:href="#g139" y="32181"/><use xlink:href="#g167" y="32198"/><use xlink:href="#g168" y="32215"/><use xlink:href="#g171" y="32232"/><use xlink:href="#g172" y="32249"/><use xlink:href="#g173" y="32266"/><use xlink:href="#g196" y="32283"/><use xlink:href="#g197" y="32300"/><use xlink:href="#g226" y="32317"/><use xlink:href="#g227" y="32334"/><rect x="304" y="32351" width="8" height="17" class="foreground"/><use xlink:href="#g260" y="32351"/><use xlink:href="#g16" y="32368"/><use xlink:href="#g28" y="32385"/><use xlink:href="#g16" y="32402"/><use xlink:href="#g16" y="32419"/><use xlink:href="#g16" y="32436"/><use xlink:href="#g16" y="32453"/><use xlink:href="#g16" y="32470"/><rect x="0" y="32487" width="528" height="17" fill="#262626"/><use xlink:href="#g241" y="32487"/></g><g><use xlink:href="#g138" y="32538"/><use xlink:href="#g139" y="32555"/><use xlink:href="#g167" y="32572"/><use xlink:href="#g168" y="32589"/><use xlink:href="#g171" y="32606"/><use xlink:href="#g172" y="32623"/><use xlink:href="#g173" y="32640"/><use xlink:href="#g196" y="32657"/><use xlink:href="#g197" y="32674"/><use xlink:href="#g226" y="32691"/><use xlink:href="#g227" y="32708"/><rect x="312" y="32725" width="8" height="17" class="foreground"/><use xlink:href="#g261" y="32725"/><use xlink:href="#g16" y="32742"/><use xlink:href="#g28" y="32759"/><use xlink:href="#g16" y="32776"/><use xlink:href="#g16" y="32793"/><use xlink:href="#g16" y="32810"/><use xlink:href="#g16" y="32827"/><use xlink:href="#g16" y="32844"/><rect x="0" y="32861" width="528" height="17" fill="#262626"/><use xlink:href="#g241" y="32861"/></g><g><use xlink:href="#g197" y="32912"/><use xlink:href="#g226" y="32929"/><use xlink:href="#g227" y="32946"/><use xlink:href="#g262" y="32963"/><use xlink:href="#g263" y="32980"/><use xlink:href="#g264" y="32997"/><use xlink:href="#g265" y="33014"/><use xlink:href="#g266" y="33031"/><use xlink:href="#g267" y="33048"/><use xlink:href="#g268" y="33065"/><use xlink:href="#g269" y="33082"/><rect x="128" y="33099" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="33099"/><use xlink:href="#g16" y="33218"/><rect x="0" y="33235" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="33235"/></g><g><use xlink:href="#g197" y="33286"/><use xlink:href="#g226" y="33303"/><use xlink:href="#g227" y="33320"/><use xlink:href="#g262" y="33337"/><use xlink:href="#g263" y="33354"/><use xlink:href="#g264" y="33371"/><use xlink:href="#g265" y="33388"/><use xlink:href="#g266" y="33405"/><use xlink:href="#g267" y="33422"/><use xlink:href="#g268" y="33439"/><use xlink:href="#g269" y="33456"/><rect x="136" y="33473" width="8" height="17" class="foreground"/><use xlink:href="#g270" y="33473"/><rect x="136" y="33490" width="56" height="17" fill="#008787"/><rect x="192" y="33490" width="8" height="17" fill="#444444"/><use xlink:href="#g42" y="33490"/><rect x="136" y="33507" width="56" height="17" fill="#008787"/><rect x="192" y="33507" width="8" height="17" fill="#444444"/><use xlink:href="#g43" y="33507"/><rect x="136" y="33524" width="56" height="17" fill="#008787"/><rect x="192" y="33524" width="8" height="17" fill="#444444"/><use xlink:href="#g44" y="33524"/><rect x="136" y="33541" width="56" height="17" fill="#008787"/><rect x="192" y="33541" width="8" height="17" fill="#444444"/><use xlink:href="#g45" y="33541"/><use xlink:href="#g16" y="33592"/><rect x="0" y="33609" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="33609"/></g><g><use xlink:href="#g197" y="33660"/><use xlink:href="#g226" y="33677"/><use xlink:href="#g227" y="33694"/><use xlink:href="#g262" y="33711"/><use xlink:href="#g263" y="33728"/><use xlink:href="#g264" y="33745"/><use xlink:href="#g265" y="33762"/><use xlink:href="#g266" y="33779"/><use xlink:href="#g267" y="33796"/><use xlink:href="#g268" y="33813"/><use xlink:href="#g269" y="33830"/><rect x="144" y="33847" width="8" height="17" class="foreground"/><use xlink:href="#g271" y="33847"/><rect x="144" y="33864" width="56" height="17" fill="#008787"/><rect x="200" y="33864" width="8" height="17" fill="#444444"/><use xlink:href="#g47" y="33864"/><use xlink:href="#g28" y="33881"/><use xlink:href="#g28" y="33898"/><use xlink:href="#g28" y="33915"/><use xlink:href="#g16" y="33966"/><rect x="0" y="33983" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="33983"/></g><g><use xlink:href="#g197" y="34034"/><use xlink:href="#g226" y="34051"/><use xlink:href="#g227" y="34068"/><use xlink:href="#g262" y="34085"/><use xlink:href="#g263" y="34102"/><use xlink:href="#g264" y="34119"/><use xlink:href="#g265" y="34136"/><use xlink:href="#g266" y="34153"/><use xlink:href="#g267" y="34170"/><use xlink:href="#g268" y="34187"/><use xlink:href="#g269" y="34204"/><rect x="152" y="34221" width="8" height="17" class="foreground"/><use xlink:href="#g272" y="34221"/><rect x="152" y="34238" width="56" height="17" fill="#008787"/><rect x="208" y="34238" width="8" height="17" fill="#444444"/><use xlink:href="#g49" y="34238"/><use xlink:href="#g28" y="34255"/><use xlink:href="#g28" y="34272"/><use xlink:href="#g28" y="34289"/><use xlink:href="#g16" y="34340"/><rect x="0" y="34357" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="34357"/></g><g><use xlink:href="#g197" y="34408"/><use xlink:href="#g226" y="34425"/><use xlink:href="#g227" y="34442"/><use xlink:href="#g262" y="34459"/><use xlink:href="#g263" y="34476"/><use xlink:href="#g264" y="34493"/><use xlink:href="#g265" y="34510"/><use xlink:href="#g266" y="34527"/><use xlink:href="#g267" y="34544"/><use xlink:href="#g268" y="34561"/><use xlink:href="#g269" y="34578"/><rect x="160" y="34595" width="8" height="17" class="foreground"/><use xlink:href="#g273" y="34595"/><rect x="160" y="34612" width="56" height="17" fill="#008787"/><rect x="216" y="34612" width="8" height="17" fill="#444444"/><use xlink:href="#g51" y="34612"/><use xlink:href="#g28" y="34629"/><use xlink:href="#g28" y="34646"/><use xlink:href="#g28" y="34663"/><use xlink:href="#g16" y="34714"/><rect x="0" y="34731" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="34731"/></g><g><use xlink:href="#g197" y="34782"/><use xlink:href="#g226" y="34799"/><use xlink:href="#g227" y="34816"/><use xlink:href="#g262" y="34833"/><use xlink:href="#g263" y="34850"/><use xlink:href="#g264" y="34867"/><use xlink:href="#g265" y="34884"/><use xlink:href="#g266" y="34901"/><use xlink:href="#g267" y="34918"/><use xlink:href="#g268" y="34935"/><use xlink:href="#g269" y="34952"/><rect x="168" y="34969" width="8" height="17" class="foreground"/><use xlink:href="#g274" y="34969"/><rect x="168" y="34986" width="136" height="17" fill="#008787"/><rect x="304" y="34986" width="8" height="17" fill="#444444"/><use xlink:href="#g275" y="34986"/><rect x="168" y="35003" width="136" height="17" fill="#008787"/><rect x="304" y="35003" width="8" height="17" fill="#444444"/><use xlink:href="#g276" y="35003"/><rect x="168" y="35020" width="136" height="17" fill="#008787"/><rect x="304" y="35020" width="8" height="17" fill="#00afaf"/><use xlink:href="#g277" y="35020"/><rect x="168" y="35037" width="136" height="17" fill="#008787"/><rect x="304" y="35037" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g213" y="35037"/><rect x="168" y="35054" width="136" height="17" fill="#008787"/><rect x="304" y="35054" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g278" y="35054"/><rect x="168" y="35071" width="136" height="17" fill="#008787"/><rect x="304" y="35071" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g279" y="35071"/><rect x="168" y="35088" width="136" height="17" fill="#008787"/><rect x="304" y="35088" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g182" y="35088"/><rect x="0" y="35105" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="35105"/></g><g><use xlink:href="#g197" y="35156"/><use xlink:href="#g226" y="35173"/><use xlink:href="#g227" y="35190"/><use xlink:href="#g262" y="35207"/><use xlink:href="#g263" y="35224"/><use xlink:href="#g264" y="35241"/><use xlink:href="#g265" y="35258"/><use xlink:href="#g266" y="35275"/><use xlink:href="#g267" y="35292"/><use xlink:href="#g268" y="35309"/><use xlink:href="#g269" y="35326"/><rect x="264" y="35343" width="8" height="17" class="foreground"/><use xlink:href="#g194" y="35343"/><rect x="168" y="35360" width="136" height="17" fill="#ffffff"/><rect x="304" y="35360" width="8" height="17" fill="#444444"/><use xlink:href="#g280" y="35360"/><rect x="168" y="35377" width="136" height="17" fill="#008787"/><rect x="304" y="35377" width="8" height="17" fill="#444444"/><use xlink:href="#g276" y="35377"/><rect x="168" y="35394" width="136" height="17" fill="#008787"/><rect x="304" y="35394" width="8" height="17" fill="#00afaf"/><use xlink:href="#g277" y="35394"/><rect x="168" y="35411" width="136" height="17" fill="#008787"/><rect x="304" y="35411" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g213" y="35411"/><rect x="168" y="35428" width="136" height="17" fill="#008787"/><rect x="304" y="35428" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g278" y="35428"/><rect x="168" y="35445" width="136" height="17" fill="#008787"/><rect x="304" y="35445" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g279" y="35445"/><rect x="168" y="35462" width="136" height="17" fill="#008787"/><rect x="304" y="35462" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g182" y="35462"/><rect x="0" y="35479" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="35479"/></g><g><use xlink:href="#g197" y="35530"/><use xlink:href="#g226" y="35547"/><use xlink:href="#g227" y="35564"/><use xlink:href="#g262" y="35581"/><use xlink:href="#g263" y="35598"/><use xlink:href="#g264" y="35615"/><use xlink:href="#g265" y="35632"/><use xlink:href="#g266" y="35649"/><use xlink:href="#g267" y="35666"/><use xlink:href="#g268" y="35683"/><use xlink:href="#g269" y="35700"/><rect x="200" y="35717" width="8" height="17" class="foreground"/><use xlink:href="#g136" y="35717"/><rect x="168" y="35734" width="136" height="17" fill="#008787"/><rect x="304" y="35734" width="8" height="17" fill="#444444"/><use xlink:href="#g275" y="35734"/><rect x="168" y="35751" width="136" height="17" fill="#ffffff"/><rect x="304" y="35751" width="8" height="17" fill="#444444"/><use xlink:href="#g281" y="35751"/><rect x="168" y="35768" width="136" height="17" fill="#008787"/><rect x="304" y="35768" width="8" height="17" fill="#00afaf"/><use xlink:href="#g277" y="35768"/><rect x="168" y="35785" width="136" height="17" fill="#008787"/><rect x="304" y="35785" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g213" y="35785"/><rect x="168" y="35802" width="136" height="17" fill="#008787"/><rect x="304" y="35802" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g278" y="35802"/><rect x="168" y="35819" width="136" height="17" fill="#008787"/><rect x="304" y="35819" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g279" y="35819"/><rect x="168" y="35836" width="136" height="17" fill="#008787"/><rect x="304" y="35836" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g182" y="35836"/><rect x="0" y="35853" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="35853"/></g><g><use xlink:href="#g197" y="35904"/><use xlink:href="#g226" y="35921"/><use xlink:href="#g227" y="35938"/><use xlink:href="#g262" y="35955"/><use xlink:href="#g263" y="35972"/><use xlink:href="#g264" y="35989"/><use xlink:href="#g265" y="36006"/><use xlink:href="#g266" y="36023"/><use xlink:href="#g267" y="36040"/><use xlink:href="#g268" y="36057"/><use xlink:href="#g269" y="36074"/><rect x="208" y="36091" width="8" height="17" class="foreground"/><use xlink:href="#g72" y="36091"/><rect x="168" y="36108" width="136" height="17" fill="#008787"/><rect x="304" y="36108" width="8" height="17" fill="#444444"/><use xlink:href="#g275" y="36108"/><rect x="168" y="36125" width="136" height="17" fill="#008787"/><rect x="304" y="36125" width="8" height="17" fill="#444444"/><use xlink:href="#g276" y="36125"/><rect x="168" y="36142" width="136" height="17" fill="#ffffff"/><rect x="304" y="36142" width="8" height="17" fill="#00afaf"/><use xlink:href="#g282" y="36142"/><rect x="168" y="36159" width="136" height="17" fill="#008787"/><rect x="304" y="36159" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g213" y="36159"/><rect x="168" y="36176" width="136" height="17" fill="#008787"/><rect x="304" y="36176" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g278" y="36176"/><rect x="168" y="36193" width="136" height="17" fill="#008787"/><rect x="304" y="36193" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g279" y="36193"/><rect x="168" y="36210" width="136" height="17" fill="#008787"/><rect x="304" y="36210" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g182" y="36210"/><rect x="0" y="36227" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="36227"/></g><g><use xlink:href="#g197" y="36278"/><use xlink:href="#g226" y="36295"/><use xlink:href="#g227" y="36312"/><use xlink:href="#g262" y="36329"/><use xlink:href="#g263" y="36346"/><use xlink:href="#g264" y="36363"/><use xlink:href="#g265" y="36380"/><use xlink:href="#g266" y="36397"/><use xlink:href="#g267" y="36414"/><use xlink:href="#g268" y="36431"/><use xlink:href="#g269" y="36448"/><rect x="192" y="36465" width="8" height="17" class="foreground"/><use xlink:href="#g283" y="36465"/><rect x="168" y="36482" width="136" height="17" fill="#008787"/><rect x="304" y="36482" width="8" height="17" fill="#444444"/><use xlink:href="#g275" y="36482"/><rect x="168" y="36499" width="136" height="17" fill="#008787"/><rect x="304" y="36499" width="8" height="17" fill="#444444"/><use xlink:href="#g276" y="36499"/><rect x="168" y="36516" width="136" height="17" fill="#008787"/><rect x="304" y="36516" width="8" height="17" fill="#00afaf"/><use xlink:href="#g277" y="36516"/><rect x="168" y="36533" width="136" height="17" fill="#ffffff"/><rect x="304" y="36533" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g284" y="36533"/><rect x="168" y="36550" width="136" height="17" fill="#008787"/><rect x="304" y="36550" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g278" y="36550"/><rect x="168" y="36567" width="136" height="17" fill="#008787"/><rect x="304" y="36567" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g279" y="36567"/><rect x="168" y="36584" width="136" height="17" fill="#008787"/><rect x="304" y="36584" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g182" y="36584"/><rect x="0" y="36601" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="36601"/></g><g><use xlink:href="#g197" y="36652"/><use xlink:href="#g226" y="36669"/><use xlink:href="#g227" y="36686"/><use xlink:href="#g262" y="36703"/><use xlink:href="#g263" y="36720"/><use xlink:href="#g264" y="36737"/><use xlink:href="#g265" y="36754"/><use xlink:href="#g266" y="36771"/><use xlink:href="#g267" y="36788"/><use xlink:href="#g268" y="36805"/><use xlink:href="#g269" y="36822"/><rect x="248" y="36839" width="8" height="17" class="foreground"/><use xlink:href="#g285" y="36839"/><rect x="168" y="36856" width="136" height="17" fill="#008787"/><rect x="304" y="36856" width="8" height="17" fill="#444444"/><use xlink:href="#g275" y="36856"/><rect x="168" y="36873" width="136" height="17" fill="#008787"/><rect x="304" y="36873" width="8" height="17" fill="#444444"/><use xlink:href="#g276" y="36873"/><rect x="168" y="36890" width="136" height="17" fill="#008787"/><rect x="304" y="36890" width="8" height="17" fill="#00afaf"/><use xlink:href="#g277" y="36890"/><rect x="168" y="36907" width="136" height="17" fill="#008787"/><rect x="304" y="36907" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g213" y="36907"/><rect x="168" y="36924" width="136" height="17" fill="#ffffff"/><rect x="304" y="36924" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g286" y="36924"/><rect x="168" y="36941" width="136" height="17" fill="#008787"/><rect x="304" y="36941" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g279" y="36941"/><rect x="168" y="36958" width="136" height="17" fill="#008787"/><rect x="304" y="36958" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g182" y="36958"/><rect x="0" y="36975" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="36975"/></g><g><use xlink:href="#g197" y="37026"/><use xlink:href="#g226" y="37043"/><use xlink:href="#g227" y="37060"/><use xlink:href="#g262" y="37077"/><use xlink:href="#g263" y="37094"/><use xlink:href="#g264" y="37111"/><use xlink:href="#g265" y="37128"/><use xlink:href="#g266" y="37145"/><use xlink:href="#g267" y="37162"/><use xlink:href="#g268" y="37179"/><use xlink:href="#g269" y="37196"/><rect x="184" y="37213" width="8" height="17" class="foreground"/><use xlink:href="#g287" y="37213"/><rect x="168" y="37230" width="136" height="17" fill="#008787"/><rect x="304" y="37230" width="8" height="17" fill="#444444"/><use xlink:href="#g275" y="37230"/><rect x="168" y="37247" width="136" height="17" fill="#008787"/><rect x="304" y="37247" width="8" height="17" fill="#444444"/><use xlink:href="#g276" y="37247"/><rect x="168" y="37264" width="136" height="17" fill="#008787"/><rect x="304" y="37264" width="8" height="17" fill="#00afaf"/><use xlink:href="#g277" y="37264"/><rect x="168" y="37281" width="136" height="17" fill="#008787"/><rect x="304" y="37281" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g213" y="37281"/><rect x="168" y="37298" width="136" height="17" fill="#008787"/><rect x="304" y="37298" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g278" y="37298"/><rect x="168" y="37315" width="136" height="17" fill="#ffffff"/><rect x="304" y="37315" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g288" y="37315"/><rect x="168" y="37332" width="136" height="17" fill="#008787"/><rect x="304" y="37332" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g182" y="37332"/><rect x="0" y="37349" width="528" height="17" fill="#262626"/><use xlink:href="#g52" y="37349"/></g><g><use xlink:href="#g227" y="37400"/><use xlink:href="#g262" y="37417"/><use xlink:href="#g263" y="37434"/><use xlink:href="#g264" y="37451"/><use xlink:href="#g265" y="37468"/><use xlink:href="#g266" y="37485"/><use xlink:href="#g267" y="37502"/><use xlink:href="#g268" y="37519"/><use xlink:href="#g269" y="37536"/><use xlink:href="#g289" y="37553"/><use xlink:href="#g290" y="37570"/><rect x="128" y="37587" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="37587"/><use xlink:href="#g28" y="37604"/><use xlink:href="#g28" y="37621"/><use xlink:href="#g291" y="37638"/><use xlink:href="#g291" y="37655"/><use xlink:href="#g16" y="37672"/><use xlink:href="#g16" y="37689"/><use xlink:href="#g16" y="37706"/><rect x="0" y="37723" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="37723"/></g><g><use xlink:href="#g227" y="37774"/><use xlink:href="#g262" y="37791"/><use xlink:href="#g263" y="37808"/><use xlink:href="#g264" y="37825"/><use xlink:href="#g265" y="37842"/><use xlink:href="#g266" y="37859"/><use xlink:href="#g267" y="37876"/><use xlink:href="#g268" y="37893"/><use xlink:href="#g269" y="37910"/><use xlink:href="#g289" y="37927"/><use xlink:href="#g290" y="37944"/><rect x="136" y="37961" width="8" height="17" class="foreground"/><use xlink:href="#g292" y="37961"/><rect x="136" y="37978" width="152" height="17" fill="#008787"/><rect x="288" y="37978" width="8" height="17" fill="#444444"/><use xlink:href="#g293" y="37978"/><rect x="136" y="37995" width="152" height="17" fill="#008787"/><rect x="288" y="37995" width="8" height="17" fill="#444444"/><use xlink:href="#g294" y="37995"/><rect x="136" y="38012" width="152" height="17" fill="#008787"/><rect x="288" y="38012" width="8" height="17" fill="#444444"/><use xlink:href="#g295" y="38012"/><rect x="136" y="38029" width="152" height="17" fill="#008787"/><rect x="288" y="38029" width="8" height="17" fill="#444444"/><use xlink:href="#g296" y="38029"/><rect x="136" y="38046" width="152" height="17" fill="#008787"/><rect x="288" y="38046" width="8" height="17" fill="#00afaf"/><use xlink:href="#g297" y="38046"/><rect x="136" y="38063" width="152" height="17" fill="#008787"/><rect x="288" y="38063" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g298" y="38063"/><rect x="136" y="38080" width="152" height="17" fill="#008787"/><rect x="288" y="38080" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g299" y="38080"/><rect x="0" y="38097" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="38097"/></g><g><use xlink:href="#g227" y="38148"/><use xlink:href="#g262" y="38165"/><use xlink:href="#g263" y="38182"/><use xlink:href="#g264" y="38199"/><use xlink:href="#g265" y="38216"/><use xlink:href="#g266" y="38233"/><use xlink:href="#g267" y="38250"/><use xlink:href="#g268" y="38267"/><use xlink:href="#g269" y="38284"/><use xlink:href="#g289" y="38301"/><use xlink:href="#g290" y="38318"/><rect x="144" y="38335" width="8" height="17" class="foreground"/><use xlink:href="#g300" y="38335"/><rect x="144" y="38352" width="152" height="17" fill="#008787"/><rect x="296" y="38352" width="8" height="17" fill="#444444"/><use xlink:href="#g301" y="38352"/><rect x="144" y="38369" width="152" height="17" fill="#008787"/><rect x="296" y="38369" width="8" height="17" fill="#444444"/><use xlink:href="#g302" y="38369"/><rect x="144" y="38386" width="152" height="17" fill="#008787"/><rect x="296" y="38386" width="8" height="17" fill="#444444"/><use xlink:href="#g303" y="38386"/><rect x="144" y="38403" width="152" height="17" fill="#008787"/><rect x="296" y="38403" width="8" height="17" fill="#444444"/><use xlink:href="#g304" y="38403"/><rect x="144" y="38420" width="152" height="17" fill="#008787"/><rect x="296" y="38420" width="8" height="17" fill="#00afaf"/><use xlink:href="#g305" y="38420"/><rect x="144" y="38437" width="152" height="17" fill="#008787"/><rect x="296" y="38437" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g306" y="38437"/><rect x="144" y="38454" width="152" height="17" fill="#008787"/><rect x="296" y="38454" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g307" y="38454"/><rect x="0" y="38471" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="38471"/></g><g><use xlink:href="#g227" y="38522"/><use xlink:href="#g262" y="38539"/><use xlink:href="#g263" y="38556"/><use xlink:href="#g264" y="38573"/><use xlink:href="#g265" y="38590"/><use xlink:href="#g266" y="38607"/><use xlink:href="#g267" y="38624"/><use xlink:href="#g268" y="38641"/><use xlink:href="#g269" y="38658"/><use xlink:href="#g289" y="38675"/><use xlink:href="#g290" y="38692"/><rect x="152" y="38709" width="8" height="17" class="foreground"/><use xlink:href="#g308" y="38709"/><rect x="152" y="38726" width="80" height="17" fill="#008787"/><rect x="232" y="38726" width="8" height="17" fill="#444444"/><use xlink:href="#g309" y="38726"/><rect x="152" y="38743" width="80" height="17" fill="#008787"/><rect x="232" y="38743" width="8" height="17" fill="#444444"/><use xlink:href="#g310" y="38743"/><rect x="152" y="38760" width="80" height="17" fill="#008787"/><rect x="232" y="38760" width="8" height="17" fill="#444444"/><use xlink:href="#g311" y="38760"/><rect x="152" y="38777" width="80" height="17" fill="#008787"/><rect x="232" y="38777" width="8" height="17" fill="#444444"/><use xlink:href="#g312" y="38777"/><use xlink:href="#g16" y="38794"/><use xlink:href="#g16" y="38811"/><use xlink:href="#g16" y="38828"/><rect x="0" y="38845" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="38845"/></g><g><use xlink:href="#g227" y="38896"/><use xlink:href="#g262" y="38913"/><use xlink:href="#g263" y="38930"/><use xlink:href="#g264" y="38947"/><use xlink:href="#g265" y="38964"/><use xlink:href="#g266" y="38981"/><use xlink:href="#g267" y="38998"/><use xlink:href="#g268" y="39015"/><use xlink:href="#g269" y="39032"/><use xlink:href="#g289" y="39049"/><use xlink:href="#g290" y="39066"/><rect x="160" y="39083" width="8" height="17" class="foreground"/><use xlink:href="#g314" y="39083"/><rect x="160" y="39100" width="136" height="17" fill="#008787"/><rect x="296" y="39100" width="8" height="17" fill="#444444"/><use xlink:href="#g315" y="39100"/><rect x="160" y="39117" width="136" height="17" fill="#008787"/><rect x="296" y="39117" width="8" height="17" fill="#444444"/><use xlink:href="#g316" y="39117"/><rect x="160" y="39134" width="136" height="17" fill="#008787"/><rect x="296" y="39134" width="8" height="17" fill="#00afaf"/><use xlink:href="#g317" y="39134"/><rect x="160" y="39151" width="136" height="17" fill="#008787"/><rect x="296" y="39151" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g318" y="39151"/><rect x="160" y="39168" width="136" height="17" fill="#008787"/><rect x="296" y="39168" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g319" y="39168"/><rect x="160" y="39185" width="136" height="17" fill="#008787"/><rect x="296" y="39185" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g320" y="39185"/><rect x="160" y="39202" width="136" height="17" fill="#008787"/><rect x="296" y="39202" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g321" y="39202"/><rect x="0" y="39219" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="39219"/></g><g><use xlink:href="#g227" y="39270"/><use xlink:href="#g262" y="39287"/><use xlink:href="#g263" y="39304"/><use xlink:href="#g264" y="39321"/><use xlink:href="#g265" y="39338"/><use xlink:href="#g266" y="39355"/><use xlink:href="#g267" y="39372"/><use xlink:href="#g268" y="39389"/><use xlink:href="#g269" y="39406"/><use xlink:href="#g289" y="39423"/><use xlink:href="#g290" y="39440"/><rect x="168" y="39457" width="8" height="17" class="foreground"/><use xlink:href="#g322" y="39457"/><rect x="168" y="39474" width="136" height="17" fill="#008787"/><rect x="304" y="39474" width="8" height="17" fill="#444444"/><use xlink:href="#g323" y="39474"/><rect x="168" y="39491" width="136" height="17" fill="#008787"/><rect x="304" y="39491" width="8" height="17" fill="#444444"/><use xlink:href="#g324" y="39491"/><rect x="168" y="39508" width="136" height="17" fill="#008787"/><rect x="304" y="39508" width="8" height="17" fill="#444444"/><use xlink:href="#g325" y="39508"/><rect x="168" y="39525" width="136" height="17" fill="#008787"/><rect x="304" y="39525" width="8" height="17" fill="#444444"/><use xlink:href="#g326" y="39525"/><rect x="168" y="39542" width="136" height="17" fill="#008787"/><rect x="304" y="39542" width="8" height="17" fill="#00afaf"/><use xlink:href="#g327" y="39542"/><rect x="168" y="39559" width="136" height="17" fill="#008787"/><rect x="304" y="39559" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g328" y="39559"/><rect x="168" y="39576" width="136" height="17" fill="#008787"/><rect x="304" y="39576" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g329" y="39576"/><rect x="0" y="39593" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="39593"/></g><g><use xlink:href="#g227" y="39644"/><use xlink:href="#g262" y="39661"/><use xlink:href="#g263" y="39678"/><use xlink:href="#g264" y="39695"/><use xlink:href="#g265" y="39712"/><use xlink:href="#g266" y="39729"/><use xlink:href="#g267" y="39746"/><use xlink:href="#g268" y="39763"/><use xlink:href="#g269" y="39780"/><use xlink:href="#g289" y="39797"/><use xlink:href="#g290" y="39814"/><rect x="176" y="39831" width="8" height="17" class="foreground"/><use xlink:href="#g330" y="39831"/><use xlink:href="#g28" y="39848"/><use xlink:href="#g28" y="39865"/><use xlink:href="#g28" y="39882"/><use xlink:href="#g28" y="39899"/><use xlink:href="#g16" y="39916"/><use xlink:href="#g16" y="39933"/><use xlink:href="#g16" y="39950"/><rect x="0" y="39967" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="39967"/></g><g><use xlink:href="#g263" y="40018"/><use xlink:href="#g264" y="40035"/><use xlink:href="#g265" y="40052"/><use xlink:href="#g266" y="40069"/><use xlink:href="#g267" y="40086"/><use xlink:href="#g268" y="40103"/><use xlink:href="#g269" y="40120"/><use xlink:href="#g289" y="40137"/><use xlink:href="#g290" y="40154"/><use xlink:href="#g331" y="40171"/><use xlink:href="#g332" y="40188"/><rect x="128" y="40205" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="40205"/><use xlink:href="#g28" y="40222"/><use xlink:href="#g28" y="40239"/><use xlink:href="#g16" y="40256"/><use xlink:href="#g16" y="40273"/><use xlink:href="#g16" y="40290"/><use xlink:href="#g16" y="40307"/><use xlink:href="#g16" y="40324"/><rect x="0" y="40341" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="40341"/></g><g><use xlink:href="#g263" y="40392"/><use xlink:href="#g264" y="40409"/><use xlink:href="#g265" y="40426"/><use xlink:href="#g266" y="40443"/><use xlink:href="#g267" y="40460"/><use xlink:href="#g268" y="40477"/><use xlink:href="#g269" y="40494"/><use xlink:href="#g289" y="40511"/><use xlink:href="#g290" y="40528"/><use xlink:href="#g331" y="40545"/><use xlink:href="#g332" y="40562"/><rect x="136" y="40579" width="8" height="17" class="foreground"/><use xlink:href="#g333" y="40579"/><rect x="136" y="40596" width="152" height="17" fill="#008787"/><rect x="288" y="40596" width="8" height="17" fill="#444444"/><use xlink:href="#g293" y="40596"/><rect x="136" y="40613" width="152" height="17" fill="#008787"/><rect x="288" y="40613" width="8" height="17" fill="#444444"/><use xlink:href="#g294" y="40613"/><rect x="136" y="40630" width="152" height="17" fill="#008787"/><rect x="288" y="40630" width="8" height="17" fill="#444444"/><use xlink:href="#g334" y="40630"/><rect x="136" y="40647" width="152" height="17" fill="#008787"/><rect x="288" y="40647" width="8" height="17" fill="#444444"/><use xlink:href="#g335" y="40647"/><rect x="136" y="40664" width="152" height="17" fill="#008787"/><rect x="288" y="40664" width="8" height="17" fill="#00afaf"/><use xlink:href="#g297" y="40664"/><rect x="136" y="40681" width="152" height="17" fill="#008787"/><rect x="288" y="40681" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g298" y="40681"/><rect x="136" y="40698" width="152" height="17" fill="#008787"/><rect x="288" y="40698" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g299" y="40698"/><rect x="0" y="40715" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="40715"/></g><g><use xlink:href="#g263" y="40766"/><use xlink:href="#g264" y="40783"/><use xlink:href="#g265" y="40800"/><use xlink:href="#g266" y="40817"/><use xlink:href="#g267" y="40834"/><use xlink:href="#g268" y="40851"/><use xlink:href="#g269" y="40868"/><use xlink:href="#g289" y="40885"/><use xlink:href="#g290" y="40902"/><use xlink:href="#g331" y="40919"/><use xlink:href="#g332" y="40936"/><rect x="144" y="40953" width="8" height="17" class="foreground"/><use xlink:href="#g336" y="40953"/><rect x="144" y="40970" width="152" height="17" fill="#008787"/><rect x="296" y="40970" width="8" height="17" fill="#444444"/><use xlink:href="#g301" y="40970"/><rect x="144" y="40987" width="152" height="17" fill="#008787"/><rect x="296" y="40987" width="8" height="17" fill="#444444"/><use xlink:href="#g302" y="40987"/><rect x="144" y="41004" width="152" height="17" fill="#008787"/><rect x="296" y="41004" width="8" height="17" fill="#444444"/><use xlink:href="#g337" y="41004"/><rect x="144" y="41021" width="152" height="17" fill="#008787"/><rect x="296" y="41021" width="8" height="17" fill="#444444"/><use xlink:href="#g338" y="41021"/><rect x="144" y="41038" width="152" height="17" fill="#008787"/><rect x="296" y="41038" width="8" height="17" fill="#00afaf"/><use xlink:href="#g305" y="41038"/><rect x="144" y="41055" width="152" height="17" fill="#008787"/><rect x="296" y="41055" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g306" y="41055"/><rect x="144" y="41072" width="152" height="17" fill="#008787"/><rect x="296" y="41072" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g307" y="41072"/><rect x="0" y="41089" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="41089"/></g><g><use xlink:href="#g263" y="41140"/><use xlink:href="#g264" y="41157"/><use xlink:href="#g265" y="41174"/><use xlink:href="#g266" y="41191"/><use xlink:href="#g267" y="41208"/><use xlink:href="#g268" y="41225"/><use xlink:href="#g269" y="41242"/><use xlink:href="#g289" y="41259"/><use xlink:href="#g290" y="41276"/><use xlink:href="#g331" y="41293"/><use xlink:href="#g332" y="41310"/><rect x="152" y="41327" width="8" height="17" class="foreground"/><use xlink:href="#g339" y="41327"/><rect x="152" y="41344" width="80" height="17" fill="#008787"/><rect x="232" y="41344" width="8" height="17" fill="#444444"/><use xlink:href="#g309" y="41344"/><rect x="152" y="41361" width="80" height="17" fill="#008787"/><rect x="232" y="41361" width="8" height="17" fill="#444444"/><use xlink:href="#g310" y="41361"/><rect x="152" y="41378" width="80" height="17" fill="#008787"/><rect x="232" y="41378" width="8" height="17" fill="#444444"/><use xlink:href="#g340" y="41378"/><rect x="152" y="41395" width="80" height="17" fill="#008787"/><rect x="232" y="41395" width="8" height="17" fill="#444444"/><use xlink:href="#g341" y="41395"/><use xlink:href="#g16" y="41412"/><use xlink:href="#g16" y="41429"/><use xlink:href="#g16" y="41446"/><rect x="0" y="41463" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="41463"/></g><g><use xlink:href="#g263" y="41514"/><use xlink:href="#g264" y="41531"/><use xlink:href="#g265" y="41548"/><use xlink:href="#g266" y="41565"/><use xlink:href="#g267" y="41582"/><use xlink:href="#g268" y="41599"/><use xlink:href="#g269" y="41616"/><use xlink:href="#g289" y="41633"/><use xlink:href="#g290" y="41650"/><use xlink:href="#g331" y="41667"/><use xlink:href="#g332" y="41684"/><rect x="160" y="41701" width="8" height="17" class="foreground"/><use xlink:href="#g342" y="41701"/><rect x="160" y="41718" width="136" height="17" fill="#008787"/><rect x="296" y="41718" width="8" height="17" fill="#444444"/><use xlink:href="#g315" y="41718"/><rect x="160" y="41735" width="136" height="17" fill="#008787"/><rect x="296" y="41735" width="8" height="17" fill="#444444"/><use xlink:href="#g316" y="41735"/><rect x="160" y="41752" width="136" height="17" fill="#008787"/><rect x="296" y="41752" width="8" height="17" fill="#00afaf"/><use xlink:href="#g343" y="41752"/><rect x="160" y="41769" width="136" height="17" fill="#008787"/><rect x="296" y="41769" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g344" y="41769"/><rect x="160" y="41786" width="136" height="17" fill="#008787"/><rect x="296" y="41786" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g319" y="41786"/><rect x="160" y="41803" width="136" height="17" fill="#008787"/><rect x="296" y="41803" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g320" y="41803"/><rect x="160" y="41820" width="136" height="17" fill="#008787"/><rect x="296" y="41820" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g321" y="41820"/><rect x="0" y="41837" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="41837"/></g><g><use xlink:href="#g263" y="41888"/><use xlink:href="#g264" y="41905"/><use xlink:href="#g265" y="41922"/><use xlink:href="#g266" y="41939"/><use xlink:href="#g267" y="41956"/><use xlink:href="#g268" y="41973"/><use xlink:href="#g269" y="41990"/><use xlink:href="#g289" y="42007"/><use xlink:href="#g290" y="42024"/><use xlink:href="#g331" y="42041"/><use xlink:href="#g332" y="42058"/><rect x="168" y="42075" width="8" height="17" class="foreground"/><use xlink:href="#g345" y="42075"/><rect x="168" y="42092" width="136" height="17" fill="#008787"/><rect x="304" y="42092" width="8" height="17" fill="#444444"/><use xlink:href="#g323" y="42092"/><rect x="168" y="42109" width="136" height="17" fill="#008787"/><rect x="304" y="42109" width="8" height="17" fill="#444444"/><use xlink:href="#g324" y="42109"/><rect x="168" y="42126" width="136" height="17" fill="#008787"/><rect x="304" y="42126" width="8" height="17" fill="#444444"/><use xlink:href="#g346" y="42126"/><rect x="168" y="42143" width="136" height="17" fill="#008787"/><rect x="304" y="42143" width="8" height="17" fill="#444444"/><use xlink:href="#g347" y="42143"/><rect x="168" y="42160" width="136" height="17" fill="#008787"/><rect x="304" y="42160" width="8" height="17" fill="#00afaf"/><use xlink:href="#g327" y="42160"/><rect x="168" y="42177" width="136" height="17" fill="#008787"/><rect x="304" y="42177" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g328" y="42177"/><rect x="168" y="42194" width="136" height="17" fill="#008787"/><rect x="304" y="42194" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g329" y="42194"/><rect x="0" y="42211" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="42211"/></g><g><use xlink:href="#g263" y="42262"/><use xlink:href="#g264" y="42279"/><use xlink:href="#g265" y="42296"/><use xlink:href="#g266" y="42313"/><use xlink:href="#g267" y="42330"/><use xlink:href="#g268" y="42347"/><use xlink:href="#g269" y="42364"/><use xlink:href="#g289" y="42381"/><use xlink:href="#g290" y="42398"/><use xlink:href="#g331" y="42415"/><use xlink:href="#g332" y="42432"/><rect x="176" y="42449" width="8" height="17" class="foreground"/><use xlink:href="#g330" y="42449"/><use xlink:href="#g28" y="42466"/><use xlink:href="#g28" y="42483"/><use xlink:href="#g16" y="42500"/><use xlink:href="#g16" y="42517"/><use xlink:href="#g16" y="42534"/><use xlink:href="#g16" y="42551"/><use xlink:href="#g16" y="42568"/><rect x="0" y="42585" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="42585"/></g><g><use xlink:href="#g263" y="42636"/><use xlink:href="#g264" y="42653"/><use xlink:href="#g265" y="42670"/><use xlink:href="#g266" y="42687"/><use xlink:href="#g267" y="42704"/><use xlink:href="#g268" y="42721"/><use xlink:href="#g269" y="42738"/><use xlink:href="#g289" y="42755"/><use xlink:href="#g290" y="42772"/><use xlink:href="#g331" y="42789"/><use xlink:href="#g332" y="42806"/><rect x="184" y="42823" width="8" height="17" class="foreground"/><use xlink:href="#g348" y="42823"/><use xlink:href="#g28" y="42840"/><use xlink:href="#g28" y="42857"/><use xlink:href="#g16" y="42874"/><use xlink:href="#g16" y="42891"/><use xlink:href="#g16" y="42908"/><use xlink:href="#g16" y="42925"/><use xlink:href="#g16" y="42942"/><rect x="0" y="42959" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="42959"/></g><g><use xlink:href="#g263" y="43010"/><use xlink:href="#g264" y="43027"/><use xlink:href="#g265" y="43044"/><use xlink:href="#g266" y="43061"/><use xlink:href="#g267" y="43078"/><use xlink:href="#g268" y="43095"/><use xlink:href="#g269" y="43112"/><use xlink:href="#g289" y="43129"/><use xlink:href="#g290" y="43146"/><use xlink:href="#g331" y="43163"/><use xlink:href="#g332" y="43180"/><rect x="184" y="43197" width="8" height="17" fill="#ff0000"/><rect x="192" y="43197" width="8" height="17" class="foreground"/><use xlink:href="#g349" y="43197"/><use xlink:href="#g28" y="43214"/><use xlink:href="#g28" y="43231"/><use xlink:href="#g16" y="43248"/><use xlink:href="#g16" y="43265"/><use xlink:href="#g16" y="43282"/><use xlink:href="#g16" y="43299"/><use xlink:href="#g16" y="43316"/><rect x="0" y="43333" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="43333"/></g><g><use xlink:href="#g263" y="43384"/><use xlink:href="#g264" y="43401"/><use xlink:href="#g265" y="43418"/><use xlink:href="#g266" y="43435"/><use xlink:href="#g267" y="43452"/><use xlink:href="#g268" y="43469"/><use xlink:href="#g269" y="43486"/><use xlink:href="#g289" y="43503"/><use xlink:href="#g290" y="43520"/><use xlink:href="#g331" y="43537"/><use xlink:href="#g332" y="43554"/><rect x="184" y="43571" width="16" height="17" fill="#ff0000"/><rect x="200" y="43571" width="8" height="17" class="foreground"/><use xlink:href="#g350" y="43571"/><use xlink:href="#g28" y="43588"/><use xlink:href="#g28" y="43605"/><use xlink:href="#g16" y="43622"/><use xlink:href="#g16" y="43639"/><use xlink:href="#g16" y="43656"/><use xlink:href="#g16" y="43673"/><use xlink:href="#g16" y="43690"/><rect x="0" y="43707" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="43707"/></g><g><use xlink:href="#g263" y="43758"/><use xlink:href="#g264" y="43775"/><use xlink:href="#g265" y="43792"/><use xlink:href="#g266" y="43809"/><use xlink:href="#g267" y="43826"/><use xlink:href="#g268" y="43843"/><use xlink:href="#g269" y="43860"/><use xlink:href="#g289" y="43877"/><use xlink:href="#g290" y="43894"/><use xlink:href="#g331" y="43911"/><use xlink:href="#g332" y="43928"/><rect x="184" y="43945" width="24" height="17" fill="#ff0000"/><rect x="208" y="43945" width="8" height="17" class="foreground"/><use xlink:href="#g351" y="43945"/><use xlink:href="#g28" y="43962"/><use xlink:href="#g28" y="43979"/><use xlink:href="#g16" y="43996"/><use xlink:href="#g16" y="44013"/><use xlink:href="#g16" y="44030"/><use xlink:href="#g16" y="44047"/><use xlink:href="#g16" y="44064"/><rect x="0" y="44081" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="44081"/></g><g><use xlink:href="#g263" y="44132"/><use xlink:href="#g264" y="44149"/><use xlink:href="#g265" y="44166"/><use xlink:href="#g266" y="44183"/><use xlink:href="#g267" y="44200"/><use xlink:href="#g268" y="44217"/><use xlink:href="#g269" y="44234"/><use xlink:href="#g289" y="44251"/><use xlink:href="#g290" y="44268"/><use xlink:href="#g331" y="44285"/><use xlink:href="#g332" y="44302"/><rect x="184" y="44319" width="32" height="17" fill="#ff0000"/><rect x="216" y="44319" width="8" height="17" class="foreground"/><use xlink:href="#g352" y="44319"/><use xlink:href="#g28" y="44336"/><use xlink:href="#g28" y="44353"/><use xlink:href="#g16" y="44370"/><use xlink:href="#g16" y="44387"/><use xlink:href="#g16" y="44404"/><use xlink:href="#g16" y="44421"/><use xlink:href="#g16" y="44438"/><rect x="0" y="44455" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="44455"/></g><g><use xlink:href="#g263" y="44506"/><use xlink:href="#g264" y="44523"/><use xlink:href="#g265" y="44540"/><use xlink:href="#g266" y="44557"/><use xlink:href="#g267" y="44574"/><use xlink:href="#g268" y="44591"/><use xlink:href="#g269" y="44608"/><use xlink:href="#g289" y="44625"/><use xlink:href="#g290" y="44642"/><use xlink:href="#g331" y="44659"/><use xlink:href="#g332" y="44676"/><rect x="184" y="44693" width="40" height="17" fill="#ff0000"/><rect x="224" y="44693" width="8" height="17" class="foreground"/><use xlink:href="#g353" y="44693"/><use xlink:href="#g28" y="44710"/><use xlink:href="#g28" y="44727"/><use xlink:href="#g16" y="44744"/><use xlink:href="#g16" y="44761"/><use xlink:href="#g16" y="44778"/><use xlink:href="#g16" y="44795"/><use xlink:href="#g16" y="44812"/><rect x="0" y="44829" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="44829"/></g><g><use xlink:href="#g263" y="44880"/><use xlink:href="#g264" y="44897"/><use xlink:href="#g265" y="44914"/><use xlink:href="#g266" y="44931"/><use xlink:href="#g267" y="44948"/><use xlink:href="#g268" y="44965"/><use xlink:href="#g269" y="44982"/><use xlink:href="#g289" y="44999"/><use xlink:href="#g290" y="45016"/><use xlink:href="#g331" y="45033"/><use xlink:href="#g332" y="45050"/><rect x="184" y="45067" width="48" height="17" fill="#ff0000"/><rect x="232" y="45067" width="8" height="17" class="foreground"/><use xlink:href="#g354" y="45067"/><use xlink:href="#g28" y="45084"/><use xlink:href="#g28" y="45101"/><use xlink:href="#g16" y="45118"/><use xlink:href="#g16" y="45135"/><use xlink:href="#g16" y="45152"/><use xlink:href="#g16" y="45169"/><use xlink:href="#g16" y="45186"/><rect x="0" y="45203" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="45203"/></g><g><use xlink:href="#g263" y="45254"/><use xlink:href="#g264" y="45271"/><use xlink:href="#g265" y="45288"/><use xlink:href="#g266" y="45305"/><use xlink:href="#g267" y="45322"/><use xlink:href="#g268" y="45339"/><use xlink:href="#g269" y="45356"/><use xlink:href="#g289" y="45373"/><use xlink:href="#g290" y="45390"/><use xlink:href="#g331" y="45407"/><use xlink:href="#g332" y="45424"/><rect x="184" y="45441" width="56" height="17" fill="#ff0000"/><rect x="240" y="45441" width="8" height="17" class="foreground"/><use xlink:href="#g355" y="45441"/><use xlink:href="#g28" y="45458"/><use xlink:href="#g28" y="45475"/><use xlink:href="#g16" y="45492"/><use xlink:href="#g16" y="45509"/><use xlink:href="#g16" y="45526"/><use xlink:href="#g16" y="45543"/><use xlink:href="#g16" y="45560"/><rect x="0" y="45577" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="45577"/></g><g><use xlink:href="#g263" y="45628"/><use xlink:href="#g264" y="45645"/><use xlink:href="#g265" y="45662"/><use xlink:href="#g266" y="45679"/><use xlink:href="#g267" y="45696"/><use xlink:href="#g268" y="45713"/><use xlink:href="#g269" y="45730"/><use xlink:href="#g289" y="45747"/><use xlink:href="#g290" y="45764"/><use xlink:href="#g331" y="45781"/><use xlink:href="#g332" y="45798"/><rect x="184" y="45815" width="64" height="17" fill="#ff0000"/><rect x="248" y="45815" width="8" height="17" class="foreground"/><use xlink:href="#g356" y="45815"/><use xlink:href="#g28" y="45832"/><use xlink:href="#g28" y="45849"/><use xlink:href="#g16" y="45866"/><use xlink:href="#g16" y="45883"/><use xlink:href="#g16" y="45900"/><use xlink:href="#g16" y="45917"/><use xlink:href="#g16" y="45934"/><rect x="0" y="45951" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="45951"/></g><g><use xlink:href="#g263" y="46002"/><use xlink:href="#g264" y="46019"/><use xlink:href="#g265" y="46036"/><use xlink:href="#g266" y="46053"/><use xlink:href="#g267" y="46070"/><use xlink:href="#g268" y="46087"/><use xlink:href="#g269" y="46104"/><use xlink:href="#g289" y="46121"/><use xlink:href="#g290" y="46138"/><use xlink:href="#g331" y="46155"/><use xlink:href="#g332" y="46172"/><rect x="184" y="46189" width="72" height="17" fill="#ff0000"/><rect x="256" y="46189" width="8" height="17" class="foreground"/><use xlink:href="#g357" y="46189"/><use xlink:href="#g28" y="46206"/><use xlink:href="#g28" y="46223"/><use xlink:href="#g16" y="46240"/><use xlink:href="#g16" y="46257"/><use xlink:href="#g16" y="46274"/><use xlink:href="#g16" y="46291"/><use xlink:href="#g16" y="46308"/><rect x="0" y="46325" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="46325"/></g><g><use xlink:href="#g263" y="46376"/><use xlink:href="#g264" y="46393"/><use xlink:href="#g265" y="46410"/><use xlink:href="#g266" y="46427"/><use xlink:href="#g267" y="46444"/><use xlink:href="#g268" y="46461"/><use xlink:href="#g269" y="46478"/><use xlink:href="#g289" y="46495"/><use xlink:href="#g290" y="46512"/><use xlink:href="#g331" y="46529"/><use xlink:href="#g332" y="46546"/><rect x="184" y="46563" width="80" height="17" fill="#ff0000"/><rect x="264" y="46563" width="8" height="17" class="foreground"/><use xlink:href="#g358" y="46563"/><use xlink:href="#g28" y="46580"/><use xlink:href="#g28" y="46597"/><use xlink:href="#g16" y="46614"/><use xlink:href="#g16" y="46631"/><use xlink:href="#g16" y="46648"/><use xlink:href="#g16" y="46665"/><use xlink:href="#g16" y="46682"/><rect x="0" y="46699" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="46699"/></g><g><use xlink:href="#g263" y="46750"/><use xlink:href="#g264" y="46767"/><use xlink:href="#g265" y="46784"/><use xlink:href="#g266" y="46801"/><use xlink:href="#g267" y="46818"/><use xlink:href="#g268" y="46835"/><use xlink:href="#g269" y="46852"/><use xlink:href="#g289" y="46869"/><use xlink:href="#g290" y="46886"/><use xlink:href="#g331" y="46903"/><use xlink:href="#g332" y="46920"/><rect x="184" y="46937" width="88" height="17" fill="#ff0000"/><rect x="272" y="46937" width="8" height="17" class="foreground"/><use xlink:href="#g359" y="46937"/><use xlink:href="#g28" y="46954"/><use xlink:href="#g28" y="46971"/><use xlink:href="#g16" y="46988"/><use xlink:href="#g16" y="47005"/><use xlink:href="#g16" y="47022"/><use xlink:href="#g16" y="47039"/><use xlink:href="#g16" y="47056"/><rect x="0" y="47073" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="47073"/></g><g><use xlink:href="#g263" y="47124"/><use xlink:href="#g264" y="47141"/><use xlink:href="#g265" y="47158"/><use xlink:href="#g266" y="47175"/><use xlink:href="#g267" y="47192"/><use xlink:href="#g268" y="47209"/><use xlink:href="#g269" y="47226"/><use xlink:href="#g289" y="47243"/><use xlink:href="#g290" y="47260"/><use xlink:href="#g331" y="47277"/><use xlink:href="#g332" y="47294"/><rect x="184" y="47311" width="96" height="17" fill="#ff0000"/><rect x="280" y="47311" width="8" height="17" class="foreground"/><use xlink:href="#g360" y="47311"/><use xlink:href="#g28" y="47328"/><use xlink:href="#g28" y="47345"/><use xlink:href="#g16" y="47362"/><use xlink:href="#g16" y="47379"/><use xlink:href="#g16" y="47396"/><use xlink:href="#g16" y="47413"/><use xlink:href="#g16" y="47430"/><rect x="0" y="47447" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="47447"/></g><g><use xlink:href="#g263" y="47498"/><use xlink:href="#g264" y="47515"/><use xlink:href="#g265" y="47532"/><use xlink:href="#g266" y="47549"/><use xlink:href="#g267" y="47566"/><use xlink:href="#g268" y="47583"/><use xlink:href="#g269" y="47600"/><use xlink:href="#g289" y="47617"/><use xlink:href="#g290" y="47634"/><use xlink:href="#g331" y="47651"/><use xlink:href="#g332" y="47668"/><rect x="184" y="47685" width="104" height="17" fill="#ff0000"/><rect x="288" y="47685" width="8" height="17" class="foreground"/><use xlink:href="#g361" y="47685"/><use xlink:href="#g28" y="47702"/><use xlink:href="#g28" y="47719"/><use xlink:href="#g16" y="47736"/><use xlink:href="#g16" y="47753"/><use xlink:href="#g16" y="47770"/><use xlink:href="#g16" y="47787"/><use xlink:href="#g16" y="47804"/><rect x="0" y="47821" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="47821"/></g><g><use xlink:href="#g263" y="47872"/><use xlink:href="#g264" y="47889"/><use xlink:href="#g265" y="47906"/><use xlink:href="#g266" y="47923"/><use xlink:href="#g267" y="47940"/><use xlink:href="#g268" y="47957"/><use xlink:href="#g269" y="47974"/><use xlink:href="#g289" y="47991"/><use xlink:href="#g290" y="48008"/><use xlink:href="#g331" y="48025"/><use xlink:href="#g332" y="48042"/><rect x="184" y="48059" width="112" height="17" fill="#ff0000"/><rect x="296" y="48059" width="8" height="17" class="foreground"/><use xlink:href="#g362" y="48059"/><use xlink:href="#g28" y="48076"/><use xlink:href="#g28" y="48093"/><use xlink:href="#g16" y="48110"/><use xlink:href="#g16" y="48127"/><use xlink:href="#g16" y="48144"/><use xlink:href="#g16" y="48161"/><use xlink:href="#g16" y="48178"/><rect x="0" y="48195" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="48195"/></g><g><use xlink:href="#g263" y="48246"/><use xlink:href="#g264" y="48263"/><use xlink:href="#g265" y="48280"/><use xlink:href="#g266" y="48297"/><use xlink:href="#g267" y="48314"/><use xlink:href="#g268" y="48331"/><use xlink:href="#g269" y="48348"/><use xlink:href="#g289" y="48365"/><use xlink:href="#g290" y="48382"/><use xlink:href="#g331" y="48399"/><use xlink:href="#g332" y="48416"/><rect x="184" y="48433" width="104" height="17" fill="#ff0000"/><rect x="288" y="48433" width="8" height="17" class="foreground"/><use xlink:href="#g361" y="48433"/><use xlink:href="#g28" y="48450"/><use xlink:href="#g28" y="48467"/><use xlink:href="#g16" y="48484"/><use xlink:href="#g16" y="48501"/><use xlink:href="#g16" y="48518"/><use xlink:href="#g16" y="48535"/><use xlink:href="#g16" y="48552"/><rect x="0" y="48569" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="48569"/></g><g><use xlink:href="#g263" y="48620"/><use xlink:href="#g264" y="48637"/><use xlink:href="#g265" y="48654"/><use xlink:href="#g266" y="48671"/><use xlink:href="#g267" y="48688"/><use xlink:href="#g268" y="48705"/><use xlink:href="#g269" y="48722"/><use xlink:href="#g289" y="48739"/><use xlink:href="#g290" y="48756"/><use xlink:href="#g331" y="48773"/><use xlink:href="#g332" y="48790"/><rect x="184" y="48807" width="88" height="17" fill="#ff0000"/><rect x="272" y="48807" width="8" height="17" class="foreground"/><use xlink:href="#g359" y="48807"/><use xlink:href="#g28" y="48824"/><use xlink:href="#g28" y="48841"/><use xlink:href="#g16" y="48858"/><use xlink:href="#g16" y="48875"/><use xlink:href="#g16" y="48892"/><use xlink:href="#g16" y="48909"/><use xlink:href="#g16" y="48926"/><rect x="0" y="48943" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="48943"/></g><g><use xlink:href="#g263" y="48994"/><use xlink:href="#g264" y="49011"/><use xlink:href="#g265" y="49028"/><use xlink:href="#g266" y="49045"/><use xlink:href="#g267" y="49062"/><use xlink:href="#g268" y="49079"/><use xlink:href="#g269" y="49096"/><use xlink:href="#g289" y="49113"/><use xlink:href="#g290" y="49130"/><use xlink:href="#g331" y="49147"/><use xlink:href="#g332" y="49164"/><rect x="184" y="49181" width="80" height="17" fill="#ff0000"/><rect x="264" y="49181" width="8" height="17" class="foreground"/><use xlink:href="#g358" y="49181"/><use xlink:href="#g28" y="49198"/><use xlink:href="#g28" y="49215"/><use xlink:href="#g16" y="49232"/><use xlink:href="#g16" y="49249"/><use xlink:href="#g16" y="49266"/><use xlink:href="#g16" y="49283"/><use xlink:href="#g16" y="49300"/><rect x="0" y="49317" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="49317"/></g><g><use xlink:href="#g263" y="49368"/><use xlink:href="#g264" y="49385"/><use xlink:href="#g265" y="49402"/><use xlink:href="#g266" y="49419"/><use xlink:href="#g267" y="49436"/><use xlink:href="#g268" y="49453"/><use xlink:href="#g269" y="49470"/><use xlink:href="#g289" y="49487"/><use xlink:href="#g290" y="49504"/><use xlink:href="#g331" y="49521"/><use xlink:href="#g332" y="49538"/><rect x="184" y="49555" width="64" height="17" fill="#ff0000"/><rect x="248" y="49555" width="8" height="17" class="foreground"/><use xlink:href="#g356" y="49555"/><use xlink:href="#g28" y="49572"/><use xlink:href="#g28" y="49589"/><use xlink:href="#g16" y="49606"/><use xlink:href="#g16" y="49623"/><use xlink:href="#g16" y="49640"/><use xlink:href="#g16" y="49657"/><use xlink:href="#g16" y="49674"/><rect x="0" y="49691" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="49691"/></g><g><use xlink:href="#g263" y="49742"/><use xlink:href="#g264" y="49759"/><use xlink:href="#g265" y="49776"/><use xlink:href="#g266" y="49793"/><use xlink:href="#g267" y="49810"/><use xlink:href="#g268" y="49827"/><use xlink:href="#g269" y="49844"/><use xlink:href="#g289" y="49861"/><use xlink:href="#g290" y="49878"/><use xlink:href="#g331" y="49895"/><use xlink:href="#g332" y="49912"/><rect x="184" y="49929" width="48" height="17" fill="#ff0000"/><rect x="232" y="49929" width="8" height="17" class="foreground"/><use xlink:href="#g354" y="49929"/><use xlink:href="#g28" y="49946"/><use xlink:href="#g28" y="49963"/><use xlink:href="#g16" y="49980"/><use xlink:href="#g16" y="49997"/><use xlink:href="#g16" y="50014"/><use xlink:href="#g16" y="50031"/><use xlink:href="#g16" y="50048"/><rect x="0" y="50065" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="50065"/></g><g><use xlink:href="#g263" y="50116"/><use xlink:href="#g264" y="50133"/><use xlink:href="#g265" y="50150"/><use xlink:href="#g266" y="50167"/><use xlink:href="#g267" y="50184"/><use xlink:href="#g268" y="50201"/><use xlink:href="#g269" y="50218"/><use xlink:href="#g289" y="50235"/><use xlink:href="#g290" y="50252"/><use xlink:href="#g331" y="50269"/><use xlink:href="#g332" y="50286"/><rect x="184" y="50303" width="40" height="17" fill="#ff0000"/><rect x="224" y="50303" width="8" height="17" class="foreground"/><use xlink:href="#g353" y="50303"/><use xlink:href="#g28" y="50320"/><use xlink:href="#g28" y="50337"/><use xlink:href="#g16" y="50354"/><use xlink:href="#g16" y="50371"/><use xlink:href="#g16" y="50388"/><use xlink:href="#g16" y="50405"/><use xlink:href="#g16" y="50422"/><rect x="0" y="50439" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="50439"/></g><g><use xlink:href="#g263" y="50490"/><use xlink:href="#g264" y="50507"/><use xlink:href="#g265" y="50524"/><use xlink:href="#g266" y="50541"/><use xlink:href="#g267" y="50558"/><use xlink:href="#g268" y="50575"/><use xlink:href="#g269" y="50592"/><use xlink:href="#g289" y="50609"/><use xlink:href="#g290" y="50626"/><use xlink:href="#g331" y="50643"/><use xlink:href="#g332" y="50660"/><rect x="184" y="50677" width="24" height="17" fill="#ff0000"/><rect x="208" y="50677" width="8" height="17" class="foreground"/><use xlink:href="#g351" y="50677"/><use xlink:href="#g28" y="50694"/><use xlink:href="#g28" y="50711"/><use xlink:href="#g16" y="50728"/><use xlink:href="#g16" y="50745"/><use xlink:href="#g16" y="50762"/><use xlink:href="#g16" y="50779"/><use xlink:href="#g16" y="50796"/><rect x="0" y="50813" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="50813"/></g><g><use xlink:href="#g263" y="50864"/><use xlink:href="#g264" y="50881"/><use xlink:href="#g265" y="50898"/><use xlink:href="#g266" y="50915"/><use xlink:href="#g267" y="50932"/><use xlink:href="#g268" y="50949"/><use xlink:href="#g269" y="50966"/><use xlink:href="#g289" y="50983"/><use xlink:href="#g290" y="51000"/><use xlink:href="#g331" y="51017"/><use xlink:href="#g332" y="51034"/><rect x="184" y="51051" width="16" height="17" fill="#ff0000"/><rect x="200" y="51051" width="8" height="17" class="foreground"/><use xlink:href="#g350" y="51051"/><use xlink:href="#g28" y="51068"/><use xlink:href="#g28" y="51085"/><use xlink:href="#g16" y="51102"/><use xlink:href="#g16" y="51119"/><use xlink:href="#g16" y="51136"/><use xlink:href="#g16" y="51153"/><use xlink:href="#g16" y="51170"/><rect x="0" y="51187" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="51187"/></g><g><use xlink:href="#g263" y="51238"/><use xlink:href="#g264" y="51255"/><use xlink:href="#g265" y="51272"/><use xlink:href="#g266" y="51289"/><use xlink:href="#g267" y="51306"/><use xlink:href="#g268" y="51323"/><use xlink:href="#g269" y="51340"/><use xlink:href="#g289" y="51357"/><use xlink:href="#g290" y="51374"/><use xlink:href="#g331" y="51391"/><use xlink:href="#g332" y="51408"/><rect x="184" y="51425" width="8" height="17" fill="#ff0000"/><rect x="192" y="51425" width="8" height="17" class="foreground"/><use xlink:href="#g349" y="51425"/><use xlink:href="#g28" y="51442"/><use xlink:href="#g28" y="51459"/><use xlink:href="#g16" y="51476"/><use xlink:href="#g16" y="51493"/><use xlink:href="#g16" y="51510"/><use xlink:href="#g16" y="51527"/><use xlink:href="#g16" y="51544"/><rect x="0" y="51561" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="51561"/></g><g><use xlink:href="#g263" y="51612"/><use xlink:href="#g264" y="51629"/><use xlink:href="#g265" y="51646"/><use xlink:href="#g266" y="51663"/><use xlink:href="#g267" y="51680"/><use xlink:href="#g268" y="51697"/><use xlink:href="#g269" y="51714"/><use xlink:href="#g289" y="51731"/><use xlink:href="#g290" y="51748"/><use xlink:href="#g331" y="51765"/><use xlink:href="#g332" y="51782"/><rect x="184" y="51799" width="8" height="17" class="foreground"/><use xlink:href="#g348" y="51799"/><use xlink:href="#g28" y="51816"/><use xlink:href="#g28" y="51833"/><use xlink:href="#g16" y="51850"/><use xlink:href="#g16" y="51867"/><use xlink:href="#g16" y="51884"/><use xlink:href="#g16" y="51901"/><use xlink:href="#g16" y="51918"/><rect x="0" y="51935" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="51935"/></g><g><use xlink:href="#g263" y="51986"/><use xlink:href="#g264" y="52003"/><use xlink:href="#g265" y="52020"/><use xlink:href="#g266" y="52037"/><use xlink:href="#g267" y="52054"/><use xlink:href="#g268" y="52071"/><use xlink:href="#g269" y="52088"/><use xlink:href="#g289" y="52105"/><use xlink:href="#g290" y="52122"/><use xlink:href="#g331" y="52139"/><use xlink:href="#g332" y="52156"/><rect x="168" y="52173" width="8" height="17" class="foreground"/><use xlink:href="#g322" y="52173"/><use xlink:href="#g28" y="52190"/><use xlink:href="#g28" y="52207"/><use xlink:href="#g16" y="52224"/><use xlink:href="#g16" y="52241"/><use xlink:href="#g16" y="52258"/><use xlink:href="#g16" y="52275"/><use xlink:href="#g16" y="52292"/><rect x="0" y="52309" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="52309"/></g><g><use xlink:href="#g263" y="52360"/><use xlink:href="#g264" y="52377"/><use xlink:href="#g265" y="52394"/><use xlink:href="#g266" y="52411"/><use xlink:href="#g267" y="52428"/><use xlink:href="#g268" y="52445"/><use xlink:href="#g269" y="52462"/><use xlink:href="#g289" y="52479"/><use xlink:href="#g290" y="52496"/><use xlink:href="#g331" y="52513"/><use xlink:href="#g332" y="52530"/><rect x="160" y="52547" width="8" height="17" class="foreground"/><use xlink:href="#g363" y="52547"/><use xlink:href="#g28" y="52564"/><use xlink:href="#g28" y="52581"/><use xlink:href="#g16" y="52598"/><use xlink:href="#g16" y="52615"/><use xlink:href="#g16" y="52632"/><use xlink:href="#g16" y="52649"/><use xlink:href="#g16" y="52666"/><rect x="0" y="52683" width="528" height="17" fill="#262626"/><use xlink:href="#g313" y="52683"/></g><g><use xlink:href="#g263" y="52734"/><use xlink:href="#g264" y="52751"/><use xlink:href="#g265" y="52768"/><use xlink:href="#g266" y="52785"/><use xlink:href="#g267" y="52802"/><use xlink:href="#g268" y="52819"/><use xlink:href="#g269" y="52836"/><use xlink:href="#g289" y="52853"/><use xlink:href="#g290" y="52870"/><use xlink:href="#g331" y="52887"/><use xlink:href="#g332" y="52904"/><rect x="144" y="52921" width="8" height="17" class="foreground"/><use xlink:href="#g364" y="52921"/><use xlink:href="#g28" y="52938"/><use xlink:href="#g28" y="52955"/><use xlink:href="#g16" y="52972"/><use xlink:href="#g16" y="52989"/><use xlink:href="#g16" y="53006"/><use xlink:href="#g16" y="53023"/><use xlink:href="#g16" y="53040"/><rect x="0" y="53057" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="53057"/></g><g><use xlink:href="#g263" y="53108"/><use xlink:href="#g264" y="53125"/><use xlink:href="#g265" y="53142"/><use xlink:href="#g266" y="53159"/><use xlink:href="#g267" y="53176"/><use xlink:href="#g268" y="53193"/><use xlink:href="#g269" y="53210"/><use xlink:href="#g289" y="53227"/><use xlink:href="#g290" y="53244"/><use xlink:href="#g331" y="53261"/><use xlink:href="#g332" y="53278"/><rect x="136" y="53295" width="8" height="17" class="foreground"/><use xlink:href="#g365" y="53295"/><use xlink:href="#g28" y="53312"/><use xlink:href="#g28" y="53329"/><use xlink:href="#g16" y="53346"/><use xlink:href="#g16" y="53363"/><use xlink:href="#g16" y="53380"/><use xlink:href="#g16" y="53397"/><use xlink:href="#g16" y="53414"/><rect x="0" y="53431" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="53431"/></g><g><use xlink:href="#g263" y="53482"/><use xlink:href="#g264" y="53499"/><use xlink:href="#g265" y="53516"/><use xlink:href="#g266" y="53533"/><use xlink:href="#g267" y="53550"/><use xlink:href="#g268" y="53567"/><use xlink:href="#g269" y="53584"/><use xlink:href="#g289" y="53601"/><use xlink:href="#g290" y="53618"/><use xlink:href="#g331" y="53635"/><use xlink:href="#g332" y="53652"/><rect x="128" y="53669" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="53669"/><use xlink:href="#g28" y="53686"/><use xlink:href="#g28" y="53703"/><use xlink:href="#g16" y="53720"/><use xlink:href="#g16" y="53737"/><use xlink:href="#g16" y="53754"/><use xlink:href="#g16" y="53771"/><use xlink:href="#g16" y="53788"/><rect x="0" y="53805" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="53805"/></g><g><use xlink:href="#g263" y="53856"/><use xlink:href="#g264" y="53873"/><use xlink:href="#g265" y="53890"/><use xlink:href="#g266" y="53907"/><use xlink:href="#g267" y="53924"/><use xlink:href="#g268" y="53941"/><use xlink:href="#g269" y="53958"/><use xlink:href="#g289" y="53975"/><use xlink:href="#g290" y="53992"/><use xlink:href="#g331" y="54009"/><use xlink:href="#g332" y="54026"/><rect x="128" y="54043" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="54043"/><use xlink:href="#g28" y="54060"/><use xlink:href="#g28" y="54077"/><use xlink:href="#g16" y="54094"/><use xlink:href="#g16" y="54111"/><use xlink:href="#g16" y="54128"/><use xlink:href="#g16" y="54145"/><use xlink:href="#g16" y="54162"/><rect x="0" y="54179" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="54179"/></g><g><use xlink:href="#g263" y="54230"/><use xlink:href="#g264" y="54247"/><use xlink:href="#g265" y="54264"/><use xlink:href="#g266" y="54281"/><use xlink:href="#g267" y="54298"/><use xlink:href="#g268" y="54315"/><use xlink:href="#g269" y="54332"/><use xlink:href="#g289" y="54349"/><use xlink:href="#g290" y="54366"/><use xlink:href="#g331" y="54383"/><use xlink:href="#g332" y="54400"/><rect x="136" y="54417" width="8" height="17" class="foreground"/><use xlink:href="#g366" y="54417"/><rect x="136" y="54434" width="128" height="17" fill="#008787"/><rect x="264" y="54434" width="8" height="17" fill="#444444"/><use xlink:href="#g367" y="54434"/><rect x="136" y="54451" width="128" height="17" fill="#008787"/><rect x="264" y="54451" width="8" height="17" fill="#444444"/><use xlink:href="#g368" y="54451"/><rect x="136" y="54468" width="128" height="17" fill="#008787"/><rect x="264" y="54468" width="8" height="17" fill="#444444"/><use xlink:href="#g369" y="54468"/><rect x="136" y="54485" width="128" height="17" fill="#008787"/><rect x="264" y="54485" width="8" height="17" fill="#444444"/><use xlink:href="#g370" y="54485"/><rect x="136" y="54502" width="128" height="17" fill="#008787"/><rect x="264" y="54502" width="8" height="17" fill="#444444"/><use xlink:href="#g371" y="54502"/><rect x="136" y="54519" width="128" height="17" fill="#008787"/><rect x="264" y="54519" width="8" height="17" fill="#444444"/><use xlink:href="#g372" y="54519"/><rect x="136" y="54536" width="128" height="17" fill="#008787"/><rect x="264" y="54536" width="8" height="17" fill="#00afaf"/><use xlink:href="#g373" y="54536"/><rect x="0" y="54553" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="54553"/></g><g><use xlink:href="#g263" y="54604"/><use xlink:href="#g264" y="54621"/><use xlink:href="#g265" y="54638"/><use xlink:href="#g266" y="54655"/><use xlink:href="#g267" y="54672"/><use xlink:href="#g268" y="54689"/><use xlink:href="#g269" y="54706"/><use xlink:href="#g289" y="54723"/><use xlink:href="#g290" y="54740"/><use xlink:href="#g331" y="54757"/><use xlink:href="#g332" y="54774"/><rect x="144" y="54791" width="8" height="17" class="foreground"/><use xlink:href="#g374" y="54791"/><rect x="144" y="54808" width="128" height="17" fill="#008787"/><rect x="272" y="54808" width="8" height="17" fill="#444444"/><use xlink:href="#g375" y="54808"/><rect x="144" y="54825" width="128" height="17" fill="#008787"/><rect x="272" y="54825" width="8" height="17" fill="#444444"/><use xlink:href="#g376" y="54825"/><rect x="144" y="54842" width="128" height="17" fill="#008787"/><rect x="272" y="54842" width="8" height="17" fill="#444444"/><use xlink:href="#g377" y="54842"/><rect x="144" y="54859" width="128" height="17" fill="#008787"/><rect x="272" y="54859" width="8" height="17" fill="#444444"/><use xlink:href="#g378" y="54859"/><rect x="144" y="54876" width="128" height="17" fill="#008787"/><rect x="272" y="54876" width="8" height="17" fill="#444444"/><use xlink:href="#g379" y="54876"/><use xlink:href="#g16" y="54893"/><use xlink:href="#g16" y="54910"/><rect x="0" y="54927" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="54927"/></g><g><use xlink:href="#g263" y="54978"/><use xlink:href="#g264" y="54995"/><use xlink:href="#g265" y="55012"/><use xlink:href="#g266" y="55029"/><use xlink:href="#g267" y="55046"/><use xlink:href="#g268" y="55063"/><use xlink:href="#g269" y="55080"/><use xlink:href="#g289" y="55097"/><use xlink:href="#g290" y="55114"/><use xlink:href="#g331" y="55131"/><use xlink:href="#g332" y="55148"/><rect x="152" y="55165" width="8" height="17" class="foreground"/><use xlink:href="#g380" y="55165"/><rect x="152" y="55182" width="56" height="17" fill="#008787"/><rect x="208" y="55182" width="8" height="17" fill="#444444"/><use xlink:href="#g381" y="55182"/><use xlink:href="#g28" y="55199"/><use xlink:href="#g16" y="55216"/><use xlink:href="#g16" y="55233"/><use xlink:href="#g16" y="55250"/><use xlink:href="#g16" y="55267"/><use xlink:href="#g16" y="55284"/><rect x="0" y="55301" width="528" height="17" fill="#262626"/><use xlink:href="#g382" y="55301"/></g><g><use xlink:href="#g263" y="55352"/><use xlink:href="#g264" y="55369"/><use xlink:href="#g265" y="55386"/><use xlink:href="#g266" y="55403"/><use xlink:href="#g267" y="55420"/><use xlink:href="#g268" y="55437"/><use xlink:href="#g269" y="55454"/><use xlink:href="#g289" y="55471"/><use xlink:href="#g290" y="55488"/><use xlink:href="#g331" y="55505"/><use xlink:href="#g332" y="55522"/><rect x="160" y="55539" width="8" height="17" class="foreground"/><use xlink:href="#g383" y="55539"/><rect x="160" y="55556" width="136" height="17" fill="#008787"/><rect x="296" y="55556" width="8" height="17" fill="#444444"/><use xlink:href="#g315" y="55556"/><rect x="160" y="55573" width="136" height="17" fill="#008787"/><rect x="296" y="55573" width="8" height="17" fill="#444444"/><use xlink:href="#g316" y="55573"/><rect x="160" y="55590" width="136" height="17" fill="#008787"/><rect x="296" y="55590" width="8" height="17" fill="#00afaf"/><use xlink:href="#g343" y="55590"/><rect x="160" y="55607" width="136" height="17" fill="#008787"/><rect x="296" y="55607" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g344" y="55607"/><rect x="160" y="55624" width="136" height="17" fill="#008787"/><rect x="296" y="55624" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g319" y="55624"/><rect x="160" y="55641" width="136" height="17" fill="#008787"/><rect x="296" y="55641" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g320" y="55641"/><rect x="160" y="55658" width="136" height="17" fill="#008787"/><rect x="296" y="55658" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g321" y="55658"/><rect x="0" y="55675" width="528" height="17" fill="#262626"/><use xlink:href="#g382" y="55675"/></g><g><use xlink:href="#g263" y="55726"/><use xlink:href="#g264" y="55743"/><use xlink:href="#g265" y="55760"/><use xlink:href="#g266" y="55777"/><use xlink:href="#g267" y="55794"/><use xlink:href="#g268" y="55811"/><use xlink:href="#g269" y="55828"/><use xlink:href="#g289" y="55845"/><use xlink:href="#g290" y="55862"/><use xlink:href="#g331" y="55879"/><use xlink:href="#g332" y="55896"/><rect x="168" y="55913" width="8" height="17" class="foreground"/><use xlink:href="#g384" y="55913"/><rect x="168" y="55930" width="136" height="17" fill="#008787"/><rect x="304" y="55930" width="8" height="17" fill="#444444"/><use xlink:href="#g323" y="55930"/><rect x="168" y="55947" width="136" height="17" fill="#008787"/><rect x="304" y="55947" width="8" height="17" fill="#444444"/><use xlink:href="#g324" y="55947"/><rect x="168" y="55964" width="136" height="17" fill="#008787"/><rect x="304" y="55964" width="8" height="17" fill="#444444"/><use xlink:href="#g346" y="55964"/><rect x="168" y="55981" width="136" height="17" fill="#008787"/><rect x="304" y="55981" width="8" height="17" fill="#444444"/><use xlink:href="#g347" y="55981"/><rect x="168" y="55998" width="136" height="17" fill="#008787"/><rect x="304" y="55998" width="8" height="17" fill="#00afaf"/><use xlink:href="#g327" y="55998"/><rect x="168" y="56015" width="136" height="17" fill="#008787"/><rect x="304" y="56015" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g328" y="56015"/><rect x="168" y="56032" width="136" height="17" fill="#008787"/><rect x="304" y="56032" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g329" y="56032"/><rect x="0" y="56049" width="528" height="17" fill="#262626"/><use xlink:href="#g382" y="56049"/></g><g><use xlink:href="#g263" y="56100"/><use xlink:href="#g264" y="56117"/><use xlink:href="#g265" y="56134"/><use xlink:href="#g266" y="56151"/><use xlink:href="#g267" y="56168"/><use xlink:href="#g268" y="56185"/><use xlink:href="#g269" y="56202"/><use xlink:href="#g289" y="56219"/><use xlink:href="#g290" y="56236"/><use xlink:href="#g331" y="56253"/><use xlink:href="#g332" y="56270"/><rect x="176" y="56287" width="8" height="17" class="foreground"/><use xlink:href="#g385" y="56287"/><rect x="168" y="56304" width="136" height="17" fill="#ffffff"/><rect x="304" y="56304" width="8" height="17" fill="#444444"/><use xlink:href="#g386" y="56304"/><rect x="168" y="56321" width="136" height="17" fill="#008787"/><rect x="304" y="56321" width="8" height="17" fill="#444444"/><use xlink:href="#g324" y="56321"/><rect x="168" y="56338" width="136" height="17" fill="#008787"/><rect x="304" y="56338" width="8" height="17" fill="#444444"/><use xlink:href="#g346" y="56338"/><rect x="168" y="56355" width="136" height="17" fill="#008787"/><rect x="304" y="56355" width="8" height="17" fill="#444444"/><use xlink:href="#g347" y="56355"/><rect x="168" y="56372" width="136" height="17" fill="#008787"/><rect x="304" y="56372" width="8" height="17" fill="#00afaf"/><use xlink:href="#g327" y="56372"/><rect x="168" y="56389" width="136" height="17" fill="#008787"/><rect x="304" y="56389" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g328" y="56389"/><rect x="168" y="56406" width="136" height="17" fill="#008787"/><rect x="304" y="56406" width="8" height="17" fill="#a8a8a8"/><use xlink:href="#g329" y="56406"/><rect x="0" y="56423" width="528" height="17" fill="#262626"/><use xlink:href="#g382" y="56423"/></g><g><use xlink:href="#g265" y="56474"/><use xlink:href="#g266" y="56491"/><use xlink:href="#g267" y="56508"/><use xlink:href="#g268" y="56525"/><use xlink:href="#g269" y="56542"/><use xlink:href="#g289" y="56559"/><use xlink:href="#g290" y="56576"/><use xlink:href="#g331" y="56593"/><use xlink:href="#g332" y="56610"/><use xlink:href="#g387" y="56627"/><use xlink:href="#g388" y="56644"/><rect x="128" y="56661" width="8" height="17" class="foreground"/><use xlink:href="#g15" y="56661"/><use xlink:href="#g16" y="56678"/><use xlink:href="#g16" y="56695"/><use xlink:href="#g16" y="56712"/><use xlink:href="#g16" y="56729"/><use xlink:href="#g16" y="56746"/><use xlink:href="#g16" y="56763"/><use xlink:href="#g16" y="56780"/><rect x="0" y="56797" width="528" height="17" fill="#262626"/><use xlink:href="#g17" y="56797"/></g><g><use xlink:href="#g265" y="56848"/><use xlink:href="#g266" y="56865"/><use xlink:href="#g267" y="56882"/><use xlink:href="#g268" y="56899"/><use xlink:href="#g269" y="56916"/><use xlink:href="#g289" y="56933"/><use xlink:href="#g290" y="56950"/><use xlink:href="#g331" y="56967"/><use xlink:href="#g332" y="56984"/><use xlink:href="#g387" y="57001"/><use xlink:href="#g388" y="57018"/><use xlink:href="#g389" y="57035"/><use xlink:href="#g390" y="57052"/><rect x="0" y="57069" width="8" height="17" class="foreground"/><use xlink:href="#g1" y="57069"/><use xlink:href="#g16" y="57086"/><use xlink:href="#g16" y="57103"/><use xlink:href="#g16" y="57120"/><use xlink:href="#g16" y="57137"/><use xlink:href="#g16" y="57154"/><use xlink:href="#g16" y="57171"/></g><g><use xlink:href="#g265" y="57222"/><use xlink:href="#g266" y="57239"/><use xlink:href="#g267" y="57256"/><use xlink:href="#g268" y="57273"/><use xlink:href="#g269" y="57290"/><use xlink:href="#g289" y="57307"/><use xlink:href="#g290" y="57324"/><use xlink:href="#g331" y="57341"/><use xlink:href="#g332" y="57358"/><use xlink:href="#g387" y="57375"/><use xlink:href="#g388" y="57392"/><use xlink:href="#g389" y="57409"/><use xlink:href="#g390" y="57426"/><rect x="16" y="57443" width="8" height="17" class="foreground"/><use xlink:href="#g2" y="57443"/><use xlink:href="#g16" y="57460"/><use xlink:href="#g16" y="57477"/><use xlink:href="#g16" y="57494"/><use xlink:href="#g16" y="57511"/><use xlink:href="#g16" y="57528"/><use xlink:href="#g16" y="57545"/></g><g><use xlink:href="#g265" y="57596"/><use xlink:href="#g266" y="57613"/><use xlink:href="#g267" y="57630"/><use xlink:href="#g268" y="57647"/><use xlink:href="#g269" y="57664"/><use xlink:href="#g289" y="57681"/><use xlink:href="#g290" y="57698"/><use xlink:href="#g331" y="57715"/><use xlink:href="#g332" y="57732"/><use xlink:href="#g387" y="57749"/><use xlink:href="#g388" y="57766"/><use xlink:href="#g389" y="57783"/><use xlink:href="#g390" y="57800"/><use xlink:href="#g391" y="57817"/><rect x="0" y="57834" width="8" height="17" class="foreground"/><use xlink:href="#g1" y="57834"/><use xlink:href="#g16" y="57851"/><use xlink:href="#g16" y="57868"/><use xlink:href="#g16" y="57885"/><use xlink:href="#g16" y="57902"/><use xlink:href="#g16" y="57919"/></g></g></svg> +</svg>
\ No newline at end of file diff --git a/docs/assets/logo.png b/docs/assets/logo.png Binary files differnew file mode 100644 index 0000000..54ced97 --- /dev/null +++ b/docs/assets/logo.png diff --git a/docs/assets/render.md b/docs/assets/render.md new file mode 100644 index 0000000..9dce624 --- /dev/null +++ b/docs/assets/render.md @@ -0,0 +1,7 @@ +Render using [termtosvg](https://github.com/nbedos/termtosvg/blob/develop/man/termtosvg.md): + +``` +termtosvg render demo.cast demo1.svg -D 2 -m40 -M300 -t progress_bar +``` + +size: 66x20 diff --git a/docs/cloudshell/run-in-docker.txt b/docs/cloudshell/run-in-docker.txt new file mode 100644 index 0000000..3e0cc43 --- /dev/null +++ b/docs/cloudshell/run-in-docker.txt @@ -0,0 +1,3 @@ +Try redis in docker(which contains a redis-server): + + docker build -t iredis . && docker run -it iredis diff --git a/docs/update-redis-doc.md b/docs/update-redis-doc.md new file mode 100644 index 0000000..2be45c0 --- /dev/null +++ b/docs/update-redis-doc.md @@ -0,0 +1,9 @@ +# How to Catch Up with Latest Redis-doc + +1. `git pull` in submodule. +2. Overwrite `iredis/data/commands.json`. +3. Diff with old `commands.json`, make the changes. +4. `mv redis-doc/commands/*.md iredis/data/commands` +5. `prettier --write --prose-wrap always iredis/data/commands/*.md` + +Done! diff --git a/iredis/__init__.py b/iredis/__init__.py new file mode 100644 index 0000000..38cf6db --- /dev/null +++ b/iredis/__init__.py @@ -0,0 +1 @@ +__version__ = "1.9.1" diff --git a/iredis/bottom.py b/iredis/bottom.py new file mode 100644 index 0000000..28746f8 --- /dev/null +++ b/iredis/bottom.py @@ -0,0 +1,35 @@ +import logging +from .commands import commands_summary +from .utils import command_syntax + +BUTTOM_TEXT = "Ctrl-D to exit;" +logger = logging.getLogger(__name__) + + +class BottomToolbar: + CHAR = "⣾⣷⣯⣟⡿⢿⣻⣽" + + def __init__(self, command_holder): + self.index = 0 + # BottomToolbar can only read this variable + self.command_holder = command_holder + + def get_animation_char(self): + animation = self.CHAR[self.index] + + self.index += 1 + if self.index == len(self.CHAR): + self.index = 0 + return animation + + def render(self): + text = BUTTOM_TEXT + # add command help if valide + if self.command_holder.command: + try: + command_info = commands_summary[self.command_holder.command] + text = command_syntax(self.command_holder.command, command_info) + except KeyError as e: + logger.exception(e) + pass + return text diff --git a/iredis/client.py b/iredis/client.py new file mode 100644 index 0000000..61613dc --- /dev/null +++ b/iredis/client.py @@ -0,0 +1,685 @@ +""" +IRedis client. +""" +import re +import os +import sys +import logging +from subprocess import run +from importlib_resources import read_text +from distutils.version import StrictVersion + +import redis +from prompt_toolkit.shortcuts import clear +from prompt_toolkit.formatted_text import FormattedText +from redis.connection import Connection, SSLConnection, UnixDomainSocketConnection +from redis.exceptions import ( + AuthenticationError, + ConnectionError, + TimeoutError, + ResponseError, +) + + +from . import markdown, renders +from .data import commands as commands_data +from .commands import ( + command2callback, + commands_summary, + command2syntax, + groups, + split_command_args, + split_unknown_args, +) +from .completers import IRedisCompleter +from .config import config +from .exceptions import NotRedisCommand, InvalidArguments, AmbiguousCommand, NotSupport +from .renders import OutputRender +from .utils import ( + compose_command_syntax, + nativestr, + exit, + convert_formatted_text_to_bytes, + parse_url, +) +from .warning import confirm_dangerous_command + +logger = logging.getLogger(__name__) +CLIENT_COMMANDS = groups["iredis"] + + +class Client: + """ + iRedis client, hold a redis-py Client to interact with Redis. + """ + + def __init__( + self, + host=None, + port=None, + db=0, + password=None, + path=None, + scheme="redis", + username=None, + ): + self.host = host + self.port = port + self.db = db + self.path = path + # FIXME username is not using... + self.username = username + self.scheme = scheme + + self.connection = self.create_connection( + host, + port, + db, + password, + path, + scheme, + username, + ) + + # all command upper case + self.answer_callbacks = command2callback + self.set_default_pager(config) + try: + self.connection.connect() + except Exception as e: + print(str(e), file=sys.stderr) + sys.exit(1) + if not config.no_info: + try: + self.get_server_info() + except Exception as e: + logger.warning(f"[After Connection] {str(e)}") + config.no_version_reason = str(e) + else: + config.no_version_reason = "--no-info flag activated" + + if config.version and re.match(r"([\d\.]+)", config.version): + self.auth_compat(config.version) + + def create_connection( + self, + host=None, + port=None, + db=0, + password=None, + path=None, + scheme="redis", + username=None, + ): + if scheme in ("redis", "rediss"): + connection_kwargs = { + "host": host, + "port": port, + "db": db, + "password": password, + "socket_keepalive": config.socket_keepalive, + } + if scheme == "rediss": + connection_class = SSLConnection + else: + connection_class = Connection + else: + connection_kwargs = {"db": db, "password": password, "path": path} + connection_class = UnixDomainSocketConnection + + if config.decode: + connection_kwargs["encoding"] = config.decode + connection_kwargs["decode_responses"] = True + connection_kwargs["encoding_errors"] = "replace" + + logger.debug( + f"connection_class={connection_class}, connection_kwargs={connection_kwargs}" + ) + + return connection_class(**connection_kwargs) + + def auth_compat(self, redis_version: str): + with_username = StrictVersion(redis_version) >= StrictVersion("6.0.0") + if with_username: + command2syntax["AUTH"] = "command_usernamex_password" + + def set_default_pager(self, config): + configured_pager = config.pager + os_environ_pager = os.environ.get("PAGER") + + if configured_pager: + logger.info('Default pager found in config file: "%s"', configured_pager) + os.environ["PAGER"] = configured_pager + elif os_environ_pager: + logger.info( + 'Default pager found in PAGER environment variable: "%s"', + os_environ_pager, + ) + os.environ["PAGER"] = os_environ_pager + else: + logger.info("No default pager found in environment. Using os default pager") + + # Set default set of less recommended options, if they are not already set. + # They are ignored if pager is different than less. + if not os.environ.get("LESS"): + os.environ["LESS"] = "-SRXF" + + def get_server_info(self): + # safe to decode Redis's INFO response + info_resp = nativestr(self.execute("INFO")) + version = re.findall(r"^redis_version:([\d\.]+)\r\n", info_resp, re.MULTILINE)[ + 0 + ] + logger.debug(f"[Redis Version] {version}") + config.version = version + + def __str__(self): + if self.scheme == "unix": + prompt = f"redis {self.path}" + else: + prompt = f"{self.host}:{self.port}" + + if self.db: + prompt = f"{prompt}[{self.db}]" + return prompt + + def client_execute_command(self, command_name, *args): + command = command_name.upper() + if command == "HELP": + yield self.do_help(*args) + if command == "PEEK": + yield from self.do_peek(*args) + if command == "CLEAR": + clear() + if command == "EXIT": + exit() + + def execute(self, *args, **kwargs): + logger.info( + f"execute: connection={self.connection} args={args}, kwargs={kwargs}" + ) + return self.execute_by_connection(self.connection, *args, **kwargs) + + def execute_by_connection(self, connection, command_name, *args, **options): + """Execute a command and return a parsed response + Here we retry once for ConnectionError. + """ + logger.info( + f"execute by connection: connection={connection}, name={command_name}, {args}, {options}" + ) + retry_times = config.retry_times # FIXME configureable + last_error = None + need_refresh_connection = False + + while retry_times >= 0: + try: + if need_refresh_connection: + print( + f"{str(last_error)} retrying... retry left: {retry_times+1}", + file=sys.stderr, + ) + connection.disconnect() + connection.connect() + logger.info(f"New connection created, retry on {connection}.") + logger.info(f"send_command: {command_name} , {args}") + connection.send_command(command_name, *args) + response = connection.read_response() + except AuthenticationError: + raise + except (ConnectionError, TimeoutError) as e: + logger.warning(f"Connection Error, got {e}, retrying...") + last_error = e + retry_times -= 1 + need_refresh_connection = True + except (ResponseError) as e: + response_message = str(e) + if response_message.startswith("MOVED"): + return self.reissue_with_redirect( + response_message, command_name, *args, **options + ) + raise e + + except redis.exceptions.ExecAbortError: + config.transaction = False + raise + else: + return response + raise last_error + + def reissue_with_redirect(self, response, *args, **kwargs): + """ + For redis cluster, when server response a "MOVE ..." response, we auto- + redirect to the target node, reissue the original command. + + This feature is not supported for unix socket connection. + """ + # Redis Cluster only supports database zero. + _, slot, ip_port = response.split(" ") + ip, port = ip_port.split(":") + port = int(port) + + print(response, file=sys.stderr) + + connection = self.create_connection(ip, port) + # if user sets dsn for dest node + # use username and password from dsn settings + if config.alias_dsn: + for dsn_name, dsn_url in config.alias_dsn.items(): + dsn = parse_url(dsn_url) + if dsn.host == ip and dsn.port == port: + print( + f"Connect {ip}:{port} via dns settings of {dsn_name}", + file=sys.stderr, + ) + connection = self.create_connection( + dsn.host, + dsn.port, + dsn.db, + dsn.password, + dsn.path, + dsn.scheme, + dsn.username, + ) + break + + connection.connect() + return self.execute_by_connection(connection, *args, **kwargs) + + def render_response(self, response, command_name): + "Parses a response from the Redis server" + logger.info(f"[Redis-Server] Response: {response}") + if config.raw: + callback = OutputRender.render_raw + # if in transaction, use queue render first + elif config.transaction: + callback = renders.OutputRender.render_transaction_queue + else: + callback = OutputRender.get_render(command_name=command_name) + rendered = callback(response) + logger.info(f"[render result] {rendered}") + return rendered + + def monitor(self): + """Redis' MONITOR command: + https://redis.io/commands/monitor + This command need to read from a stream resp, so + it's different + """ + while 1: + response = self.connection.read_response() + if config.raw: + yield OutputRender.render_raw(response) + else: + yield OutputRender.render_bulk_string_decode(response) + + def subscribing(self): + while 1: + response = self.connection.read_response() + if config.raw: + yield OutputRender.render_raw(response) + else: + yield OutputRender.render_subscribe(response) + + def unsubscribing(self): + "unsubscribe from all channels" + response = self.execute("UNSUBSCRIBE") + if config.raw: + yield OutputRender.render_raw(response) + else: + yield OutputRender.render_subscribe(response) + + def split_command_and_pipeline(self, rawinput, completer: IRedisCompleter): + """ + split user raw input to redis command and shell pipeline. + eg: + GET json | jq .key + return: GET json, jq . key + """ + grammar = completer.get_completer(input_text=rawinput).compiled_grammar + matched = grammar.match(rawinput) + if not matched: + # invalide command! + return rawinput, None + variables = matched.variables() + shell_command = variables.get("shellcommand") + if shell_command: + redis_command = rawinput.replace(shell_command, "") + shell_command = shell_command.lstrip("| ") + return redis_command, shell_command + return rawinput, None + + def send_command(self, raw_command, completer=None): # noqa + """ + Send raw_command to redis-server, return parsed response. + + :param raw_command: text raw_command, not parsed + :param completer: RedisGrammarCompleter will update completer + based on redis response. eg: update key completer after ``keys`` + raw_command + """ + if completer is None: # not in a tty + redis_command, shell_command = raw_command, None + else: + redis_command, shell_command = self.split_command_and_pipeline( + raw_command, completer + ) + logger.info(f"[Prepare command] Redis: {redis_command}, Shell: {shell_command}") + try: + try: + command_name, args = split_command_args(redis_command) + except (InvalidArguments, AmbiguousCommand): + logger.warn( + "This is not a iredis known command, send to redis-server anyway..." + ) + command_name, args = split_unknown_args(redis_command) + + logger.info(f"[Split command] command: {command_name}, args: {args}") + input_command_upper = command_name.upper() + # Confirm for dangerous command + if config.warning: + confirm = confirm_dangerous_command(input_command_upper) + if confirm is True: + print("Your Call!!", file=sys.stderr) + elif confirm is False: + print("Canceled!", file=sys.stderr) + return + # None: continue... + + self.pre_hook(raw_command, command_name, args, completer) + # if raw_command is not supposed to send to server + if input_command_upper in CLIENT_COMMANDS: + logger.info(f"{input_command_upper} is an iredis command.") + yield from self.client_execute_command(command_name, *args) + return + + redis_resp = self.execute(command_name, *args) + # if shell_command and enable shell, do not render, just run in shell pipe and show the + # subcommand's stdout/stderr + if shell_command and config.shell: + # pass the raw response of redis to shell command + if isinstance(redis_resp, list): + # FIXME not handling nested list, use renders.render_raw + # instead + stdin = b"\n".join(redis_resp) + else: + stdin = redis_resp + run(shell_command, input=stdin, shell=True) + return + + self.after_hook(raw_command, command_name, args, completer, redis_resp) + yield self.render_response(redis_resp, command_name) + + # FIXME generator response do not support pipeline + if input_command_upper == "MONITOR": + # TODO special render for monitor + try: + yield from self.monitor() + except KeyboardInterrupt: + pass + elif input_command_upper in [ + "SUBSCRIBE", + "PSUBSCRIBE", + ]: # enter subscribe mode + try: + yield from self.subscribing() + except KeyboardInterrupt: + yield from self.unsubscribing() + except Exception as e: + logger.exception(e) + if config.raw: + render_callback = OutputRender.render_raw + else: + render_callback = OutputRender.render_error + yield render_callback(f"ERROR {str(e)}".encode()) + finally: + config.withscores = False + + def after_hook(self, command, command_name, args, completer, response): + # === After hook === + # SELECT db on AUTH + if command_name.upper() == "AUTH": + if self.db: + select_result = self.execute("SELECT", self.db) + if nativestr(select_result) != "OK": + raise ConnectionError("Invalid Database") + # When the connection is TimeoutError or ConnectionError, reconnect the connection will use it + self.connection.password = args[0] + elif command_name.upper() == "SELECT": + logger.debug("[After hook] Command is SELECT, change self.db.") + self.db = int(args[0]) + # When the connection is TimeoutError or ConnectionError, reconnect the connection will use it + self.connection.db = self.db + elif command_name.upper() == "MULTI": + logger.debug("[After hook] Command is MULTI, start transaction.") + config.transaction = True + + if completer: + completer.update_completer_for_response(command_name, args, response) + + def pre_hook(self, command, command_name, args, completer: IRedisCompleter): + """ + Before execute command, patch completers first. + Eg: When user run `GET foo`, key completer need to + touch foo. + + Only works when compile-grammar thread is done. + """ + if command_name.upper() == "HELLO": + raise NotSupport("IRedis currently not support RESP3, sorry about that.") + # TRANSATION state chage + if command_name.upper() in ["EXEC", "DISCARD"]: + logger.debug(f"[After hook] Command is {command_name}, unset transaction.") + config.transaction = False + # score display for sorted set + if command_name.upper() in ["ZSCAN", "ZPOPMAX", "ZPOPMIN"]: + config.withscores = True + + # not a tty + if not completer: + logger.warning( + "[Pre patch completer] Complter is None, not a tty, " + "not patch completers, not set withscores" + ) + return + completer.update_completer_for_input(command) + + redis_grammar = completer.get_completer(command).compiled_grammar + m = redis_grammar.match(command) + if not m: + # invalide command! + return + variables = m.variables() + # zset withscores + withscores = variables.get("withscores") + if withscores: + config.withscores = True + + def do_help(self, *args): + command_docs_name = "-".join(args).lower() + command_summary_name = " ".join(args).upper() + try: + doc = read_text(commands_data, f"{command_docs_name}.md") + except FileNotFoundError: + raise NotRedisCommand( + f"{command_summary_name} is not a valide Redis command." + ) + rendered_detail = markdown.render(doc) + summary_dict = commands_summary[command_summary_name] + + avaiable_version = summary_dict.get("since", "?") + server_version = config.version + # FIXME anything strange with single quotes? + logger.debug(f"[--version--] '{server_version}'") + try: + is_avaiable = StrictVersion(server_version) > StrictVersion( + avaiable_version + ) + except Exception as e: + logger.exception(e) + is_avaiable = None + + if is_avaiable: + avaiable_text = f"(Avaiable on your redis-server: {server_version})" + elif is_avaiable is False: + avaiable_text = f"(Not avaiable on your redis-server: {server_version})" + else: + avaiable_text = "" + since_text = f"{avaiable_version} {avaiable_text}" + + summary = [ + ("", "\n"), + ("class:doccommand", " " + command_summary_name), + ("", "\n"), + ("class:dockey", " summary: "), + ("", summary_dict.get("summary", "No summary")), + ("", "\n"), + ("class:dockey", " complexity: "), + ("", summary_dict.get("complexity", "?")), + ("", "\n"), + ("class:dockey", " since: "), + ("", since_text), + ("", "\n"), + ("class:dockey", " group: "), + ("", summary_dict.get("group", "?")), + ("", "\n"), + ("class:dockey", " syntax: "), + ("", command_summary_name), # command + *compose_command_syntax(summary_dict, style_class=""), # command args + ("", "\n\n"), + ] + + to_render = FormattedText(summary + rendered_detail) + if config.raw: + return convert_formatted_text_to_bytes(to_render) + return to_render + + def do_peek(self, key): + """ + PEEK command implementation. + + It's a generator, will run different redis commands based on the key's + type, yields FormattedText once a command reached result. + + Redis current supported types: + string, list, set, zset, hash and stream. + """ + + def _string(key): + strlen = self.execute("strlen", key) + yield FormattedText([("class:dockey", "strlen: "), ("", str(strlen))]) + + value = self.execute("GET", key) + yield FormattedText( + [ + ("class:dockey", "value: "), + ("", renders.OutputRender.render_bulk_string(value)), + ] + ) + + def _list(key): + llen = self.execute("llen", key) + yield FormattedText([("class:dockey", "llen: "), ("", str(llen))]) + if llen <= 20: + contents = self.execute(f"LRANGE {key} 0 -1") + else: + first_10 = self.execute(f"LRANGE {key} 0 9") + last_10 = self.execute(f"LRANGE {key} -10 -1") + contents = first_10 + [f"{llen-20} elements was omitted ..."] + last_10 + yield FormattedText([("class:dockey", "elements: ")]) + yield renders.OutputRender.render_list(contents) + + def _set(key): + cardinality = self.execute("scard", key) + yield FormattedText( + [("class:dockey", "cardinality: "), ("", str(cardinality))] + ) + if cardinality <= 20: + contents = self.execute("smembers", key) + yield FormattedText([("class:dockey", "members: ")]) + yield renders.OutputRender.render_list(contents) + else: + _, contents = self.execute(f"sscan {key} 0 count 20") + first_n = len(contents) + yield FormattedText([("class:dockey", f"members (first {first_n}): ")]) + yield renders.OutputRender.render_members(contents) + # TODO update completers + + def _zset(key): + count = self.execute(f"zcount {key} -inf +inf") + yield FormattedText([("class:dockey", "zcount: "), ("", str(count))]) + if count <= 20: + contents = self.execute(f"zrange {key} 0 -1 withscores") + yield FormattedText([("class:dockey", "members: ")]) + yield renders.OutputRender.render_members(contents) + else: + _, contents = self.execute(f"zscan {key} 0 count 20") + first_n = len(contents) // 2 + yield FormattedText([("class:dockey", f"members (first {first_n}): ")]) + config.withscores = True + output = renders.OutputRender.render_members(contents) + config.withscores = False + yield output + + def _hash(key): + hlen = self.execute(f"hlen {key}") + yield FormattedText([("class:dockey", "hlen: "), ("", str(hlen))]) + if hlen <= 20: + contents = self.execute(f"hgetall {key}") + yield FormattedText([("class:dockey", "fields: ")]) + else: + _, contents = self.execute(f"hscan {key} 0 count 20") + first_n = len(contents) // 2 + yield FormattedText([("class:dockey", f"fields (first {first_n}): ")]) + yield renders.OutputRender.render_hash_pairs(contents) + + def _stream(key): + xinfo = self.execute("xinfo stream", key) + yield FormattedText([("class:dockey", "XINFO: ")]) + yield renders.OutputRender.render_list(xinfo) + + # incase the result is too long, we yield only once so the outputer + # can pager it. + peek_response = [] + key_type = nativestr(self.execute("type", key)) + if key_type == "none": + yield f"{key} doesn't exist." + return + + encoding = nativestr(self.execute("object encoding", key)) + + # use `memory usage` to get memory, this command available from redis4.0 + mem = "" + if config.version and StrictVersion(config.version) >= StrictVersion("4.0.0"): + memory_usage_value = str(self.execute("memory usage", key)) + mem = f" mem: {memory_usage_value} bytes" + + ttl = str(self.execute("ttl", key)) + + key_info = f"{key_type} ({encoding}){mem}, ttl: {ttl}" + + # FIXME raw write_result parse FormattedText + peek_response.append(FormattedText([("class:dockey", "key: "), ("", key_info)])) + + detail_action_fun = { + "string": _string, + "list": _list, + "set": _set, + "zset": _zset, + "hash": _hash, + "stream": _stream, + }[key_type] + detail = list(detail_action_fun(key)) + peek_response.extend(detail) + + # merge them into only one FormattedText + flat_formatted_text_pair = [] + for index, formatted_text in enumerate(peek_response): + for ft in formatted_text: + flat_formatted_text_pair.append(ft) + if index < len(peek_response) - 1: + flat_formatted_text_pair.append(renders.NEWLINE_TUPLE) + + if config.raw: + yield convert_formatted_text_to_bytes(flat_formatted_text_pair) + return + yield FormattedText(flat_formatted_text_pair) diff --git a/iredis/commands.py b/iredis/commands.py new file mode 100644 index 0000000..358c8b7 --- /dev/null +++ b/iredis/commands.py @@ -0,0 +1,151 @@ +import re +import csv +import json +import logging +import functools +from importlib_resources import read_text, open_text + +from .utils import timer, strip_quote_args +from .exceptions import InvalidArguments, AmbiguousCommand +from . import data as project_data + + +logger = logging.getLogger(__name__) + + +def _load_command_summary(): + commands_summary = json.loads(read_text(project_data, "commands.json")) + return commands_summary + + +def _load_command(): + """ + load command informations from file. + :returns: + - original_commans: dict, command name : Command + - command_group: dict, group_name: command_names + """ + first_line = True + command2callback = {} + command2syntax = {} + groups = {} + with open_text(project_data, "command_syntax.csv") as command_syntax: + csvreader = csv.reader(command_syntax) + for line in csvreader: + if first_line: + first_line = False + continue + group, command, syntax, func_name = line + command2callback[command] = func_name + command2syntax[command] = syntax + groups.setdefault(group, []).append(command) + + return command2callback, command2syntax, groups + + +def _load_dangerous(): + """ + Load dangerous commands from csv file. + """ + first_line = True + dangerous_command = {} + with open_text(project_data, "dangerous_commands.csv") as dangerous_file: + csvreader = csv.reader(dangerous_file) + for line in csvreader: + if first_line: + first_line = False + continue + command, reason = line + dangerous_command[command] = reason + return dangerous_command + + +timer("[Loader] Start loading commands file...") +command2callback, command2syntax, groups = _load_command() +# all redis command strings, in UPPER case +# NOTE: Must sort by length, to match longest command first +all_commands = sorted( + list(command2callback.keys()) + ["HELP"], key=lambda x: len(x), reverse=True +) +# load commands information from redis-doc/commands.json +commands_summary = _load_command_summary() +# add iredis' commands' summary +commands_summary.update( + { + "HELP": { + "summary": "Show documents for a Redis command.", + "complexity": "O(1).", + "arguments": [{"name": "command", "type": "string"}], + "since": "1.0", + "group": "iredis", + }, + "CLEAR": { + "summary": "Clear the screen like bash clear.", + "complexity": "O(1).", + "since": "1.0", + "group": "iredis", + }, + "EXIT": { + "summary": "Exit iredis.", + "complexity": "O(1).", + "since": "1.0", + "group": "iredis", + }, + "PEEK": { + "summary": "Get the key's type and value.", + "arguments": [{"name": "key", "type": "key"}], + "since": "1.0", + "complexity": "O(1).", + "since": "1.0", + "group": "iredis", + }, + } +) +timer("[Loader] Finished loading commands.") +dangerous_commands = _load_dangerous() + + +@functools.lru_cache(maxsize=2048) +def split_command_args(command): + """ + Split Redis command text into command and args. + + :param command: redis command string, with args + """ + global all_commands + + command = command.strip() + for command_name in all_commands: + # for command that is paritaly inputed, like `command in`, we should + # match with `command info`, otherwise, `command in` will result in + # `command` with `args` is ('in') which is an invalid case. + normalized_input_command = " ".join(command.split()).upper() + if ( + re.search("\s", command) + and command_name.startswith(normalized_input_command) + and command_name != normalized_input_command + ): + raise AmbiguousCommand("command is not finished") + # allow multiplt space in user inputed command + command_allow_multi_spaces = "[ ]+".join(command_name.split()) + matcher = re.match(fr"({command_allow_multi_spaces})( |$)", command.upper()) + if matcher: + matched_command_len = len(matcher.group(1)) + input_command = command[:matched_command_len] + input_args = command[matcher.end() :] + break + else: + raise InvalidArguments(f"`{command}` is not a valide Redis Command") + + args = list(strip_quote_args(input_args)) + + return input_command, args + + +def split_unknown_args(command): + """ + Split user's input into command and args. + """ + command = command.strip() + input_command, *input_args = command.split(" ") + return input_command, list(strip_quote_args(" ".join(input_args))) diff --git a/iredis/completers.py b/iredis/completers.py new file mode 100644 index 0000000..6756637 --- /dev/null +++ b/iredis/completers.py @@ -0,0 +1,359 @@ +import logging +from typing import Iterable + +import pendulum +from prompt_toolkit.completion import ( + CompleteEvent, + Completer, + Completion, + FuzzyWordCompleter, + WordCompleter, +) +from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter +from prompt_toolkit.document import Document + +from .commands import split_command_args, commands_summary, all_commands +from .config import config +from .exceptions import InvalidArguments, AmbiguousCommand +from .redis_grammar import CONST, command_grammar, get_command_grammar +from .utils import strip_quote_args, ensure_str + +logger = logging.getLogger(__name__) + + +class MostRecentlyUsedFirstWordMixin: + """ + A Mixin for WordCompleter, with a `touch()` method can make latest used + word appears first. And evict old completion word when `max_words` reached. + + Not thread safe. + """ + + def __init__(self, max_words, words, *args, **kwargs): + self.words = words + self.max_words = max_words + super().__init__(words, *args, **kwargs) + + def touch(self, word): + """ + Make sure word is in the first place of the completer + list. + """ + if word in self.words: + self.words.remove(word) + else: # not in words + if len(self.words) == self.max_words: # full + self.words.pop() + self.words.insert(0, word) + + def touch_words(self, words): + for word in words: + self.touch(word) + + +class MostRecentlyUsedFirstWordCompleter( + MostRecentlyUsedFirstWordMixin, FuzzyWordCompleter +): + pass + + +class IntegerTypeCompleter(MostRecentlyUsedFirstWordMixin, WordCompleter): + def __init__(self): + words = [] + for i in range(1, 64): + words.append(f"i{i}") # signed integer, 64 bit max + words.append(f"u{i}") # unsigned integer, 63 bit max + words.append("i64") + super().__init__(len(words), list(reversed(words))) + + +class TimestampCompleter(Completer): + """ + Completer for timestamp based on input. + + Features: + * Auto complete humanize time, like 3 -> 3 minutes ago, 3 hours ago. + * Auto guess datetime, complete by its timestamp. 2020-01-01 12:00 + -> 1577851200. + + The timezone is read from system. + """ + + when_lower_than = { + "year": 20, + "month": 12, + "day": 31, + "hour": 100, + "minute": 1000, + "second": 1000_000, + } + + def _completion_humanize_time(self, document: Document) -> Iterable[Completion]: + text = document.text + if not text.isnumeric(): + return + current = int(text) + now = pendulum.now() + for unit, minium in self.when_lower_than.items(): + if current <= minium: + dt = now.subtract(**{f"{unit}s": current}) + meta = f"{text} {unit}{'s' if current > 1 else ''} ago ({dt.format('YYYY-MM-DD HH:mm:ss')})" + yield Completion( + str(dt.int_timestamp * 1000), + start_position=-len(document.text_before_cursor), + display_meta=meta, + ) + + def _completion_formatted_time(self, document: Document) -> Iterable[Completion]: + text = document.text + try: + dt = pendulum.parse(text) + except Exception: + return + yield Completion( + str(dt.int_timestamp * 1000), + start_position=-len(document.text_before_cursor), + display_meta=str(dt), + ) + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + completions = list(self._completion_humanize_time(document)) + list( + self._completion_formatted_time(document) + ) + + # here we yield bigger timestamp first. + for completion in sorted(completions, key=lambda a: a.text): + yield completion + + +class IRedisCompleter(Completer): + """ + Completer class that can dynamically returns any Completer. + + :param get_completer: Callable that returns a :class:`.Completer` instance. + """ + + def __init__(self, hint=False, completion_casing="upper"): + super().__init__() + self.completer_mapping = self.get_completer_mapping(hint, completion_casing) + self.current_completer = self.root_completer = GrammarCompleter( + command_grammar, self.completer_mapping + ) + + @property + def key_completer(self) -> MostRecentlyUsedFirstWordCompleter: + return self.completer_mapping["key"] + + @property + def member_completer(self) -> MostRecentlyUsedFirstWordCompleter: + return self.completer_mapping["member"] + + @property + def field_completer(self) -> MostRecentlyUsedFirstWordCompleter: + return self.completer_mapping["field"] + + @property + def group_completer(self) -> MostRecentlyUsedFirstWordCompleter: + return self.completer_mapping["group"] + + @property + def catetoryname_completer(self) -> MostRecentlyUsedFirstWordCompleter: + return self.completer_mapping["categoryname"] + + @property + def username_completer(self) -> MostRecentlyUsedFirstWordCompleter: + return self.completer_mapping["username"] + + def get_completer(self, input_text): + try: + command, _ = split_command_args(input_text) + # here will compile grammar for this command + grammar = get_command_grammar(command) + completer = GrammarCompleter( + compiled_grammar=grammar, completers=self.completer_mapping + ) + except (InvalidArguments, AmbiguousCommand): + completer = self.root_completer + + return completer + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + input_text = document.text + self.current_completer = self.get_completer(input_text) + return self.current_completer.get_completions(document, complete_event) + + def update_completer_for_input(self, command): + completer = self.get_completer(command) + grammar = completer.compiled_grammar + m = grammar.match(command) + if not m: + # invalide command! + return + variables = m.variables() + + # auto update completion words, if it's LRU strategy. + for _token, _completer in self.completer_mapping.items(): + if not isinstance(_completer, MostRecentlyUsedFirstWordMixin): + continue + + # getall always returns a [] + tokens_in_command = variables.getall(_token) + for _token_in_command in tokens_in_command: + # prompt_toolkit didn't support multi tokens + # like DEL key1 key2 key3 + # so we have to split them manualy + for single_token in strip_quote_args(_token_in_command): + _completer.touch(single_token) + + def update_completer_for_response(self, command_name, args, response): + command_name = " ".join(command_name.split()).upper() + logger.info( + f"Try update completer using response... command_name is {command_name}" + ) + if response is None: + return + + response = ensure_str(response) + if command_name in ("HKEYS",): + self.field_completer.touch_words(response) + logger.debug(f"[Completer] field completer updated with {response}.") + + if command_name in ("HGETALL",): + self.field_completer.touch_words(response[::2]) + logger.debug(f"[Completer] field completer updated with {response[::2]}.") + + if command_name in ("ZPOPMAX", "ZPOPMIN", "ZRANGE", "ZRANGE", "ZRANGEBYSCORE"): + if config.withscores: + self.member_completer.touch_words(response[::2]) + logger.debug( + f"[Completer] member completer updated with {response[::2]}." + ) + else: + self.member_completer.touch_words(response) + logger.debug(f"[Completer] member completer updated with {response}.") + + if command_name in ("KEYS",): + self.key_completer.touch_words(response) + logger.debug(f"[Completer] key completer updated with {response}.") + + if command_name in ("SCAN",): + self.key_completer.touch_words(response[1]) + logger.debug(f"[Completer] key completer updated with {response[1]}.") + + if command_name in ("SSCAN", "ZSCAN"): + self.member_completer.touch_words(response[1]) + logger.debug(f"[Completer] member completer updated with {response[1]}.") + + if command_name in ("HSCAN",): + self.field_completer.touch_words(response[1][::2]) + logger.debug( + f"[Completer] field completer updated with {response[1][::2]}." + ) + + # only update categoryname completer when `ACL CAT` without args. + if command_name == "ACL CAT" and not args: + self.catetoryname_completer.touch_words(response) + if command_name == "ACL USERS": + self.username_completer.touch_words(response) + + def _touch_members(self, items): + _step = 1 + + if config.withscores: + _step = 2 + + self.member_completer.touch_words(ensure_str(items)[::_step]) + + def _touch_hash_pairs(self, items): + self.field_completer.touch_words(ensure_str(items)[::2]) + + def _touch_keys(self, items): + self.key_completer.touch_words(ensure_str(items)) + + def __repr__(self) -> str: + return "DynamicCompleter(%r -> %r)" % ( + self.get_completer, + self.current_completer, + ) + + def get_completer_mapping(self, hint_on, completion_casing): + completer_mapping = {} + completer_mapping.update( + { + key: WordCompleter(tokens.split(" "), ignore_case=True) + for key, tokens in CONST.items() + } + ) + key_completer = MostRecentlyUsedFirstWordCompleter(config.completer_max, []) + member_completer = MostRecentlyUsedFirstWordCompleter(config.completer_max, []) + field_completer = MostRecentlyUsedFirstWordCompleter(config.completer_max, []) + group_completer = MostRecentlyUsedFirstWordCompleter(config.completer_max, []) + username_completer = MostRecentlyUsedFirstWordCompleter( + config.completer_max, [] + ) + categoryname_completer = MostRecentlyUsedFirstWordCompleter(100, []) + timestamp_completer = TimestampCompleter() + integer_type_completer = IntegerTypeCompleter() + + completer_mapping.update( + { + # all key related completers share the same completer + "keys": key_completer, + "key": key_completer, + "destination": key_completer, + "newkey": key_completer, + # member + "member": member_completer, + "members": member_completer, + # zmember + # TODO sperate sorted set and set + # hash fields + "field": field_completer, + "fields": field_completer, + # stream groups + "group": group_completer, + # stream id + "stream_id": timestamp_completer, + "inttype": integer_type_completer, + "categoryname": categoryname_completer, + "username": username_completer, + } + ) + + # command completer + if hint_on: + command_hint = { + key: info["summary"] for key, info in commands_summary.items() + } + hint = { + command: command_hint.get(command.upper()) for command in all_commands + } + hint.update( + { + command.lower(): command_hint.get(command.upper()) + for command in all_commands + } + ) + else: + hint = {} + + upper_commands = all_commands[::-1] + lower_commands = [command.lower() for command in all_commands[::-1]] + auto_commands = upper_commands + lower_commands + + ignore_case = completion_casing != "auto" + + command_completions = { + "auto": auto_commands, + "upper": upper_commands, + "lower": lower_commands, + }.get(completion_casing) + + completer_mapping["command"] = WordCompleter( + command_completions, ignore_case=ignore_case, sentence=True, meta_dict=hint + ) + return completer_mapping diff --git a/iredis/config.py b/iredis/config.py new file mode 100644 index 0000000..fefccdb --- /dev/null +++ b/iredis/config.py @@ -0,0 +1,130 @@ +from importlib_resources import path +import os +import logging + +from configobj import ConfigObj, ConfigObjError +from . import data as project_data + +# TODO verbose logger to print to stdout +logger = logging.getLogger(__name__) + + +system_config_file = "/etc/iredisrc" +pwd_config_file = os.path.join(os.getcwd(), ".iredisrc") + + +class Config: + """ + Global config, set once on start, then + become readonly, never change again. + + :param raw: weather write raw bytes to stdout without any + decoding. + :param decode: How to decode bytes response.(For display and + Completers) + default is None, means show literal bytes. But completers + will try use utf-8 decoding. + """ + + def __init__(self): + self.raw = None + self.completer_max = None + # show command hint? + self.newbie_mode = None + self.rainbow = None + self.retry_times = 2 + self.socket_keepalive = None + self.decode = None + self.no_info = None + self.bottom_bar = None + self.shell = None + self.enable_pager = None + self.pager = None + + self.warning = True + + self.no_version_reason = None + self.log_location = None + self.history_location = None + self.completion_casing = None + self.alias_dsn = None + + # ===bad code=== + # below are not configs, it's global state, it's wrong to write this + # please do not add more global state. + # FIXME this should be removed. + # use client attributes instead. + # use kwargs in render functions. + + # for transaction render + self.queued_commands = [] + self.transaction = False + # display zset withscores? + self.withscores = False + self.version = "Unknown" + + def __setter__(self, name, value): + # for every time start a transaction + # clear the queued commands first + if name == "transaction" and value is True: + self.queued_commands = [] + super().__setattr__(name, value) + + +config = Config() + + +def read_config_file(f): + """Read a config file.""" + + if isinstance(f, str): + f = os.path.expanduser(f) + + try: + config = ConfigObj(f, interpolation=False, encoding="utf8") + except ConfigObjError as e: + logger.error( + "Unable to parse line {0} of config file " "'{1}'.".format(e.line_number, f) + ) + logger.error("Using successfully parsed config values.") + return e.config + except (IOError, OSError) as e: + logger.error( + "You don't have permission to read " "config file '{0}'.".format(e.filename) + ) + return None + + return config + + +def load_config_files(iredisrc): + global config + + with path(project_data, "iredisrc") as p: + config_obj = ConfigObj(str(p)) + + for _file in [system_config_file, iredisrc, pwd_config_file]: + _config = read_config_file(_file) + if bool(_config) is True: + config_obj.merge(_config) + config_obj.filename = _config.filename + + config.raw = config_obj["main"].as_bool("raw") + config.completer_max = config_obj["main"].as_int("completer_max") + config.retry_times = config_obj["main"].as_int("retry_times") + config.newbie_mode = config_obj["main"].as_bool("newbie_mode") + config.rainbow = config_obj["main"].as_bool("rainbow") + config.socket_keepalive = config_obj["main"].as_bool("socket_keepalive") + config.no_info = config_obj["main"].as_bool("no_info") + config.bottom_bar = config_obj["main"].as_bool("bottom_bar") + config.warning = config_obj["main"].as_bool("warning") + config.decode = config_obj["main"]["decode"] + config.log_location = config_obj["main"]["log_location"] + config.completion_casing = config_obj["main"]["completion_casing"] + config.history_location = config_obj["main"]["history_location"] + config.alias_dsn = config_obj["alias_dsn"] + config.shell = config_obj["main"].as_bool("shell") + config.pager = config_obj["main"].get("pager") + config.enable_pager = config_obj["main"].as_bool("enable_pager") + + return config_obj diff --git a/iredis/data/__init__.py b/iredis/data/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/iredis/data/__init__.py diff --git a/iredis/data/command_syntax.csv b/iredis/data/command_syntax.csv new file mode 100644 index 0000000..e29c2c0 --- /dev/null +++ b/iredis/data/command_syntax.csv @@ -0,0 +1,264 @@ +Group,Command,Syntax,Callback +cluster,CLUSTER ADDSLOTS,command_slots,render_simple_string +cluster,CLUSTER BUMPEPOCH,command,render_simple_string +cluster,CLUSTER COUNT-FAILURE-REPORTS,command_node,render_int +cluster,CLUSTER COUNTKEYSINSLOT,command_slot,render_int +cluster,CLUSTER DELSLOTS,command_slots,render_simple_string +cluster,CLUSTER FAILOVER,command_failoverchoice,render_simple_string +cluster,CLUSTER FLUSHSLOTS,command,render_simple_string +cluster,CLUSTER FORGET,command_node,render_simple_string +cluster,CLUSTER GETKEYSINSLOT,command_slot_count,render_list +cluster,CLUSTER INFO,command,render_bulk_string_decode +cluster,CLUSTER KEYSLOT,command_key,render_int +cluster,CLUSTER MEET,command_ip_port,render_simple_string +cluster,CLUSTER MYID,command,render_bulk_string_decode +cluster,CLUSTER NODES,command,render_bulk_string_decode +cluster,CLUSTER REPLICAS,command_node,render_bulk_string_decode +cluster,CLUSTER REPLICATE,command_node,render_simple_string +cluster,CLUSTER RESET,command_resetchoice,render_simple_string +cluster,CLUSTER SAVECONFIG,command,render_simple_string +cluster,CLUSTER SET-CONFIG-EPOCH,command_epoch,render_simple_string +cluster,CLUSTER SETSLOT,command_slot_slotsubcmd_nodex,render_simple_string +cluster,CLUSTER SLAVES,command_node,render_bulk_string_decode +cluster,CLUSTER SLOTS,command,render_list +cluster,READONLY,command,render_simple_string +cluster,READWRITE,command,render_simple_string +connection,AUTH,command_password,render_simple_string +connection,ECHO,command_message,render_bulk_string +connection,HELLO,command_any,render_list +connection,PING,command_messagex,render_bulk_string +connection,QUIT,command,render_simple_string +connection,SELECT,command_index,render_simple_string +connection,CLIENT CACHING,command_yes,render_simple_string +connection,CLIENT GETREDIR,command,render_int +connection,CLIENT TRACKING,command_client_tracking,render_simple_string +connection,CLIENT LIST,command_type_conntype_x,render_bulk_string_decode +connection,CLIENT GETNAME,command,render_bulk_string +connection,CLIENT ID,command,render_int +connection,CLIENT KILL,command_clientkill,render_string_or_int +connection,CLIENT PAUSE,command_timeout,render_simple_string +connection,CLIENT REPLY,command_switch,render_simple_string +connection,CLIENT SETNAME,command_value,render_simple_string +connection,CLIENT UNBLOCK,command_clientid_errorx,render_int +generic,DEL,command_keys,render_int +generic,DUMP,command_key,render_bulk_string +generic,EXISTS,command_keys,render_int +generic,EXPIRE,command_key_second,render_int +generic,EXPIREAT,command_key_timestamp,render_int +generic,KEYS,command_pattern,command_keys +generic,MIGRATE,command_migrate,render_simple_string +generic,MOVE,command_key_index,render_int +generic,OBJECT,command_object_key,render_string_or_int +generic,PERSIST,command_key,render_int +generic,PEXPIRE,command_key_millisecond,render_int +generic,PEXPIREAT,command_key_timestampms,render_int +generic,PTTL,command_key,render_int +generic,RANDOMKEY,command,render_bulk_string +generic,RENAME,command_key_newkey,render_simple_string +generic,RENAMENX,command_key_newkey,render_int +generic,RESTORE,command_restore,render_simple_string +generic,SCAN,command_cursor_match_pattern_count_type,command_scan +generic,SORT,command_any,render_list_or_string +generic,TOUCH,command_keys,render_int +generic,TTL,command_key,render_int +generic,TYPE,command_key,render_bulk_string +generic,UNLINK,command_keys,render_int +generic,WAIT,command_count_timeout,render_int +geo,GEOADD,command_key_longitude_latitude_members,render_int +geo,GEODIST,command_geodist,render_bulk_string +geo,GEOHASH,command_key_members,render_list +geo,GEOPOS,command_key_members,render_list +geo,GEORADIUS,command_radius,render_list_or_string +geo,GEORADIUSBYMEMBER,command_georadiusbymember,render_list_or_string +hash,HDEL,command_key_fields,render_int +hash,HEXISTS,command_key_field,render_int +hash,HGET,command_key_field,render_bulk_string +hash,HGETALL,command_key,render_hash_pairs +hash,HINCRBY,command_key_field_delta,render_int +hash,HINCRBYFLOAT,command_key_field_float,render_bulk_string +hash,HKEYS,command_key,command_hkeys +hash,HLEN,command_key,render_int +hash,HMGET,command_key_fields,render_list +hash,HMSET,command_key_fieldvalues,render_bulk_string +hash,HSCAN,command_key_cursor_match_pattern_count,command_hscan +hash,HSET,command_key_field_value,render_int +hash,HSETNX,command_key_field_value,render_int +hash,HSTRLEN,command_key_field,render_int +hash,HVALS,command_key,render_list +hyperloglog,PFADD,command_key_values,render_int +hyperloglog,PFCOUNT,command_keys,render_int +hyperloglog,PFMERGE,command_newkey_keys,render_simple_string +list,BLPOP,command_keys_timeout,render_list_or_string +list,BRPOP,command_keys_timeout,render_list_or_string +list,BRPOPLPUSH,command_key_newkey_timeout,render_bulk_string +list,LINDEX,command_key_position,render_bulk_string +list,LINSERT,command_key_positionchoice_pivot_value,render_int +list,LLEN,command_key,render_int +list,LPOS,command_lpos,render_list_or_string +list,LPOP,command_key,render_bulk_string +list,LPUSH,command_key_values,render_int +list,LPUSHX,command_key_values,render_int +list,LRANGE,command_key_start_end,render_list +list,LREM,command_key_position_value,render_int +list,LSET,command_key_position_value,render_simple_string +list,LTRIM,command_key_start_end,render_simple_string +list,RPOP,command_key,render_bulk_string +list,RPOPLPUSH,command_key_newkey,render_bulk_string +list,RPUSH,command_key_values,render_int +list,RPUSHX,command_key_value,render_int +pubsub,PSUBSCRIBE,command_channels,render_subscribe +pubsub,PUBLISH,command_channel_message,render_int +pubsub,PUBSUB,command_pubsubcmd_channels,render_list_or_string +pubsub,PUNSUBSCRIBE,command_channels,render_subscribe +pubsub,SUBSCRIBE,command_channels,render_subscribe +pubsub,UNSUBSCRIBE,command_channels,render_subscribe +scripting,EVAL,command_lua_any,render_list_or_string +scripting,EVALSHA,command_any,render_list_or_string +scripting,SCRIPT DEBUG,command_scriptdebug,render_simple_string +scripting,SCRIPT EXISTS,command_any,render_list +scripting,SCRIPT FLUSH,command,render_simple_string +scripting,SCRIPT KILL,command,render_simple_string +scripting,SCRIPT LOAD,command_lua_any,render_bulk_string_decode +server,ACL CAT,command_categorynamex,render_list +server,ACL DELUSER,command_usernames,render_int +server,ACL GENPASS,command_countx,render_bulk_string +server,ACL GETUSER,command_username,render_list +server,ACL HELP,command,render_list +server,ACL LIST,command,render_list +server,ACL LOAD,command,render_simple_string +server,ACL LOG,command_count_or_resetx,render_list_or_string +server,ACL SAVE,command,render_simple_string +server,ACL SETUSER,command_username_rules,render_simple_string +server,ACL USERS,command,render_list +server,ACL WHOAMI,command,render_bulk_string +server,SWAPDB,command_index_index,render_simple_string +server,BGREWRITEAOF,command,render_simple_string +server,BGSAVE,command_schedulex,render_simple_string +server,COMMAND,command,render_list +server,COMMAND COUNT,command,render_int +server,COMMAND GETKEYS,command_any,render_list +server,COMMAND INFO,command_commandname,render_list +server,CONFIG GET,command_parameter,render_nested_pair +server,CONFIG RESETSTAT,command,render_simple_string +server,CONFIG REWRITE,command,render_simple_string +server,CONFIG SET,command_parameter_value,render_simple_string +server,DBSIZE,command,render_int +server,DEBUG OBJECT,command_key,render_simple_string +server,DEBUG SEGFAULT,command,render_simple_string +server,FLUSHALL,command_asyncx,render_simple_string +server,FLUSHDB,command_asyncx,render_simple_string +server,INFO,command_sectionx,render_bulk_string_decode +server,LOLWUT,command_version,render_bytes +server,LASTSAVE,command,render_unixtime +server,LATENCY DOCTOR,command,render_bulk_string_decode +server,LATENCY GRAPH,command_graphevent,render_bulk_string_decode +server,LATENCY HELP,command,render_list +server,LATENCY HISTORY,command_graphevent,render_list +server,LATENCY LATEST,command,render_list +server,LATENCY RESET,command_graphevents,render_int +server,MEMORY DOCTOR,command,render_bulk_string_decode +server,MEMORY HELP,command,render_list +server,MEMORY MALLOC-STATS,command,render_bulk_string_decode +server,MEMORY PURGE,command,render_simple_string +server,MEMORY STATS,command,render_nested_pair +server,MEMORY USAGE,command_key_samples_count,render_int +server,MODULE LIST,command,render_list +server,MODULE LOAD,command_any,render_simple_string +server,MODULE UNLOAD,command_any,render_simple_string +server,MONITOR,command,render_simple_string +server,PSYNC,command_replicationid_offset,render_bulk_string_decode +server,REPLICAOF,command_any,render_simple_string +server,ROLE,command,render_list +server,SAVE,command,render_simple_string +server,SHUTDOWN,command_shutdown,render_simple_string +server,SLAVEOF,command_any,render_simple_string +server,SLOWLOG,command_slowlog,render_slowlog +server,SYNC,command,render_bulk_string +server,TIME,command,render_time +set,SADD,command_key_members,render_int +set,SCARD,command_key,render_int +set,SDIFF,command_keys,render_list +set,SDIFFSTORE,command_destination_keys,render_int +set,SINTER,command_keys,render_list +set,SINTERSTORE,command_destination_keys,render_int +set,SISMEMBER,command_key_member,render_int +set,SMEMBERS,command_key,render_list +set,SMOVE,command_key_newkey_member,render_int +set,SPOP,command_key_count_x,render_list_or_string +set,SRANDMEMBER,command_key_count_x,render_list_or_string +set,SREM,command_key_members,render_int +set,SSCAN,command_key_cursor_match_pattern_count,command_sscan +set,SUNION,command_keys,render_list +set,SUNIONSTORE,command_destination_keys,render_int +sorted_set,BZPOPMAX,command_keys_timeout,render_list_or_string +sorted_set,BZPOPMIN,command_keys_timeout,render_list_or_string +sorted_set,ZADD,command_key_condition_changed_incr_score_members,render_string_or_int +sorted_set,ZCARD,command_key,render_int +sorted_set,ZCOUNT,command_key_min_max,render_int +sorted_set,ZINCRBY,command_key_float_member,render_bulk_string +sorted_set,ZINTERSTORE,command_any,render_int +sorted_set,ZLEXCOUNT,command_key_lexmin_lexmax,render_int +sorted_set,ZPOPMAX,command_key_count_x,render_members +sorted_set,ZPOPMIN,command_key_count_x,render_members +sorted_set,ZRANGE,command_key_start_end_withscores_x,render_members +sorted_set,ZRANGEBYLEX,command_key_lexmin_lexmax_limit_offset_count,render_list +sorted_set,ZRANGEBYSCORE,command_key_min_max_withscore_x_limit_offset_count_x,render_members +sorted_set,ZRANK,command_key_member,render_int +sorted_set,ZREM,command_key_members,render_int +sorted_set,ZREMRANGEBYLEX,command_key_lexmin_lexmax,render_int +sorted_set,ZREMRANGEBYRANK,command_key_start_end,render_int +sorted_set,ZREMRANGEBYSCORE,command_key_min_max,render_int +sorted_set,ZREVRANGE,command_key_start_end_withscores_x,render_list +sorted_set,ZREVRANGEBYLEX,command_key_lexmin_lexmax_limit_offset_count,render_list +sorted_set,ZREVRANGEBYSCORE,command_key_min_max_withscore_x_limit_offset_count_x,render_list +sorted_set,ZREVRANK,command_key_member,render_int +sorted_set,ZSCAN,command_key_cursor_match_pattern_count,command_sscan +sorted_set,ZSCORE,command_key_member,render_bulk_string +sorted_set,ZUNIONSTORE,command_any,render_int +stream,XACK,command_key_group_ids,render_int +stream,XADD,command_xadd,render_bulk_string +stream,XCLAIM,command_xclaim,render_list +stream,XDEL,command_key_ids,render_int +stream,XGROUP,command_xgroup,render_string_or_int +stream,XINFO,command_xinfo,render_list +stream,XLEN,command_key,render_int +stream,XPENDING,command_xpending,render_list +stream,XRANGE,command_key_start_end_countx,render_list +stream,XREAD,command_xread,render_list +stream,XREADGROUP,command_xreadgroup,render_list +stream,XREVRANGE,command_key_start_end_countx,render_list +stream,XTRIM,command_key_maxlen,render_int +string,APPEND,command_key_value,render_int +string,BITCOUNT,command_key_start_end_x,render_int +string,BITFIELD,command_bitfield,render_list +string,BITOP,command_operation_key_keys,render_int +string,BITPOS,command_key_bit_start_end,render_int +string,DECR,command_key,render_int +string,DECRBY,command_key_delta,render_int +string,GET,command_key,render_bulk_string +string,GETBIT,command_key_offset,render_int +string,GETRANGE,command_key_start_end,render_bulk_string +string,GETSET,command_key_value,render_bulk_string +string,INCR,command_key,render_int +string,INCRBY,command_key_delta,render_int +string,INCRBYFLOAT,command_key_float,render_bulk_string +string,MGET,command_keys,render_list +string,MSET,command_key_valuess,render_simple_string +string,MSETNX,command_key_valuess,render_int +string,PSETEX,command_key_millisecond_value,render_bulk_string +string,SET,command_set,render_simple_string +string,SETBIT,command_key_offset_bit,render_int +string,SETEX,command_key_second_value,render_bulk_string +string,SETNX,command_key_value,render_int +string,SETRANGE,command_key_offset_value,render_int +string,STRALGO,command_stralgo,render_list_or_string +string,STRLEN,command_key,render_int +transactions,DISCARD,command,render_simple_string +transactions,EXEC,command,render_list_or_string +transactions,MULTI,command,render_simple_string +transactions,UNWATCH,command,render_simple_string +transactions,WATCH,command_keys,render_simple_string +iredis,HELP,command_command, +iredis,PEEK,command_key, +iredis,CLEAR,command, +iredis,EXIT,command, diff --git a/iredis/data/commands.json b/iredis/data/commands.json new file mode 100644 index 0000000..ae05b80 --- /dev/null +++ b/iredis/data/commands.json @@ -0,0 +1,4470 @@ +{ + "ACL LOAD": { + "summary": "Reload the ACLs from the configured ACL file", + "complexity": "O(N). Where N is the number of configured users.", + "since": "6.0.0", + "group": "server" + }, + "ACL SAVE": { + "summary": "Save the current ACL rules in the configured ACL file", + "complexity": "O(N). Where N is the number of configured users.", + "since": "6.0.0", + "group": "server" + }, + "ACL LIST": { + "summary": "List the current ACL rules in ACL config file format", + "complexity": "O(N). Where N is the number of configured users.", + "since": "6.0.0", + "group": "server" + }, + "ACL USERS": { + "summary": "List the username of all the configured ACL rules", + "complexity": "O(N). Where N is the number of configured users.", + "since": "6.0.0", + "group": "server" + }, + "ACL SETUSER": { + "summary": "Modify or create the rules for a specific ACL user", + "complexity": "O(N). Where N is the number of rules provided.", + "arguments": [ + { + "name": "rule", + "type": "string", + "multiple": true + } + ], + "since": "6.0.0", + "group": "server" + }, + "ACL DELUSER": { + "summary": "Remove the specified ACL users and the associated rules", + "complexity": "O(1) amortized time considering the typical user.", + "arguments": [ + { + "name": "username", + "type": "string", + "multiple": true + } + ], + "since": "6.0.0", + "group": "server" + }, + "ACL CAT": { + "summary": "List the ACL categories or the commands inside a category", + "complexity": "O(1) since the categories and commands are a fixed set.", + "arguments": [ + { + "name": "categoryname", + "type": "string", + "optional": true + } + ], + "since": "6.0.0", + "group": "server" + }, + "ACL GENPASS": { + "summary": "Generate a pseudorandom secure password to use for ACL users", + "complexity": "O(1)", + "arguments": [ + { + "name": "bits", + "type": "integer", + "optional": true + } + ], + "since": "6.0.0", + "group": "server" + }, + "ACL WHOAMI": { + "summary": "Return the name of the user associated to the current connection", + "complexity": "O(1)", + "since": "6.0.0", + "group": "server" + }, + "ACL LOG": { + "summary": "List latest events denied because of ACLs in place", + "complexity": "O(N) with N being the number of entries shown.", + "arguments": [ + { + "name": "count or RESET", + "type": "string", + "optional": true + } + ], + "since": "6.0.0", + "group": "server" + }, + "APPEND": { + "summary": "Append a value to a key", + "complexity": "O(1). The amortized time complexity is O(1) assuming the appended value is small and the already present value is of any size, since the dynamic string library used by Redis will double the free space available on every reallocation.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "value", + "type": "string" + } + ], + "since": "2.0.0", + "group": "string" + }, + "AUTH": { + "summary": "Authenticate to the server", + "arguments": [ + { + "name": "password", + "type": "string" + } + ], + "since": "1.0.0", + "group": "connection" + }, + "BGREWRITEAOF": { + "summary": "Asynchronously rewrite the append-only file", + "since": "1.0.0", + "group": "server" + }, + "BGSAVE": { + "summary": "Asynchronously save the dataset to disk", + "arguments": [ + { + "name": "schedule", + "type": "enum", + "enum": [ + "SCHEDULE" + ], + "optional": true + } + ], + "since": "1.0.0", + "group": "server" + }, + "BITCOUNT": { + "summary": "Count set bits in a string", + "complexity": "O(N)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": [ + "start", + "end" + ], + "type": [ + "integer", + "integer" + ], + "optional": true + } + ], + "since": "2.6.0", + "group": "string" + }, + "BITFIELD": { + "summary": "Perform arbitrary bitfield integer operations on strings", + "complexity": "O(1) for each subcommand specified", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "command": "GET", + "name": [ + "type", + "offset" + ], + "type": [ + "type", + "integer" + ], + "optional": true + }, + { + "command": "SET", + "name": [ + "type", + "offset", + "value" + ], + "type": [ + "type", + "integer", + "integer" + ], + "optional": true + }, + { + "command": "INCRBY", + "name": [ + "type", + "offset", + "increment" + ], + "type": [ + "type", + "integer", + "integer" + ], + "optional": true + }, + { + "command": "OVERFLOW", + "type": "enum", + "enum": [ + "WRAP", + "SAT", + "FAIL" + ], + "optional": true + } + ], + "since": "3.2.0", + "group": "string" + }, + "BITOP": { + "summary": "Perform bitwise operations between strings", + "complexity": "O(N)", + "arguments": [ + { + "name": "operation", + "type": "string" + }, + { + "name": "destkey", + "type": "key" + }, + { + "name": "key", + "type": "key", + "multiple": true + } + ], + "since": "2.6.0", + "group": "string" + }, + "BITPOS": { + "summary": "Find first bit set or clear in a string", + "complexity": "O(N)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "bit", + "type": "integer" + }, + { + "name": "start", + "type": "integer", + "optional": true + }, + { + "name": "end", + "type": "integer", + "optional": true + } + ], + "since": "2.8.7", + "group": "string" + }, + "BLPOP": { + "summary": "Remove and get the first element in a list, or block until one is available", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key", + "multiple": true + }, + { + "name": "timeout", + "type": "integer" + } + ], + "since": "2.0.0", + "group": "list" + }, + "BRPOP": { + "summary": "Remove and get the last element in a list, or block until one is available", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key", + "multiple": true + }, + { + "name": "timeout", + "type": "integer" + } + ], + "since": "2.0.0", + "group": "list" + }, + "BRPOPLPUSH": { + "summary": "Pop an element from a list, push it to another list and return it; or block until one is available", + "complexity": "O(1)", + "arguments": [ + { + "name": "source", + "type": "key" + }, + { + "name": "destination", + "type": "key" + }, + { + "name": "timeout", + "type": "integer" + } + ], + "since": "2.2.0", + "group": "list" + }, + "BZPOPMIN": { + "summary": "Remove and return the member with the lowest score from one or more sorted sets, or block until one is available", + "complexity": "O(log(N)) with N being the number of elements in the sorted set.", + "arguments": [ + { + "name": "key", + "type": "key", + "multiple": true + }, + { + "name": "timeout", + "type": "integer" + } + ], + "since": "5.0.0", + "group": "sorted_set" + }, + "BZPOPMAX": { + "summary": "Remove and return the member with the highest score from one or more sorted sets, or block until one is available", + "complexity": "O(log(N)) with N being the number of elements in the sorted set.", + "arguments": [ + { + "name": "key", + "type": "key", + "multiple": true + }, + { + "name": "timeout", + "type": "integer" + } + ], + "since": "5.0.0", + "group": "sorted_set" + }, + "CLIENT CACHING": { + "summary": "Instruct the server about tracking or not keys in the next request", + "complexity": "O(1)", + "arguments": [ + { + "name": "mode", + "type": "enum", + "enum": [ + "YES", + "NO" + ] + } + ], + "since": "6.0.0", + "group": "connection" + }, + "CLIENT ID": { + "summary": "Returns the client ID for the current connection", + "complexity": "O(1)", + "since": "5.0.0", + "group": "connection" + }, + "CLIENT KILL": { + "summary": "Kill the connection of a client", + "complexity": "O(N) where N is the number of client connections", + "arguments": [ + { + "name": "ip:port", + "type": "string", + "optional": true + }, + { + "command": "ID", + "name": "client-id", + "type": "integer", + "optional": true + }, + { + "command": "TYPE", + "type": "enum", + "enum": [ + "normal", + "master", + "slave", + "pubsub" + ], + "optional": true + }, + { + "command": "ADDR", + "name": "ip:port", + "type": "string", + "optional": true + }, + { + "command": "SKIPME", + "name": "yes/no", + "type": "string", + "optional": true + } + ], + "since": "2.4.0", + "group": "connection" + }, + "CLIENT LIST": { + "summary": "Get the list of client connections", + "complexity": "O(N) where N is the number of client connections", + "arguments": [ + { + "command": "TYPE", + "type": "enum", + "enum": [ + "normal", + "master", + "replica", + "pubsub" + ], + "optional": true + } + ], + "since": "2.4.0", + "group": "connection" + }, + "CLIENT GETNAME": { + "summary": "Get the current connection name", + "complexity": "O(1)", + "since": "2.6.9", + "group": "connection" + }, + "CLIENT GETREDIR": { + "summary": "Get tracking notifications redirection client ID if any", + "complexity": "O(1)", + "since": "6.0.0", + "group": "connection" + }, + "CLIENT PAUSE": { + "summary": "Stop processing commands from clients for some time", + "complexity": "O(1)", + "arguments": [ + { + "name": "timeout", + "type": "integer" + } + ], + "since": "2.9.50", + "group": "connection" + }, + "CLIENT REPLY": { + "summary": "Instruct the server whether to reply to commands", + "complexity": "O(1)", + "arguments": [ + { + "name": "reply-mode", + "type": "enum", + "enum": [ + "ON", + "OFF", + "SKIP" + ] + } + ], + "since": "3.2", + "group": "connection" + }, + "CLIENT SETNAME": { + "summary": "Set the current connection name", + "complexity": "O(1)", + "since": "2.6.9", + "arguments": [ + { + "name": "connection-name", + "type": "string" + } + ], + "group": "connection" + }, + "CLIENT TRACKING": { + "summary": "Enable or disable server assisted client side caching support", + "complexity": "O(1)", + "arguments": [ + { + "name": "status", + "type": "enum", + "enum": [ + "ON", + "OFF" + ] + }, + { + "command": "REDIRECT", + "name": "client-id", + "type": "integer", + "optional": true + }, + { + "command": "PREFIX", + "name": "prefix", + "type": "srting", + "optional": true + }, + { + "name": "BCAST", + "type": "enum", + "enum": [ + "BCAST" + ], + "optional": true + }, + { + "name": "OPTIN", + "type": "enum", + "enum": [ + "OPTIN" + ], + "optional": true + }, + { + "name": "OPTOUT", + "type": "enum", + "enum": [ + "OPTOUT" + ], + "optional": true + }, + { + "name": "NOLOOP", + "type": "enum", + "enum": [ + "NOLOOP" + ], + "optional": true + } + ], + "since": "6.0.0", + "group": "connection" + }, + "CLIENT UNBLOCK": { + "summary": "Unblock a client blocked in a blocking command from a different connection", + "complexity": "O(log N) where N is the number of client connections", + "arguments": [ + { + "name": "client-id", + "type": "integer" + }, + { + "name": "unblock-type", + "type": "enum", + "enum": [ + "TIMEOUT", + "ERROR" + ], + "optional": true + } + ], + "since": "5.0.0", + "group": "connection" + }, + "CLUSTER ADDSLOTS": { + "summary": "Assign new hash slots to receiving node", + "complexity": "O(N) where N is the total number of hash slot arguments", + "arguments": [ + { + "name": "slot", + "type": "integer", + "multiple": true + } + ], + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER BUMPEPOCH": { + "summary": "Advance the cluster config epoch", + "complexity": "O(1)", + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER COUNT-FAILURE-REPORTS": { + "summary": "Return the number of failure reports active for a given node", + "complexity": "O(N) where N is the number of failure reports", + "arguments": [ + { + "name": "node-id", + "type": "string" + } + ], + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER COUNTKEYSINSLOT": { + "summary": "Return the number of local keys in the specified hash slot", + "complexity": "O(1)", + "arguments": [ + { + "name": "slot", + "type": "integer" + } + ], + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER DELSLOTS": { + "summary": "Set hash slots as unbound in receiving node", + "complexity": "O(N) where N is the total number of hash slot arguments", + "arguments": [ + { + "name": "slot", + "type": "integer", + "multiple": true + } + ], + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER FAILOVER": { + "summary": "Forces a replica to perform a manual failover of its master.", + "complexity": "O(1)", + "arguments": [ + { + "name": "options", + "type": "enum", + "enum": [ + "FORCE", + "TAKEOVER" + ], + "optional": true + } + ], + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER FLUSHSLOTS": { + "summary": "Delete a node's own slots information", + "complexity": "O(1)", + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER FORGET": { + "summary": "Remove a node from the nodes table", + "complexity": "O(1)", + "arguments": [ + { + "name": "node-id", + "type": "string" + } + ], + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER GETKEYSINSLOT": { + "summary": "Return local key names in the specified hash slot", + "complexity": "O(log(N)) where N is the number of requested keys", + "arguments": [ + { + "name": "slot", + "type": "integer" + }, + { + "name": "count", + "type": "integer" + } + ], + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER INFO": { + "summary": "Provides info about Redis Cluster node state", + "complexity": "O(1)", + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER KEYSLOT": { + "summary": "Returns the hash slot of the specified key", + "complexity": "O(N) where N is the number of bytes in the key", + "arguments": [ + { + "name": "key", + "type": "string" + } + ], + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER MEET": { + "summary": "Force a node cluster to handshake with another node", + "complexity": "O(1)", + "arguments": [ + { + "name": "ip", + "type": "string" + }, + { + "name": "port", + "type": "integer" + } + ], + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER MYID": { + "summary": "Return the node id", + "complexity": "O(1)", + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER NODES": { + "summary": "Get Cluster config for the node", + "complexity": "O(N) where N is the total number of Cluster nodes", + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER REPLICATE": { + "summary": "Reconfigure a node as a replica of the specified master node", + "complexity": "O(1)", + "arguments": [ + { + "name": "node-id", + "type": "string" + } + ], + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER RESET": { + "summary": "Reset a Redis Cluster node", + "complexity": "O(N) where N is the number of known nodes. The command may execute a FLUSHALL as a side effect.", + "arguments": [ + { + "name": "reset-type", + "type": "enum", + "enum": [ + "HARD", + "SOFT" + ], + "optional": true + } + ], + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER SAVECONFIG": { + "summary": "Forces the node to save cluster state on disk", + "complexity": "O(1)", + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER SET-CONFIG-EPOCH": { + "summary": "Set the configuration epoch in a new node", + "complexity": "O(1)", + "arguments": [ + { + "name": "config-epoch", + "type": "integer" + } + ], + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER SETSLOT": { + "summary": "Bind a hash slot to a specific node", + "complexity": "O(1)", + "arguments": [ + { + "name": "slot", + "type": "integer" + }, + { + "name": "subcommand", + "type": "enum", + "enum": [ + "IMPORTING", + "MIGRATING", + "STABLE", + "NODE" + ] + }, + { + "name": "node-id", + "type": "string", + "optional": true + } + ], + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER SLAVES": { + "summary": "List replica nodes of the specified master node", + "complexity": "O(1)", + "arguments": [ + { + "name": "node-id", + "type": "string" + } + ], + "since": "3.0.0", + "group": "cluster" + }, + "CLUSTER REPLICAS": { + "summary": "List replica nodes of the specified master node", + "complexity": "O(1)", + "arguments": [ + { + "name": "node-id", + "type": "string" + } + ], + "since": "5.0.0", + "group": "cluster" + }, + "CLUSTER SLOTS": { + "summary": "Get array of Cluster slot to node mappings", + "complexity": "O(N) where N is the total number of Cluster nodes", + "since": "3.0.0", + "group": "cluster" + }, + "COMMAND": { + "summary": "Get array of Redis command details", + "complexity": "O(N) where N is the total number of Redis commands", + "since": "2.8.13", + "group": "server" + }, + "COMMAND COUNT": { + "summary": "Get total number of Redis commands", + "complexity": "O(1)", + "since": "2.8.13", + "group": "server" + }, + "COMMAND GETKEYS": { + "summary": "Extract keys given a full Redis command", + "complexity": "O(N) where N is the number of arguments to the command", + "since": "2.8.13", + "group": "server" + }, + "COMMAND INFO": { + "summary": "Get array of specific Redis command details", + "complexity": "O(N) when N is number of commands to look up", + "since": "2.8.13", + "arguments": [ + { + "name": "command-name", + "type": "string", + "multiple": true + } + ], + "group": "server" + }, + "CONFIG GET": { + "summary": "Get the value of a configuration parameter", + "arguments": [ + { + "name": "parameter", + "type": "string" + } + ], + "since": "2.0.0", + "group": "server" + }, + "CONFIG REWRITE": { + "summary": "Rewrite the configuration file with the in memory configuration", + "since": "2.8.0", + "group": "server" + }, + "CONFIG SET": { + "summary": "Set a configuration parameter to the given value", + "arguments": [ + { + "name": "parameter", + "type": "string" + }, + { + "name": "value", + "type": "string" + } + ], + "since": "2.0.0", + "group": "server" + }, + "CONFIG RESETSTAT": { + "summary": "Reset the stats returned by INFO", + "complexity": "O(1)", + "since": "2.0.0", + "group": "server" + }, + "DBSIZE": { + "summary": "Return the number of keys in the selected database", + "since": "1.0.0", + "group": "server" + }, + "DEBUG OBJECT": { + "summary": "Get debugging information about a key", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "1.0.0", + "group": "server" + }, + "DEBUG SEGFAULT": { + "summary": "Make the server crash", + "since": "1.0.0", + "group": "server" + }, + "DECR": { + "summary": "Decrement the integer value of a key by one", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "1.0.0", + "group": "string" + }, + "DECRBY": { + "summary": "Decrement the integer value of a key by the given number", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "decrement", + "type": "integer" + } + ], + "since": "1.0.0", + "group": "string" + }, + "DEL": { + "summary": "Delete a key", + "complexity": "O(N) where N is the number of keys that will be removed. When a key to remove holds a value other than a string, the individual complexity for this key is O(M) where M is the number of elements in the list, set, sorted set or hash. Removing a single key that holds a string value is O(1).", + "arguments": [ + { + "name": "key", + "type": "key", + "multiple": true + } + ], + "since": "1.0.0", + "group": "generic" + }, + "DISCARD": { + "summary": "Discard all commands issued after MULTI", + "since": "2.0.0", + "group": "transactions" + }, + "DUMP": { + "summary": "Return a serialized version of the value stored at the specified key.", + "complexity": "O(1) to access the key and additional O(N*M) to serialized it, where N is the number of Redis objects composing the value and M their average size. For small string values the time complexity is thus O(1)+O(1*M) where M is small, so simply O(1).", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "2.6.0", + "group": "generic" + }, + "ECHO": { + "summary": "Echo the given string", + "arguments": [ + { + "name": "message", + "type": "string" + } + ], + "since": "1.0.0", + "group": "connection" + }, + "EVAL": { + "summary": "Execute a Lua script server side", + "complexity": "Depends on the script that is executed.", + "arguments": [ + { + "name": "script", + "type": "string" + }, + { + "name": "numkeys", + "type": "integer" + }, + { + "name": "key", + "type": "key", + "multiple": true + }, + { + "name": "arg", + "type": "string", + "multiple": true + } + ], + "since": "2.6.0", + "group": "scripting" + }, + "EVALSHA": { + "summary": "Execute a Lua script server side", + "complexity": "Depends on the script that is executed.", + "arguments": [ + { + "name": "sha1", + "type": "string" + }, + { + "name": "numkeys", + "type": "integer" + }, + { + "name": "key", + "type": "key", + "multiple": true + }, + { + "name": "arg", + "type": "string", + "multiple": true + } + ], + "since": "2.6.0", + "group": "scripting" + }, + "EXEC": { + "summary": "Execute all commands issued after MULTI", + "since": "1.2.0", + "group": "transactions" + }, + "EXISTS": { + "summary": "Determine if a key exists", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key", + "multiple": true + } + ], + "since": "1.0.0", + "group": "generic" + }, + "EXPIRE": { + "summary": "Set a key's time to live in seconds", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "seconds", + "type": "integer" + } + ], + "since": "1.0.0", + "group": "generic" + }, + "EXPIREAT": { + "summary": "Set the expiration for a key as a UNIX timestamp", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "timestamp", + "type": "posix time" + } + ], + "since": "1.2.0", + "group": "generic" + }, + "FLUSHALL": { + "summary": "Remove all keys from all databases", + "arguments": [ + { + "name": "async", + "type": "enum", + "enum": [ + "ASYNC" + ], + "optional": true + } + ], + "since": "1.0.0", + "group": "server" + }, + "FLUSHDB": { + "summary": "Remove all keys from the current database", + "arguments": [ + { + "name": "async", + "type": "enum", + "enum": [ + "ASYNC" + ], + "optional": true + } + ], + "since": "1.0.0", + "group": "server" + }, + "GEOADD": { + "summary": "Add one or more geospatial items in the geospatial index represented using a sorted set", + "complexity": "O(log(N)) for each item added, where N is the number of elements in the sorted set.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": [ + "longitude", + "latitude", + "member" + ], + "type": [ + "double", + "double", + "string" + ], + "multiple": true + } + ], + "since": "3.2.0", + "group": "geo" + }, + "GEOHASH": { + "summary": "Returns members of a geospatial index as standard geohash strings", + "complexity": "O(log(N)) for each member requested, where N is the number of elements in the sorted set.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "member", + "type": "string", + "multiple": true + } + ], + "since": "3.2.0", + "group": "geo" + }, + "GEOPOS": { + "summary": "Returns longitude and latitude of members of a geospatial index", + "complexity": "O(log(N)) for each member requested, where N is the number of elements in the sorted set.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "member", + "type": "string", + "multiple": true + } + ], + "since": "3.2.0", + "group": "geo" + }, + "GEODIST": { + "summary": "Returns the distance between two members of a geospatial index", + "complexity": "O(log(N))", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "member1", + "type": "string" + }, + { + "name": "member2", + "type": "string" + }, + { + "name": "unit", + "type": "enum", + "enum": [ + "m", + "km", + "ft", + "mi" + ], + "optional": true + } + ], + "since": "3.2.0", + "group": "geo" + }, + "GEORADIUS": { + "summary": "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point", + "complexity": "O(N+log(M)) where N is the number of elements inside the bounding box of the circular area delimited by center and radius and M is the number of items inside the index.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "longitude", + "type": "double" + }, + { + "name": "latitude", + "type": "double" + }, + { + "name": "radius", + "type": "double" + }, + { + "name": "unit", + "type": "enum", + "enum": [ + "m", + "km", + "ft", + "mi" + ] + }, + { + "name": "withcoord", + "type": "enum", + "enum": [ + "WITHCOORD" + ], + "optional": true + }, + { + "name": "withdist", + "type": "enum", + "enum": [ + "WITHDIST" + ], + "optional": true + }, + { + "name": "withhash", + "type": "enum", + "enum": [ + "WITHHASH" + ], + "optional": true + }, + { + "command": "COUNT", + "name": "count", + "type": "integer", + "optional": true + }, + { + "name": "order", + "type": "enum", + "enum": [ + "ASC", + "DESC" + ], + "optional": true + }, + { + "command": "STORE", + "name": "key", + "type": "key", + "optional": true + }, + { + "command": "STOREDIST", + "name": "key", + "type": "key", + "optional": true + } + ], + "since": "3.2.0", + "group": "geo" + }, + "GEORADIUSBYMEMBER": { + "summary": "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member", + "complexity": "O(N+log(M)) where N is the number of elements inside the bounding box of the circular area delimited by center and radius and M is the number of items inside the index.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "member", + "type": "string" + }, + { + "name": "radius", + "type": "double" + }, + { + "name": "unit", + "type": "enum", + "enum": [ + "m", + "km", + "ft", + "mi" + ] + }, + { + "name": "withcoord", + "type": "enum", + "enum": [ + "WITHCOORD" + ], + "optional": true + }, + { + "name": "withdist", + "type": "enum", + "enum": [ + "WITHDIST" + ], + "optional": true + }, + { + "name": "withhash", + "type": "enum", + "enum": [ + "WITHHASH" + ], + "optional": true + }, + { + "command": "COUNT", + "name": "count", + "type": "integer", + "optional": true + }, + { + "name": "order", + "type": "enum", + "enum": [ + "ASC", + "DESC" + ], + "optional": true + }, + { + "command": "STORE", + "name": "key", + "type": "key", + "optional": true + }, + { + "command": "STOREDIST", + "name": "key", + "type": "key", + "optional": true + } + ], + "since": "3.2.0", + "group": "geo" + }, + "GET": { + "summary": "Get the value of a key", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "1.0.0", + "group": "string" + }, + "GETBIT": { + "summary": "Returns the bit value at offset in the string value stored at key", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "offset", + "type": "integer" + } + ], + "since": "2.2.0", + "group": "string" + }, + "GETRANGE": { + "summary": "Get a substring of the string stored at a key", + "complexity": "O(N) where N is the length of the returned string. The complexity is ultimately determined by the returned length, but because creating a substring from an existing string is very cheap, it can be considered O(1) for small strings.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "start", + "type": "integer" + }, + { + "name": "end", + "type": "integer" + } + ], + "since": "2.4.0", + "group": "string" + }, + "GETSET": { + "summary": "Set the string value of a key and return its old value", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "value", + "type": "string" + } + ], + "since": "1.0.0", + "group": "string" + }, + "HDEL": { + "summary": "Delete one or more hash fields", + "complexity": "O(N) where N is the number of fields to be removed.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ], + "since": "2.0.0", + "group": "hash" + }, + "HELLO": { + "summary": "switch Redis protocol", + "complexity": "O(1)", + "arguments": [ + { + "name": "protover", + "type": "integer" + }, + { + "command": "AUTH", + "name": [ + "username", + "password" + ], + "type": [ + "string", + "string" + ], + "optional": true + }, + { + "command": "SETNAME", + "name": "clientname", + "type": "string", + "optional": true + } + ], + "since": "6.0.0", + "group": "connection" + }, + "HEXISTS": { + "summary": "Determine if a hash field exists", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "field", + "type": "string" + } + ], + "since": "2.0.0", + "group": "hash" + }, + "HGET": { + "summary": "Get the value of a hash field", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "field", + "type": "string" + } + ], + "since": "2.0.0", + "group": "hash" + }, + "HGETALL": { + "summary": "Get all the fields and values in a hash", + "complexity": "O(N) where N is the size of the hash.", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "2.0.0", + "group": "hash" + }, + "HINCRBY": { + "summary": "Increment the integer value of a hash field by the given number", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "field", + "type": "string" + }, + { + "name": "increment", + "type": "integer" + } + ], + "since": "2.0.0", + "group": "hash" + }, + "HINCRBYFLOAT": { + "summary": "Increment the float value of a hash field by the given amount", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "field", + "type": "string" + }, + { + "name": "increment", + "type": "double" + } + ], + "since": "2.6.0", + "group": "hash" + }, + "HKEYS": { + "summary": "Get all the fields in a hash", + "complexity": "O(N) where N is the size of the hash.", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "2.0.0", + "group": "hash" + }, + "HLEN": { + "summary": "Get the number of fields in a hash", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "2.0.0", + "group": "hash" + }, + "HMGET": { + "summary": "Get the values of all the given hash fields", + "complexity": "O(N) where N is the number of fields being requested.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ], + "since": "2.0.0", + "group": "hash" + }, + "HMSET": { + "summary": "Set multiple hash fields to multiple values", + "complexity": "O(N) where N is the number of fields being set.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": [ + "field", + "value" + ], + "type": [ + "string", + "string" + ], + "multiple": true + } + ], + "since": "2.0.0", + "group": "hash" + }, + "HSET": { + "summary": "Set the string value of a hash field", + "complexity": "O(1) for each field/value pair added, so O(N) to add N field/value pairs when the command is called with multiple field/value pairs.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": [ + "field", + "value" + ], + "type": [ + "string", + "string" + ], + "multiple": true + } + ], + "since": "2.0.0", + "group": "hash" + }, + "HSETNX": { + "summary": "Set the value of a hash field, only if the field does not exist", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "field", + "type": "string" + }, + { + "name": "value", + "type": "string" + } + ], + "since": "2.0.0", + "group": "hash" + }, + "HSTRLEN": { + "summary": "Get the length of the value of a hash field", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "field", + "type": "string" + } + ], + "since": "3.2.0", + "group": "hash" + }, + "HVALS": { + "summary": "Get all the values in a hash", + "complexity": "O(N) where N is the size of the hash.", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "2.0.0", + "group": "hash" + }, + "INCR": { + "summary": "Increment the integer value of a key by one", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "1.0.0", + "group": "string" + }, + "INCRBY": { + "summary": "Increment the integer value of a key by the given amount", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "increment", + "type": "integer" + } + ], + "since": "1.0.0", + "group": "string" + }, + "INCRBYFLOAT": { + "summary": "Increment the float value of a key by the given amount", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "increment", + "type": "double" + } + ], + "since": "2.6.0", + "group": "string" + }, + "INFO": { + "summary": "Get information and statistics about the server", + "arguments": [ + { + "name": "section", + "type": "string", + "optional": true + } + ], + "since": "1.0.0", + "group": "server" + }, + "LOLWUT": { + "summary": "Display some computer art and the Redis version", + "arguments": [ + { + "command": "VERSION", + "name": "version", + "type": "integer", + "optional": true + } + ], + "since": "5.0.0", + "group": "server" + }, + "KEYS": { + "summary": "Find all keys matching the given pattern", + "complexity": "O(N) with N being the number of keys in the database, under the assumption that the key names in the database and the given pattern have limited length.", + "arguments": [ + { + "name": "pattern", + "type": "pattern" + } + ], + "since": "1.0.0", + "group": "generic" + }, + "LASTSAVE": { + "summary": "Get the UNIX time stamp of the last successful save to disk", + "since": "1.0.0", + "group": "server" + }, + "LINDEX": { + "summary": "Get an element from a list by its index", + "complexity": "O(N) where N is the number of elements to traverse to get to the element at index. This makes asking for the first or the last element of the list O(1).", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "index", + "type": "integer" + } + ], + "since": "1.0.0", + "group": "list" + }, + "LINSERT": { + "summary": "Insert an element before or after another element in a list", + "complexity": "O(N) where N is the number of elements to traverse before seeing the value pivot. This means that inserting somewhere on the left end on the list (head) can be considered O(1) and inserting somewhere on the right end (tail) is O(N).", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "where", + "type": "enum", + "enum": [ + "BEFORE", + "AFTER" + ] + }, + { + "name": "pivot", + "type": "string" + }, + { + "name": "element", + "type": "string" + } + ], + "since": "2.2.0", + "group": "list" + }, + "LLEN": { + "summary": "Get the length of a list", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "1.0.0", + "group": "list" + }, + "LPOP": { + "summary": "Remove and get the first element in a list", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "1.0.0", + "group": "list" + }, + "LPUSH": { + "summary": "Prepend one or multiple elements to a list", + "complexity": "O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "element", + "type": "string", + "multiple": true + } + ], + "since": "1.0.0", + "group": "list" + }, + "LPUSHX": { + "summary": "Prepend an element to a list, only if the list exists", + "complexity": "O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "element", + "type": "string", + "multiple": true + } + ], + "since": "2.2.0", + "group": "list" + }, + "LRANGE": { + "summary": "Get a range of elements from a list", + "complexity": "O(S+N) where S is the distance of start offset from HEAD for small lists, from nearest end (HEAD or TAIL) for large lists; and N is the number of elements in the specified range.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "start", + "type": "integer" + }, + { + "name": "stop", + "type": "integer" + } + ], + "since": "1.0.0", + "group": "list" + }, + "LREM": { + "summary": "Remove elements from a list", + "complexity": "O(N+M) where N is the length of the list and M is the number of elements removed.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "count", + "type": "integer" + }, + { + "name": "element", + "type": "string" + } + ], + "since": "1.0.0", + "group": "list" + }, + "LSET": { + "summary": "Set the value of an element in a list by its index", + "complexity": "O(N) where N is the length of the list. Setting either the first or the last element of the list is O(1).", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "index", + "type": "integer" + }, + { + "name": "element", + "type": "string" + } + ], + "since": "1.0.0", + "group": "list" + }, + "LTRIM": { + "summary": "Trim a list to the specified range", + "complexity": "O(N) where N is the number of elements to be removed by the operation.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "start", + "type": "integer" + }, + { + "name": "stop", + "type": "integer" + } + ], + "since": "1.0.0", + "group": "list" + }, + "MEMORY DOCTOR": { + "summary": "Outputs memory problems report", + "since": "4.0.0", + "group": "server" + }, + "MEMORY HELP": { + "summary": "Show helpful text about the different subcommands", + "since": "4.0.0", + "group": "server" + }, + "MEMORY MALLOC-STATS": { + "summary": "Show allocator internal stats", + "since": "4.0.0", + "group": "server" + }, + "MEMORY PURGE": { + "summary": "Ask the allocator to release memory", + "since": "4.0.0", + "group": "server" + }, + "MEMORY STATS": { + "summary": "Show memory usage details", + "since": "4.0.0", + "group": "server" + }, + "MEMORY USAGE": { + "summary": "Estimate the memory usage of a key", + "complexity": "O(N) where N is the number of samples.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "command": "SAMPLES", + "name": "count", + "type": "integer", + "optional": true + } + ], + "since": "4.0.0", + "group": "server" + }, + "MGET": { + "summary": "Get the values of all the given keys", + "complexity": "O(N) where N is the number of keys to retrieve.", + "arguments": [ + { + "name": "key", + "type": "key", + "multiple": true + } + ], + "since": "1.0.0", + "group": "string" + }, + "MIGRATE": { + "summary": "Atomically transfer a key from a Redis instance to another one.", + "complexity": "This command actually executes a DUMP+DEL in the source instance, and a RESTORE in the target instance. See the pages of these commands for time complexity. Also an O(N) data transfer between the two instances is performed.", + "arguments": [ + { + "name": "host", + "type": "string" + }, + { + "name": "port", + "type": "string" + }, + { + "name": "key", + "type": "enum", + "enum": [ + "key", + "\"\"" + ] + }, + { + "name": "destination-db", + "type": "integer" + }, + { + "name": "timeout", + "type": "integer" + }, + { + "name": "copy", + "type": "enum", + "enum": [ + "COPY" + ], + "optional": true + }, + { + "name": "replace", + "type": "enum", + "enum": [ + "REPLACE" + ], + "optional": true + }, + { + "command": "AUTH", + "name": "password", + "type": "string", + "optional": true + }, + { + "name": "key", + "command": "KEYS", + "type": "key", + "variadic": true, + "optional": true + } + ], + "since": "2.6.0", + "group": "generic" + }, + "MODULE LIST": { + "summary": "List all modules loaded by the server", + "complexity": "O(N) where N is the number of loaded modules.", + "since": "4.0.0", + "group": "server" + }, + "MODULE LOAD": { + "summary": "Load a module", + "complexity": "O(1)", + "arguments": [ + { + "name": "path", + "type": "string" + }, + { + "name": "arg", + "type": "string", + "variadic": true, + "optional": true + } + ], + "since": "4.0.0", + "group": "server" + }, + "MODULE UNLOAD": { + "summary": "Unload a module", + "complexity": "O(1)", + "arguments": [ + { + "name": "name", + "type": "string" + } + ], + "since": "4.0.0", + "group": "server" + }, + "MONITOR": { + "summary": "Listen for all requests received by the server in real time", + "since": "1.0.0", + "group": "server" + }, + "MOVE": { + "summary": "Move a key to another database", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "db", + "type": "integer" + } + ], + "since": "1.0.0", + "group": "generic" + }, + "MSET": { + "summary": "Set multiple keys to multiple values", + "complexity": "O(N) where N is the number of keys to set.", + "arguments": [ + { + "name": [ + "key", + "value" + ], + "type": [ + "key", + "string" + ], + "multiple": true + } + ], + "since": "1.0.1", + "group": "string" + }, + "MSETNX": { + "summary": "Set multiple keys to multiple values, only if none of the keys exist", + "complexity": "O(N) where N is the number of keys to set.", + "arguments": [ + { + "name": [ + "key", + "value" + ], + "type": [ + "key", + "string" + ], + "multiple": true + } + ], + "since": "1.0.1", + "group": "string" + }, + "MULTI": { + "summary": "Mark the start of a transaction block", + "since": "1.2.0", + "group": "transactions" + }, + "OBJECT": { + "summary": "Inspect the internals of Redis objects", + "complexity": "O(1) for all the currently implemented subcommands.", + "since": "2.2.3", + "group": "generic", + "arguments": [ + { + "name": "subcommand", + "type": "string" + }, + { + "name": "arguments", + "type": "string", + "optional": true, + "multiple": true + } + ] + }, + "PERSIST": { + "summary": "Remove the expiration from a key", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "2.2.0", + "group": "generic" + }, + "PEXPIRE": { + "summary": "Set a key's time to live in milliseconds", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "milliseconds", + "type": "integer" + } + ], + "since": "2.6.0", + "group": "generic" + }, + "PEXPIREAT": { + "summary": "Set the expiration for a key as a UNIX timestamp specified in milliseconds", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "milliseconds-timestamp", + "type": "posix time" + } + ], + "since": "2.6.0", + "group": "generic" + }, + "PFADD": { + "summary": "Adds the specified elements to the specified HyperLogLog.", + "complexity": "O(1) to add every element.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "element", + "type": "string", + "multiple": true + } + ], + "since": "2.8.9", + "group": "hyperloglog" + }, + "PFCOUNT": { + "summary": "Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s).", + "complexity": "O(1) with a very small average constant time when called with a single key. O(N) with N being the number of keys, and much bigger constant times, when called with multiple keys.", + "arguments": [ + { + "name": "key", + "type": "key", + "multiple": true + } + ], + "since": "2.8.9", + "group": "hyperloglog" + }, + "PFMERGE": { + "summary": "Merge N different HyperLogLogs into a single one.", + "complexity": "O(N) to merge N HyperLogLogs, but with high constant times.", + "arguments": [ + { + "name": "destkey", + "type": "key" + }, + { + "name": "sourcekey", + "type": "key", + "multiple": true + } + ], + "since": "2.8.9", + "group": "hyperloglog" + }, + "PING": { + "summary": "Ping the server", + "arguments": [ + { + "name": "message", + "type": "string", + "optional": true + } + ], + "since": "1.0.0", + "group": "connection" + }, + "PSETEX": { + "summary": "Set the value and expiration in milliseconds of a key", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "milliseconds", + "type": "integer" + }, + { + "name": "value", + "type": "string" + } + ], + "since": "2.6.0", + "group": "string" + }, + "PSUBSCRIBE": { + "summary": "Listen for messages published to channels matching the given patterns", + "complexity": "O(N) where N is the number of patterns the client is already subscribed to.", + "arguments": [ + { + "name": [ + "pattern" + ], + "type": [ + "pattern" + ], + "multiple": true + } + ], + "since": "2.0.0", + "group": "pubsub" + }, + "PUBSUB": { + "summary": "Inspect the state of the Pub/Sub subsystem", + "complexity": "O(N) for the CHANNELS subcommand, where N is the number of active channels, and assuming constant time pattern matching (relatively short channels and patterns). O(N) for the NUMSUB subcommand, where N is the number of requested channels. O(1) for the NUMPAT subcommand.", + "arguments": [ + { + "name": "subcommand", + "type": "string" + }, + { + "name": "argument", + "type": "string", + "optional": true, + "multiple": true + } + ], + "since": "2.8.0", + "group": "pubsub" + }, + "PTTL": { + "summary": "Get the time to live for a key in milliseconds", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "2.6.0", + "group": "generic" + }, + "PUBLISH": { + "summary": "Post a message to a channel", + "complexity": "O(N+M) where N is the number of clients subscribed to the receiving channel and M is the total number of subscribed patterns (by any client).", + "arguments": [ + { + "name": "channel", + "type": "string" + }, + { + "name": "message", + "type": "string" + } + ], + "since": "2.0.0", + "group": "pubsub" + }, + "PUNSUBSCRIBE": { + "summary": "Stop listening for messages posted to channels matching the given patterns", + "complexity": "O(N+M) where N is the number of patterns the client is already subscribed and M is the number of total patterns subscribed in the system (by any client).", + "arguments": [ + { + "name": "pattern", + "type": "pattern", + "optional": true, + "multiple": true + } + ], + "since": "2.0.0", + "group": "pubsub" + }, + "QUIT": { + "summary": "Close the connection", + "since": "1.0.0", + "group": "connection" + }, + "RANDOMKEY": { + "summary": "Return a random key from the keyspace", + "complexity": "O(1)", + "since": "1.0.0", + "group": "generic" + }, + "READONLY": { + "summary": "Enables read queries for a connection to a cluster replica node", + "complexity": "O(1)", + "since": "3.0.0", + "group": "cluster" + }, + "READWRITE": { + "summary": "Disables read queries for a connection to a cluster replica node", + "complexity": "O(1)", + "since": "3.0.0", + "group": "cluster" + }, + "RENAME": { + "summary": "Rename a key", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "newkey", + "type": "key" + } + ], + "since": "1.0.0", + "group": "generic" + }, + "RENAMENX": { + "summary": "Rename a key, only if the new key does not exist", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "newkey", + "type": "key" + } + ], + "since": "1.0.0", + "group": "generic" + }, + "RESTORE": { + "summary": "Create a key using the provided serialized value, previously obtained using DUMP.", + "complexity": "O(1) to create the new key and additional O(N*M) to reconstruct the serialized value, where N is the number of Redis objects composing the value and M their average size. For small string values the time complexity is thus O(1)+O(1*M) where M is small, so simply O(1). However for sorted set values the complexity is O(N*M*log(N)) because inserting values into sorted sets is O(log(N)).", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "ttl", + "type": "integer" + }, + { + "name": "serialized-value", + "type": "string" + }, + { + "name": "replace", + "type": "enum", + "enum": [ + "REPLACE" + ], + "optional": true + }, + { + "name": "absttl", + "type": "enum", + "enum": [ + "ABSTTL" + ], + "optional": true + }, + { + "command": "IDLETIME", + "name": "seconds", + "type": "integer", + "optional": true + }, + { + "command": "FREQ", + "name": "frequency", + "type": "integer", + "optional": true + } + ], + "since": "2.6.0", + "group": "generic" + }, + "ROLE": { + "summary": "Return the role of the instance in the context of replication", + "since": "2.8.12", + "group": "server" + }, + "RPOP": { + "summary": "Remove and get the last element in a list", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "1.0.0", + "group": "list" + }, + "RPOPLPUSH": { + "summary": "Remove the last element in a list, prepend it to another list and return it", + "complexity": "O(1)", + "arguments": [ + { + "name": "source", + "type": "key" + }, + { + "name": "destination", + "type": "key" + } + ], + "since": "1.2.0", + "group": "list" + }, + "RPUSH": { + "summary": "Append one or multiple elements to a list", + "complexity": "O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "element", + "type": "string", + "multiple": true + } + ], + "since": "1.0.0", + "group": "list" + }, + "RPUSHX": { + "summary": "Append an element to a list, only if the list exists", + "complexity": "O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "element", + "type": "string", + "multiple": true + } + ], + "since": "2.2.0", + "group": "list" + }, + "SADD": { + "summary": "Add one or more members to a set", + "complexity": "O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "member", + "type": "string", + "multiple": true + } + ], + "since": "1.0.0", + "group": "set" + }, + "SAVE": { + "summary": "Synchronously save the dataset to disk", + "since": "1.0.0", + "group": "server" + }, + "SCARD": { + "summary": "Get the number of members in a set", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "1.0.0", + "group": "set" + }, + "SCRIPT DEBUG": { + "summary": "Set the debug mode for executed scripts.", + "complexity": "O(1)", + "arguments": [ + { + "name": "mode", + "type": "enum", + "enum": [ + "YES", + "SYNC", + "NO" + ] + } + ], + "since": "3.2.0", + "group": "scripting" + }, + "SCRIPT EXISTS": { + "summary": "Check existence of scripts in the script cache.", + "complexity": "O(N) with N being the number of scripts to check (so checking a single script is an O(1) operation).", + "arguments": [ + { + "name": "sha1", + "type": "string", + "multiple": true + } + ], + "since": "2.6.0", + "group": "scripting" + }, + "SCRIPT FLUSH": { + "summary": "Remove all the scripts from the script cache.", + "complexity": "O(N) with N being the number of scripts in cache", + "since": "2.6.0", + "group": "scripting" + }, + "SCRIPT KILL": { + "summary": "Kill the script currently in execution.", + "complexity": "O(1)", + "since": "2.6.0", + "group": "scripting" + }, + "SCRIPT LOAD": { + "summary": "Load the specified Lua script into the script cache.", + "complexity": "O(N) with N being the length in bytes of the script body.", + "arguments": [ + { + "name": "script", + "type": "string" + } + ], + "since": "2.6.0", + "group": "scripting" + }, + "SDIFF": { + "summary": "Subtract multiple sets", + "complexity": "O(N) where N is the total number of elements in all given sets.", + "arguments": [ + { + "name": "key", + "type": "key", + "multiple": true + } + ], + "since": "1.0.0", + "group": "set" + }, + "SDIFFSTORE": { + "summary": "Subtract multiple sets and store the resulting set in a key", + "complexity": "O(N) where N is the total number of elements in all given sets.", + "arguments": [ + { + "name": "destination", + "type": "key" + }, + { + "name": "key", + "type": "key", + "multiple": true + } + ], + "since": "1.0.0", + "group": "set" + }, + "SELECT": { + "summary": "Change the selected database for the current connection", + "arguments": [ + { + "name": "index", + "type": "integer" + } + ], + "since": "1.0.0", + "group": "connection" + }, + "SET": { + "summary": "Set the string value of a key", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "value", + "type": "string" + }, + { + "name": "expiration", + "type": "enum", + "enum": [ + "EX seconds", + "PX milliseconds" + ], + "optional": true + }, + { + "name": "condition", + "type": "enum", + "enum": [ + "NX", + "XX" + ], + "optional": true + }, + { + "name": "keepttl", + "type": "enum", + "enum": [ + "KEEPTTL" + ], + "optional": true + } + ], + "since": "1.0.0", + "group": "string" + }, + "SETBIT": { + "summary": "Sets or clears the bit at offset in the string value stored at key", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "offset", + "type": "integer" + }, + { + "name": "value", + "type": "integer" + } + ], + "since": "2.2.0", + "group": "string" + }, + "SETEX": { + "summary": "Set the value and expiration of a key", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "seconds", + "type": "integer" + }, + { + "name": "value", + "type": "string" + } + ], + "since": "2.0.0", + "group": "string" + }, + "SETNX": { + "summary": "Set the value of a key, only if the key does not exist", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "value", + "type": "string" + } + ], + "since": "1.0.0", + "group": "string" + }, + "SETRANGE": { + "summary": "Overwrite part of a string at key starting at the specified offset", + "complexity": "O(1), not counting the time taken to copy the new string in place. Usually, this string is very small so the amortized complexity is O(1). Otherwise, complexity is O(M) with M being the length of the value argument.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "offset", + "type": "integer" + }, + { + "name": "value", + "type": "string" + } + ], + "since": "2.2.0", + "group": "string" + }, + "SHUTDOWN": { + "summary": "Synchronously save the dataset to disk and then shut down the server", + "arguments": [ + { + "name": "save-mode", + "type": "enum", + "enum": [ + "NOSAVE", + "SAVE" + ], + "optional": true + } + ], + "since": "1.0.0", + "group": "server" + }, + "SINTER": { + "summary": "Intersect multiple sets", + "complexity": "O(N*M) worst case where N is the cardinality of the smallest set and M is the number of sets.", + "arguments": [ + { + "name": "key", + "type": "key", + "multiple": true + } + ], + "since": "1.0.0", + "group": "set" + }, + "SINTERSTORE": { + "summary": "Intersect multiple sets and store the resulting set in a key", + "complexity": "O(N*M) worst case where N is the cardinality of the smallest set and M is the number of sets.", + "arguments": [ + { + "name": "destination", + "type": "key" + }, + { + "name": "key", + "type": "key", + "multiple": true + } + ], + "since": "1.0.0", + "group": "set" + }, + "SISMEMBER": { + "summary": "Determine if a given value is a member of a set", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "member", + "type": "string" + } + ], + "since": "1.0.0", + "group": "set" + }, + "SLAVEOF": { + "summary": "Make the server a replica of another instance, or promote it as master. Deprecated starting with Redis 5. Use REPLICAOF instead.", + "arguments": [ + { + "name": "host", + "type": "string" + }, + { + "name": "port", + "type": "string" + } + ], + "since": "1.0.0", + "group": "server" + }, + "REPLICAOF": { + "summary": "Make the server a replica of another instance, or promote it as master.", + "arguments": [ + { + "name": "host", + "type": "string" + }, + { + "name": "port", + "type": "string" + } + ], + "since": "5.0.0", + "group": "server" + }, + "SLOWLOG": { + "summary": "Manages the Redis slow queries log", + "arguments": [ + { + "name": "subcommand", + "type": "string" + }, + { + "name": "argument", + "type": "string", + "optional": true + } + ], + "since": "2.2.12", + "group": "server" + }, + "SMEMBERS": { + "summary": "Get all the members in a set", + "complexity": "O(N) where N is the set cardinality.", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "1.0.0", + "group": "set" + }, + "SMOVE": { + "summary": "Move a member from one set to another", + "complexity": "O(1)", + "arguments": [ + { + "name": "source", + "type": "key" + }, + { + "name": "destination", + "type": "key" + }, + { + "name": "member", + "type": "string" + } + ], + "since": "1.0.0", + "group": "set" + }, + "SORT": { + "summary": "Sort the elements in a list, set or sorted set", + "complexity": "O(N+M*log(M)) where N is the number of elements in the list or set to sort, and M the number of returned elements. When the elements are not sorted, complexity is currently O(N) as there is a copy step that will be avoided in next releases.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "command": "BY", + "name": "pattern", + "type": "pattern", + "optional": true + }, + { + "command": "LIMIT", + "name": [ + "offset", + "count" + ], + "type": [ + "integer", + "integer" + ], + "optional": true + }, + { + "command": "GET", + "name": "pattern", + "type": "string", + "optional": true, + "multiple": true + }, + { + "name": "order", + "type": "enum", + "enum": [ + "ASC", + "DESC" + ], + "optional": true + }, + { + "name": "sorting", + "type": "enum", + "enum": [ + "ALPHA" + ], + "optional": true + }, + { + "command": "STORE", + "name": "destination", + "type": "key", + "optional": true + } + ], + "since": "1.0.0", + "group": "generic" + }, + "SPOP": { + "summary": "Remove and return one or multiple random members from a set", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "count", + "type": "integer", + "optional": true + } + ], + "since": "1.0.0", + "group": "set" + }, + "SRANDMEMBER": { + "summary": "Get one or multiple random members from a set", + "complexity": "Without the count argument O(1), otherwise O(N) where N is the absolute value of the passed count.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "count", + "type": "integer", + "optional": true + } + ], + "since": "1.0.0", + "group": "set" + }, + "SREM": { + "summary": "Remove one or more members from a set", + "complexity": "O(N) where N is the number of members to be removed.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "member", + "type": "string", + "multiple": true + } + ], + "since": "1.0.0", + "group": "set" + }, + "STRALGO": { + "summary": "Run algorithms (currently LCS) against strings", + "complexity": "For LCS O(strlen(s1)*strlen(s2))", + "arguments": [ + { + "name": "algorithm", + "type": "enum", + "enum": [ + "LCS" + ] + }, + { + "name": "algo-specific-argument", + "type": "string", + "multiple": true + } + ], + "since": "6.0.0", + "group": "string" + }, + "STRLEN": { + "summary": "Get the length of the value stored in a key", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "2.2.0", + "group": "string" + }, + "SUBSCRIBE": { + "summary": "Listen for messages published to the given channels", + "complexity": "O(N) where N is the number of channels to subscribe to.", + "arguments": [ + { + "name": "channel", + "type": "string", + "multiple": true + } + ], + "since": "2.0.0", + "group": "pubsub" + }, + "SUNION": { + "summary": "Add multiple sets", + "complexity": "O(N) where N is the total number of elements in all given sets.", + "arguments": [ + { + "name": "key", + "type": "key", + "multiple": true + } + ], + "since": "1.0.0", + "group": "set" + }, + "SUNIONSTORE": { + "summary": "Add multiple sets and store the resulting set in a key", + "complexity": "O(N) where N is the total number of elements in all given sets.", + "arguments": [ + { + "name": "destination", + "type": "key" + }, + { + "name": "key", + "type": "key", + "multiple": true + } + ], + "since": "1.0.0", + "group": "set" + }, + "SWAPDB": { + "summary": "Swaps two Redis databases", + "arguments": [ + { + "name": "index1", + "type": "integer" + }, + { + "name": "index2", + "type": "integer" + } + ], + "since": "4.0.0", + "group": "server" + }, + "SYNC": { + "summary": "Internal command used for replication", + "since": "1.0.0", + "group": "server" + }, + "PSYNC": { + "summary": "Internal command used for replication", + "arguments": [ + { + "name": "replicationid", + "type": "integer" + }, + { + "name": "offset", + "type": "integer" + } + ], + "since": "2.8.0", + "group": "server" + }, + "TIME": { + "summary": "Return the current server time", + "complexity": "O(1)", + "since": "2.6.0", + "group": "server" + }, + "TOUCH": { + "summary": "Alters the last access time of a key(s). Returns the number of existing keys specified.", + "complexity": "O(N) where N is the number of keys that will be touched.", + "arguments": [ + { + "name": "key", + "type": "key", + "multiple": true + } + ], + "since": "3.2.1", + "group": "generic" + }, + "TTL": { + "summary": "Get the time to live for a key", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "1.0.0", + "group": "generic" + }, + "TYPE": { + "summary": "Determine the type stored at key", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "1.0.0", + "group": "generic" + }, + "UNSUBSCRIBE": { + "summary": "Stop listening for messages posted to the given channels", + "complexity": "O(N) where N is the number of clients already subscribed to a channel.", + "arguments": [ + { + "name": "channel", + "type": "string", + "optional": true, + "multiple": true + } + ], + "since": "2.0.0", + "group": "pubsub" + }, + "UNLINK": { + "summary": "Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking.", + "complexity": "O(1) for each key removed regardless of its size. Then the command does O(N) work in a different thread in order to reclaim memory, where N is the number of allocations the deleted objects where composed of.", + "arguments": [ + { + "name": "key", + "type": "key", + "multiple": true + } + ], + "since": "4.0.0", + "group": "generic" + }, + "UNWATCH": { + "summary": "Forget about all watched keys", + "complexity": "O(1)", + "since": "2.2.0", + "group": "transactions" + }, + "WAIT": { + "summary": "Wait for the synchronous replication of all the write commands sent in the context of the current connection", + "complexity": "O(1)", + "arguments": [ + { + "name": "numreplicas", + "type": "integer" + }, + { + "name": "timeout", + "type": "integer" + } + ], + "since": "3.0.0", + "group": "generic" + }, + "WATCH": { + "summary": "Watch the given keys to determine execution of the MULTI/EXEC block", + "complexity": "O(1) for every key.", + "arguments": [ + { + "name": "key", + "type": "key", + "multiple": true + } + ], + "since": "2.2.0", + "group": "transactions" + }, + "ZADD": { + "summary": "Add one or more members to a sorted set, or update its score if it already exists", + "complexity": "O(log(N)) for each item added, where N is the number of elements in the sorted set.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "condition", + "type": "enum", + "enum": [ + "NX", + "XX" + ], + "optional": true + }, + { + "name": "change", + "type": "enum", + "enum": [ + "CH" + ], + "optional": true + }, + { + "name": "increment", + "type": "enum", + "enum": [ + "INCR" + ], + "optional": true + }, + { + "name": [ + "score", + "member" + ], + "type": [ + "double", + "string" + ], + "multiple": true + } + ], + "since": "1.2.0", + "group": "sorted_set" + }, + "ZCARD": { + "summary": "Get the number of members in a sorted set", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "1.2.0", + "group": "sorted_set" + }, + "ZCOUNT": { + "summary": "Count the members in a sorted set with scores within the given values", + "complexity": "O(log(N)) with N being the number of elements in the sorted set.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "min", + "type": "double" + }, + { + "name": "max", + "type": "double" + } + ], + "since": "2.0.0", + "group": "sorted_set" + }, + "ZINCRBY": { + "summary": "Increment the score of a member in a sorted set", + "complexity": "O(log(N)) where N is the number of elements in the sorted set.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "increment", + "type": "integer" + }, + { + "name": "member", + "type": "string" + } + ], + "since": "1.2.0", + "group": "sorted_set" + }, + "ZINTERSTORE": { + "summary": "Intersect multiple sorted sets and store the resulting sorted set in a new key", + "complexity": "O(N*K)+O(M*log(M)) worst case with N being the smallest input sorted set, K being the number of input sorted sets and M being the number of elements in the resulting sorted set.", + "arguments": [ + { + "name": "destination", + "type": "key" + }, + { + "name": "numkeys", + "type": "integer" + }, + { + "name": "key", + "type": "key", + "multiple": true + }, + { + "command": "WEIGHTS", + "name": "weight", + "type": "integer", + "variadic": true, + "optional": true + }, + { + "command": "AGGREGATE", + "name": "aggregate", + "type": "enum", + "enum": [ + "SUM", + "MIN", + "MAX" + ], + "optional": true + } + ], + "since": "2.0.0", + "group": "sorted_set" + }, + "ZLEXCOUNT": { + "summary": "Count the number of members in a sorted set between a given lexicographical range", + "complexity": "O(log(N)) with N being the number of elements in the sorted set.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "min", + "type": "string" + }, + { + "name": "max", + "type": "string" + } + ], + "since": "2.8.9", + "group": "sorted_set" + }, + "ZPOPMAX": { + "summary": "Remove and return members with the highest scores in a sorted set", + "complexity": "O(log(N)*M) with N being the number of elements in the sorted set, and M being the number of elements popped.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "count", + "type": "integer", + "optional": true + } + ], + "since": "5.0.0", + "group": "sorted_set" + }, + "ZPOPMIN": { + "summary": "Remove and return members with the lowest scores in a sorted set", + "complexity": "O(log(N)*M) with N being the number of elements in the sorted set, and M being the number of elements popped.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "count", + "type": "integer", + "optional": true + } + ], + "since": "5.0.0", + "group": "sorted_set" + }, + "ZRANGE": { + "summary": "Return a range of members in a sorted set, by index", + "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements returned.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "start", + "type": "integer" + }, + { + "name": "stop", + "type": "integer" + }, + { + "name": "withscores", + "type": "enum", + "enum": [ + "WITHSCORES" + ], + "optional": true + } + ], + "since": "1.2.0", + "group": "sorted_set" + }, + "ZRANGEBYLEX": { + "summary": "Return a range of members in a sorted set, by lexicographical range", + "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "min", + "type": "string" + }, + { + "name": "max", + "type": "string" + }, + { + "command": "LIMIT", + "name": [ + "offset", + "count" + ], + "type": [ + "integer", + "integer" + ], + "optional": true + } + ], + "since": "2.8.9", + "group": "sorted_set" + }, + "ZREVRANGEBYLEX": { + "summary": "Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings.", + "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "max", + "type": "string" + }, + { + "name": "min", + "type": "string" + }, + { + "command": "LIMIT", + "name": [ + "offset", + "count" + ], + "type": [ + "integer", + "integer" + ], + "optional": true + } + ], + "since": "2.8.9", + "group": "sorted_set" + }, + "ZRANGEBYSCORE": { + "summary": "Return a range of members in a sorted set, by score", + "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "min", + "type": "double" + }, + { + "name": "max", + "type": "double" + }, + { + "name": "withscores", + "type": "enum", + "enum": [ + "WITHSCORES" + ], + "optional": true + }, + { + "command": "LIMIT", + "name": [ + "offset", + "count" + ], + "type": [ + "integer", + "integer" + ], + "optional": true + } + ], + "since": "1.0.5", + "group": "sorted_set" + }, + "ZRANK": { + "summary": "Determine the index of a member in a sorted set", + "complexity": "O(log(N))", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "member", + "type": "string" + } + ], + "since": "2.0.0", + "group": "sorted_set" + }, + "ZREM": { + "summary": "Remove one or more members from a sorted set", + "complexity": "O(M*log(N)) with N being the number of elements in the sorted set and M the number of elements to be removed.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "member", + "type": "string", + "multiple": true + } + ], + "since": "1.2.0", + "group": "sorted_set" + }, + "ZREMRANGEBYLEX": { + "summary": "Remove all members in a sorted set between the given lexicographical range", + "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements removed by the operation.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "min", + "type": "string" + }, + { + "name": "max", + "type": "string" + } + ], + "since": "2.8.9", + "group": "sorted_set" + }, + "ZREMRANGEBYRANK": { + "summary": "Remove all members in a sorted set within the given indexes", + "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements removed by the operation.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "start", + "type": "integer" + }, + { + "name": "stop", + "type": "integer" + } + ], + "since": "2.0.0", + "group": "sorted_set" + }, + "ZREMRANGEBYSCORE": { + "summary": "Remove all members in a sorted set within the given scores", + "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements removed by the operation.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "min", + "type": "double" + }, + { + "name": "max", + "type": "double" + } + ], + "since": "1.2.0", + "group": "sorted_set" + }, + "ZREVRANGE": { + "summary": "Return a range of members in a sorted set, by index, with scores ordered from high to low", + "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements returned.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "start", + "type": "integer" + }, + { + "name": "stop", + "type": "integer" + }, + { + "name": "withscores", + "type": "enum", + "enum": [ + "WITHSCORES" + ], + "optional": true + } + ], + "since": "1.2.0", + "group": "sorted_set" + }, + "ZREVRANGEBYSCORE": { + "summary": "Return a range of members in a sorted set, by score, with scores ordered from high to low", + "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "max", + "type": "double" + }, + { + "name": "min", + "type": "double" + }, + { + "name": "withscores", + "type": "enum", + "enum": [ + "WITHSCORES" + ], + "optional": true + }, + { + "command": "LIMIT", + "name": [ + "offset", + "count" + ], + "type": [ + "integer", + "integer" + ], + "optional": true + } + ], + "since": "2.2.0", + "group": "sorted_set" + }, + "ZREVRANK": { + "summary": "Determine the index of a member in a sorted set, with scores ordered from high to low", + "complexity": "O(log(N))", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "member", + "type": "string" + } + ], + "since": "2.0.0", + "group": "sorted_set" + }, + "ZSCORE": { + "summary": "Get the score associated with the given member in a sorted set", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "member", + "type": "string" + } + ], + "since": "1.2.0", + "group": "sorted_set" + }, + "ZUNIONSTORE": { + "summary": "Add multiple sorted sets and store the resulting sorted set in a new key", + "complexity": "O(N)+O(M log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set.", + "arguments": [ + { + "name": "destination", + "type": "key" + }, + { + "name": "numkeys", + "type": "integer" + }, + { + "name": "key", + "type": "key", + "multiple": true + }, + { + "command": "WEIGHTS", + "name": "weight", + "type": "integer", + "variadic": true, + "optional": true + }, + { + "command": "AGGREGATE", + "name": "aggregate", + "type": "enum", + "enum": [ + "SUM", + "MIN", + "MAX" + ], + "optional": true + } + ], + "since": "2.0.0", + "group": "sorted_set" + }, + "SCAN": { + "summary": "Incrementally iterate the keys space", + "complexity": "O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.", + "arguments": [ + { + "name": "cursor", + "type": "integer" + }, + { + "command": "MATCH", + "name": "pattern", + "type": "pattern", + "optional": true + }, + { + "command": "COUNT", + "name": "count", + "type": "integer", + "optional": true + }, + { + "command": "TYPE", + "name": "type", + "type": "string", + "optional": true + } + ], + "since": "2.8.0", + "group": "generic" + }, + "SSCAN": { + "summary": "Incrementally iterate Set elements", + "complexity": "O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection..", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "cursor", + "type": "integer" + }, + { + "command": "MATCH", + "name": "pattern", + "type": "pattern", + "optional": true + }, + { + "command": "COUNT", + "name": "count", + "type": "integer", + "optional": true + } + ], + "since": "2.8.0", + "group": "set" + }, + "HSCAN": { + "summary": "Incrementally iterate hash fields and associated values", + "complexity": "O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection..", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "cursor", + "type": "integer" + }, + { + "command": "MATCH", + "name": "pattern", + "type": "pattern", + "optional": true + }, + { + "command": "COUNT", + "name": "count", + "type": "integer", + "optional": true + } + ], + "since": "2.8.0", + "group": "hash" + }, + "ZSCAN": { + "summary": "Incrementally iterate sorted sets elements and associated scores", + "complexity": "O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection..", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "cursor", + "type": "integer" + }, + { + "command": "MATCH", + "name": "pattern", + "type": "pattern", + "optional": true + }, + { + "command": "COUNT", + "name": "count", + "type": "integer", + "optional": true + } + ], + "since": "2.8.0", + "group": "sorted_set" + }, + "XINFO": { + "summary": "Get information on streams and consumer groups", + "complexity": "O(N) with N being the number of returned items for the subcommands CONSUMERS and GROUPS. The STREAM subcommand is O(log N) with N being the number of items in the stream.", + "arguments": [ + { + "command": "CONSUMERS", + "name": [ + "key", + "groupname" + ], + "type": [ + "key", + "string" + ], + "optional": true + }, + { + "command": "GROUPS", + "name": "key", + "type": "key", + "optional": true + }, + { + "command": "STREAM", + "name": "key", + "type": "key", + "optional": true + }, + { + "name": "help", + "type": "enum", + "enum": [ + "HELP" + ], + "optional": true + } + ], + "since": "5.0.0", + "group": "stream" + }, + "XADD": { + "summary": "Appends a new entry to a stream", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "ID", + "type": "string" + }, + { + "name": [ + "field", + "value" + ], + "type": [ + "string", + "string" + ], + "multiple": true + } + ], + "since": "5.0.0", + "group": "stream" + }, + "XTRIM": { + "summary": "Trims the stream to (approximately if '~' is passed) a certain size", + "complexity": "O(N), with N being the number of evicted entries. Constant times are very small however, since entries are organized in macro nodes containing multiple entries that can be released with a single deallocation.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "strategy", + "type": "enum", + "enum": [ + "MAXLEN" + ] + }, + { + "name": "approx", + "type": "enum", + "enum": [ + "~" + ], + "optional": true + }, + { + "name": "count", + "type": "integer" + } + ], + "since": "5.0.0", + "group": "stream" + }, + "XDEL": { + "summary": "Removes the specified entries from the stream. Returns the number of items actually deleted, that may be different from the number of IDs passed in case certain IDs do not exist.", + "complexity": "O(1) for each single item to delete in the stream, regardless of the stream size.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "ID", + "type": "string", + "multiple": true + } + ], + "since": "5.0.0", + "group": "stream" + }, + "XRANGE": { + "summary": "Return a range of elements in a stream, with IDs matching the specified IDs interval", + "complexity": "O(N) with N being the number of elements being returned. If N is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(1).", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "start", + "type": "string" + }, + { + "name": "end", + "type": "string" + }, + { + "command": "COUNT", + "name": "count", + "type": "integer", + "optional": true + } + ], + "since": "5.0.0", + "group": "stream" + }, + "XREVRANGE": { + "summary": "Return a range of elements in a stream, with IDs matching the specified IDs interval, in reverse order (from greater to smaller IDs) compared to XRANGE", + "complexity": "O(N) with N being the number of elements returned. If N is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(1).", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "end", + "type": "string" + }, + { + "name": "start", + "type": "string" + }, + { + "command": "COUNT", + "name": "count", + "type": "integer", + "optional": true + } + ], + "since": "5.0.0", + "group": "stream" + }, + "XLEN": { + "summary": "Return the number of entires in a stream", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "key" + } + ], + "since": "5.0.0", + "group": "stream" + }, + "XREAD": { + "summary": "Return never seen elements in multiple streams, with IDs greater than the ones reported by the caller for each stream. Can block.", + "complexity": "For each stream mentioned: O(N) with N being the number of elements being returned, it means that XREAD-ing with a fixed COUNT is O(1). Note that when the BLOCK option is used, XADD will pay O(M) time in order to serve the M clients blocked on the stream getting new data.", + "arguments": [ + { + "command": "COUNT", + "name": "count", + "type": "integer", + "optional": true + }, + { + "command": "BLOCK", + "name": "milliseconds", + "type": "integer", + "optional": true + }, + { + "name": "streams", + "type": "enum", + "enum": [ + "STREAMS" + ] + }, + { + "name": "key", + "type": "key", + "multiple": true + }, + { + "name": "id", + "type": "string", + "multiple": true + } + ], + "since": "5.0.0", + "group": "stream" + }, + "XGROUP": { + "summary": "Create, destroy, and manage consumer groups.", + "complexity": "O(1) for all the subcommands, with the exception of the DESTROY subcommand which takes an additional O(M) time in order to delete the M entries inside the consumer group pending entries list (PEL).", + "arguments": [ + { + "command": "CREATE", + "name": [ + "key", + "groupname", + "id-or-$" + ], + "type": [ + "key", + "string", + "string" + ], + "optional": true + }, + { + "command": "SETID", + "name": [ + "key", + "groupname", + "id-or-$" + ], + "type": [ + "key", + "string", + "string" + ], + "optional": true + }, + { + "command": "DESTROY", + "name": [ + "key", + "groupname" + ], + "type": [ + "key", + "string" + ], + "optional": true + }, + { + "command": "DELCONSUMER", + "name": [ + "key", + "groupname", + "consumername" + ], + "type": [ + "key", + "string", + "string" + ], + "optional": true + } + ], + "since": "5.0.0", + "group": "stream" + }, + "XREADGROUP": { + "summary": "Return new entries from a stream using a consumer group, or access the history of the pending entries for a given consumer. Can block.", + "complexity": "For each stream mentioned: O(M) with M being the number of elements returned. If M is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(1). On the other side when XREADGROUP blocks, XADD will pay the O(N) time in order to serve the N clients blocked on the stream getting new data.", + "arguments": [ + { + "command": "GROUP", + "name": [ + "group", + "consumer" + ], + "type": [ + "string", + "string" + ] + }, + { + "command": "COUNT", + "name": "count", + "type": "integer", + "optional": true + }, + { + "command": "BLOCK", + "name": "milliseconds", + "type": "integer", + "optional": true + }, + { + "name": "noack", + "type": "enum", + "enum": [ + "NOACK" + ], + "optional": true + }, + { + "name": "streams", + "type": "enum", + "enum": [ + "STREAMS" + ] + }, + { + "name": "key", + "type": "key", + "multiple": true + }, + { + "name": "ID", + "type": "string", + "multiple": true + } + ], + "since": "5.0.0", + "group": "stream" + }, + "XACK": { + "summary": "Marks a pending message as correctly processed, effectively removing it from the pending entries list of the consumer group. Return value of the command is the number of messages successfully acknowledged, that is, the IDs we were actually able to resolve in the PEL.", + "complexity": "O(1) for each message ID processed.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "group", + "type": "string" + }, + { + "name": "ID", + "type": "string", + "multiple": true + } + ], + "since": "5.0.0", + "group": "stream" + }, + "XCLAIM": { + "summary": "Changes (or acquires) ownership of a message in a consumer group, as if the message was delivered to the specified consumer.", + "complexity": "O(log N) with N being the number of messages in the PEL of the consumer group.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "group", + "type": "string" + }, + { + "name": "consumer", + "type": "string" + }, + { + "name": "min-idle-time", + "type": "string" + }, + { + "name": "ID", + "type": "string", + "multiple": true + }, + { + "command": "IDLE", + "name": "ms", + "type": "integer", + "optional": true + }, + { + "command": "TIME", + "name": "ms-unix-time", + "type": "integer", + "optional": true + }, + { + "command": "RETRYCOUNT", + "name": "count", + "type": "integer", + "optional": true + }, + { + "name": "force", + "enum": [ + "FORCE" + ], + "optional": true + }, + { + "name": "justid", + "enum": [ + "JUSTID" + ], + "optional": true + } + ], + "since": "5.0.0", + "group": "stream" + }, + "XPENDING": { + "summary": "Return information and entries from a stream consumer group pending entries list, that are messages fetched but never acknowledged.", + "complexity": "O(N) with N being the number of elements returned, so asking for a small fixed number of entries per call is O(1). When the command returns just the summary it runs in O(1) time assuming the list of consumers is small, otherwise there is additional O(N) time needed to iterate every consumer.", + "arguments": [ + { + "name": "key", + "type": "key" + }, + { + "name": "group", + "type": "string" + }, + { + "name": [ + "start", + "end", + "count" + ], + "type": [ + "string", + "string", + "integer" + ], + "optional": true + }, + { + "name": "consumer", + "type": "string", + "optional": true + } + ], + "since": "5.0.0", + "group": "stream" + }, + "LATENCY DOCTOR": { + "summary": "Return a human readable latency analysis report.", + "since": "2.8.13", + "group": "server" + }, + "LATENCY GRAPH": { + "summary": "Return a latency graph for the event.", + "arguments": [ + { + "name": "event", + "type": "string" + } + ], + "since": "2.8.13", + "group": "server" + }, + "LATENCY HISTORY": { + "summary": "Return timestamp-latency samples for the event.", + "arguments": [ + { + "name": "event", + "type": "string" + } + ], + "since": "2.8.13", + "group": "server" + }, + "LATENCY LATEST": { + "summary": "Return the latest latency samples for all events.", + "since": "2.8.13", + "group": "server" + }, + "LATENCY RESET": { + "summary": "Reset latency data for one or more events.", + "arguments": [ + { + "name": "event", + "type": "string", + "optional": true + } + ], + "since": "2.8.13", + "group": "server" + }, + "LATENCY HELP": { + "summary": "Show helpful text about the different subcommands.", + "since": "2.8.13", + "group": "server" + } +} diff --git a/iredis/data/commands/__init__.py b/iredis/data/commands/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/iredis/data/commands/__init__.py diff --git a/iredis/data/commands/acl-cat.md b/iredis/data/commands/acl-cat.md new file mode 100644 index 0000000..eedf692 --- /dev/null +++ b/iredis/data/commands/acl-cat.md @@ -0,0 +1,84 @@ +The command shows the available ACL categories if called without arguments. If a +category name is given, the command shows all the Redis commands in the +specified category. + +ACL categories are very useful in order to create ACL rules that include or +exclude a large set of commands at once, without specifying every single +command. For instance, the following rule will let the user `karin` perform +everything but the most dangerous operations that may affect the server +stability: + + ACL SETUSER karin on +@all -@dangerous + +We first add all the commands to the set of commands that `karin` is able to +execute, but then we remove all the dangerous commands. + +Checking for all the available categories is as simple as: + +``` +> ACL CAT + 1) "keyspace" + 2) "read" + 3) "write" + 4) "set" + 5) "sortedset" + 6) "list" + 7) "hash" + 8) "string" + 9) "bitmap" +10) "hyperloglog" +11) "geo" +12) "stream" +13) "pubsub" +14) "admin" +15) "fast" +16) "slow" +17) "blocking" +18) "dangerous" +19) "connection" +20) "transaction" +21) "scripting" +``` + +Then we may want to know what commands are part of a given category: + +``` +> ACL CAT dangerous + 1) "flushdb" + 2) "acl" + 3) "slowlog" + 4) "debug" + 5) "role" + 6) "keys" + 7) "pfselftest" + 8) "client" + 9) "bgrewriteaof" +10) "replicaof" +11) "monitor" +12) "restore-asking" +13) "latency" +14) "replconf" +15) "pfdebug" +16) "bgsave" +17) "sync" +18) "config" +19) "flushall" +20) "cluster" +21) "info" +22) "lastsave" +23) "slaveof" +24) "swapdb" +25) "module" +26) "restore" +27) "migrate" +28) "save" +29) "shutdown" +30) "psync" +31) "sort" +``` + +@return + +@array-reply: a list of ACL categories or a list of commands inside a given +category. The command may return an error if an invalid category name is given +as argument. diff --git a/iredis/data/commands/acl-deluser.md b/iredis/data/commands/acl-deluser.md new file mode 100644 index 0000000..88359fe --- /dev/null +++ b/iredis/data/commands/acl-deluser.md @@ -0,0 +1,17 @@ +Delete all the specified ACL users and terminate all the connections that are +authenticated with such users. Note: the special `default` user cannot be +removed from the system, this is the default user that every new connection is +authenticated with. The list of users may include usernames that do not exist, +in such case no operation is performed for the non existing users. + +@return + +@integer-reply: The number of users that were deleted. This number will not +always match the number of arguments since certain users may not exist. + +@examples + +``` +> ACL DELUSER antirez +1 +``` diff --git a/iredis/data/commands/acl-genpass.md b/iredis/data/commands/acl-genpass.md new file mode 100644 index 0000000..46043cc --- /dev/null +++ b/iredis/data/commands/acl-genpass.md @@ -0,0 +1,43 @@ +ACL users need a solid password in order to authenticate to the server without +security risks. Such password does not need to be remembered by humans, but only +by computers, so it can be very long and strong (unguessable by an external +attacker). The `ACL GENPASS` command generates a password starting from +/dev/urandom if available, otherwise (in systems without /dev/urandom) it uses a +weaker system that is likely still better than picking a weak password by hand. + +By default (if /dev/urandom is available) the password is strong and can be used +for other uses in the context of a Redis application, for instance in order to +create unique session identifiers or other kind of unguessable and not colliding +IDs. The password generation is also very cheap because we don't really ask +/dev/urandom for bits at every execution. At startup Redis creates a seed using +/dev/urandom, then it will use SHA256 in counter mode, with +HMAC-SHA256(seed,counter) as primitive, in order to create more random bytes as +needed. This means that the application developer should be feel free to abuse +`ACL GENPASS` to create as many secure pseudorandom strings as needed. + +The command output is an hexadecimal representation of a binary string. By +default it emits 256 bits (so 64 hex characters). The user can provide an +argument in form of number of bits to emit from 1 to 1024 to change the output +length. Note that the number of bits provided is always rounded to the next +multiple of 4. So for instance asking for just 1 bit password will result in 4 +bits to be emitted, in the form of a single hex character. + +@return + +@bulk-string-reply: by default 64 bytes string representing 256 bits of +pseudorandom data. Otherwise if an argument if needed, the output string length +is the number of specified bits (rounded to the next multiple of 4) divided +by 4. + +@examples + +``` +> ACL GENPASS +"dd721260bfe1b3d9601e7fbab36de6d04e2e67b0ef1c53de59d45950db0dd3cc" + +> ACL GENPASS 32 +"355ef3dd" + +> ACL GENPASS 5 +"90" +``` diff --git a/iredis/data/commands/acl-getuser.md b/iredis/data/commands/acl-getuser.md new file mode 100644 index 0000000..a3bebe3 --- /dev/null +++ b/iredis/data/commands/acl-getuser.md @@ -0,0 +1,28 @@ +The command returns all the rules defined for an existing ACL user. + +Specifically, it lists the user's ACL flags, password hashes and key name +patterns. Note that command rules are returned as a string in the same format +used with the `ACL SETUSER` command. This description of command rules reflects +the user's effective permissions, so while it may not be identical to the set of +rules used to configure the user, it is still functionally identical. + +@array-reply: a list of ACL rule definitions for the user. + +@examples + +Here's the default configuration for the default user: + +``` +> ACL GETUSER default +1) "flags" +2) 1) "on" + 2) "allkeys" + 3) "allcommands" + 4) "nopass" +3) "passwords" +4) (empty array) +5) "commands" +6) "+@all" +7) "keys" +8) 1) "*" +``` diff --git a/iredis/data/commands/acl-help.md b/iredis/data/commands/acl-help.md new file mode 100644 index 0000000..3ec1ffb --- /dev/null +++ b/iredis/data/commands/acl-help.md @@ -0,0 +1,6 @@ +The `ACL HELP` command returns a helpful text describing the different +subcommands. + +@return + +@array-reply: a list of subcommands and their descriptions diff --git a/iredis/data/commands/acl-list.md b/iredis/data/commands/acl-list.md new file mode 100644 index 0000000..ebfa36b --- /dev/null +++ b/iredis/data/commands/acl-list.md @@ -0,0 +1,17 @@ +The command shows the currently active ACL rules in the Redis server. Each line +in the returned array defines a different user, and the format is the same used +in the redis.conf file or the external ACL file, so you can cut and paste what +is returned by the ACL LIST command directly inside a configuration file if you +wish (but make sure to check `ACL SAVE`). + +@return + +An array of strings. + +@examples + +``` +> ACL LIST +1) "user antirez on #9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 ~objects:* +@all -@admin -@dangerous" +2) "user default on nopass ~* +@all" +``` diff --git a/iredis/data/commands/acl-load.md b/iredis/data/commands/acl-load.md new file mode 100644 index 0000000..3892bb4 --- /dev/null +++ b/iredis/data/commands/acl-load.md @@ -0,0 +1,27 @@ +When Redis is configured to use an ACL file (with the `aclfile` configuration +option), this command will reload the ACLs from the file, replacing all the +current ACL rules with the ones defined in the file. The command makes sure to +have an _all or nothing_ behavior, that is: + +- If every line in the file is valid, all the ACLs are loaded. +- If one or more line in the file is not valid, nothing is loaded, and the old + ACL rules defined in the server memory continue to be used. + +@return + +@simple-string-reply: `OK` on success. + +The command may fail with an error for several reasons: if the file is not +readable, if there is an error inside the file, and in such case the error will +be reported to the user in the error. Finally the command will fail if the +server is not configured to use an external ACL file. + +@examples + +``` +> ACL LOAD ++OK + +> ACL LOAD +-ERR /tmp/foo:1: Unknown command or category name in ACL... +``` diff --git a/iredis/data/commands/acl-log.md b/iredis/data/commands/acl-log.md new file mode 100644 index 0000000..6dbdb85 --- /dev/null +++ b/iredis/data/commands/acl-log.md @@ -0,0 +1,41 @@ +The command shows a list of recent ACL security events: + +1. Failures to authenticate their connections with `AUTH` or `HELLO`. +2. Commands denied because against the current ACL rules. +3. Commands denied because accessing keys not allowed in the current ACL rules. + +The optional argument specifies how many entries to show. By default up to ten +failures are returned. The special `RESET` argument clears the log. Entries are +displayed starting from the most recent. + +@return + +When called to show security events: + +@array-reply: a list of ACL security events. + +When called with `RESET`: + +@simple-string-reply: `OK` if the security log was cleared. + +@examples + +``` +> AUTH someuser wrongpassword +(error) WRONGPASS invalid username-password pair +> ACL LOG 1 +1) 1) "count" + 2) (integer) 1 + 3) "reason" + 4) "auth" + 5) "context" + 6) "toplevel" + 7) "object" + 8) "AUTH" + 9) "username" + 10) "someuser" + 11) "age-seconds" + 12) "4.0960000000000001" + 13) "client-info" + 14) "id=6 addr=127.0.0.1:63026 fd=8 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=48 qbuf-free=32720 obl=0 oll=0 omem=0 events=r cmd=auth user=default" +``` diff --git a/iredis/data/commands/acl-save.md b/iredis/data/commands/acl-save.md new file mode 100644 index 0000000..17580c6 --- /dev/null +++ b/iredis/data/commands/acl-save.md @@ -0,0 +1,20 @@ +When Redis is configured to use an ACL file (with the `aclfile` configuration +option), this command will save the currently defined ACLs from the server +memory to the ACL file. + +@return + +@simple-string-reply: `OK` on success. + +The command may fail with an error for several reasons: if the file cannot be +written or if the server is not configured to use an external ACL file. + +@examples + +``` +> ACL SAVE ++OK + +> ACL SAVE +-ERR There was an error trying to save the ACLs. Please check the server logs for more information +``` diff --git a/iredis/data/commands/acl-setuser.md b/iredis/data/commands/acl-setuser.md new file mode 100644 index 0000000..476d178 --- /dev/null +++ b/iredis/data/commands/acl-setuser.md @@ -0,0 +1,115 @@ +Create an ACL user with the specified rules or modify the rules of an existing +user. This is the main interface in order to manipulate Redis ACL users +interactively: if the username does not exist, the command creates the username +without any privilege, then reads from left to right all the rules provided as +successive arguments, setting the user ACL rules as specified. + +If the user already exists, the provided ACL rules are simply applied _in +addition_ to the rules already set. For example: + + ACL SETUSER virginia on allkeys +set + +The above command will create a user called `virginia` that is active (the on +rule), can access any key (allkeys rule), and can call the set command (+set +rule). Then another SETUSER call can modify the user rules: + + ACL SETUSER virginia +get + +The above rule will not apply the new rule to the user virginia, so other than +`SET`, the user virginia will now be able to also use the `GET` command. + +When we want to be sure to define an user from scratch, without caring if it had +previously defined rules associated, we can use the special rule `reset` as +first rule, in order to flush all the other existing rules: + + ACL SETUSER antirez reset [... other rules ...] + +After resetting an user, it returns back to the status it has when it was just +created: non active (off rule), can't execute any command, can't access any key: + + > ACL SETUSER antirez reset + +OK + > ACL LIST + 1) "user antirez off -@all" + +ACL rules are either words like "on", "off", "reset", "allkeys", or are special +rules that start with a special character, and are followed by another string +(without any space in between), like "+SET". + +The following documentation is a reference manual about the capabilities of this +command, however our [ACL tutorial](/topics/acl) may be a more gentle +introduction to how the ACL system works in general. + +## List of rules + +This is a list of all the supported Redis ACL rules: + +- `on`: set the user as active, it will be possible to authenticate as this user + using `AUTH <username> <password>`. +- `off`: set user as not active, it will be impossible to log as this user. + Please note that if a user gets disabled (set to off) after there are + connections already authenticated with such a user, the connections will + continue to work as expected. To also kill the old connections you can use + `CLIENT KILL` with the user option. An alternative is to delete the user with + `ACL DELUSER`, that will result in all the connections authenticated as the + deleted user to be disconnected. +- `~<pattern>`: add the specified key pattern (glob style pattern, like in the + `KEYS` command), to the list of key patterns accessible by the user. You can + add as many key patterns you want to the same user. Example: `~objects:*` +- `allkeys`: alias for `~*`, it allows the user to access all the keys. +- `resetkey`: removes all the key patterns from the list of key patterns the + user can access. +- `+<command>`: add this command to the list of the commands the user can call. + Example: `+zadd`. +- `+@<category>`: add all the commands in the specified category to the list of + commands the user is able to execute. Example: `+@string` (adds all the string + commands). For a list of categories check the `ACL CAT` command. +- `+<command>|<subcommand>`: add the specified command to the list of the + commands the user can execute, but only for the specified subcommand. Example: + `+config|get`. Generates an error if the specified command is already allowed + in its full version for the specified user. Note: there is no symmetrical + command to remove subcommands, you need to remove the whole command and re-add + the subcommands you want to allow. This is much safer than removing + subcommands, in the future Redis may add new dangerous subcommands, so + configuring by subtraction is not good. +- `allcommands`: alias of `+@all`. Adds all the commands there are in the + server, including _future commands_ loaded via module, to be executed by this + user. +- `-<command>`. Like `+<command>` but removes the command instead of adding it. +- `-@<category>`: Like `+@<category>` but removes all the commands in the + category instead of adding them. +- `nocommands`: alias for `-@all`. Removes all the commands, the user will no + longer be able to execute anything. +- `nopass`: the user is set as a "no password" user. It means that it will be + possible to authenticate as such user with any password. By default, the + `default` special user is set as "nopass". The `nopass` rule will also reset + all the configured passwords for the user. +- `>password`: Add the specified clear text password as an hashed password in + the list of the users passwords. Every user can have many active passwords, so + that password rotation will be simpler. The specified password is not stored + in cleartext inside the server. Example: `>mypassword`. +- `#<hashedpassword>`: Add the specified hashed password to the list of user + passwords. A Redis hashed password is hashed with SHA256 and translated into a + hexadecimal string. Example: + `#c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2`. +- `<password`: Like `>password` but removes the password instead of adding it. +- `!<hashedpassword>`: Like `#<hashedpassword>` but removes the password instead + of adding it. +- reset: Remove any capability from the user. It is set to off, without + passwords, unable to execute any command, unable to access any key. + +@return + +@simple-string-reply: `OK` on success. + +If the rules contain errors, the error is returned. + +@examples + +``` +> ACL SETUSER alan allkeys +@string +@set -SADD >alanpassword ++OK + +> ACL SETUSER antirez heeyyyy +(error) ERR Error in ACL SETUSER modifier 'heeyyyy': Syntax error +``` diff --git a/iredis/data/commands/acl-users.md b/iredis/data/commands/acl-users.md new file mode 100644 index 0000000..c4d4d8c --- /dev/null +++ b/iredis/data/commands/acl-users.md @@ -0,0 +1,15 @@ +The command shows a list of all the usernames of the currently configured users +in the Redis ACL system. + +@return + +An array of strings. + +@examples + +``` +> ACL USERS +1) "anna" +2) "antirez" +3) "default" +``` diff --git a/iredis/data/commands/acl-whoami.md b/iredis/data/commands/acl-whoami.md new file mode 100644 index 0000000..3007760 --- /dev/null +++ b/iredis/data/commands/acl-whoami.md @@ -0,0 +1,14 @@ +Return the username the current connection is authenticated with. New +connections are authenticated with the "default" user. They can change user +using `AUTH`. + +@return + +@bulk-string-reply: the username of the current connection. + +@examples + +``` +> ACL WHOAMI +"default" +``` diff --git a/iredis/data/commands/append.md b/iredis/data/commands/append.md new file mode 100644 index 0000000..c354122 --- /dev/null +++ b/iredis/data/commands/append.md @@ -0,0 +1,55 @@ +If `key` already exists and is a string, this command appends the `value` at the +end of the string. If `key` does not exist it is created and set as an empty +string, so `APPEND` will be similar to `SET` in this special case. + +@return + +@integer-reply: the length of the string after the append operation. + +@examples + +```cli +EXISTS mykey +APPEND mykey "Hello" +APPEND mykey " World" +GET mykey +``` + +## Pattern: Time series + +The `APPEND` command can be used to create a very compact representation of a +list of fixed-size samples, usually referred as _time series_. Every time a new +sample arrives we can store it using the command + +``` +APPEND timeseries "fixed-size sample" +``` + +Accessing individual elements in the time series is not hard: + +- `STRLEN` can be used in order to obtain the number of samples. +- `GETRANGE` allows for random access of elements. If our time series have + associated time information we can easily implement a binary search to get + range combining `GETRANGE` with the Lua scripting engine available in Redis + 2.6. +- `SETRANGE` can be used to overwrite an existing time series. + +The limitation of this pattern is that we are forced into an append-only mode of +operation, there is no way to cut the time series to a given size easily because +Redis currently lacks a command able to trim string objects. However the space +efficiency of time series stored in this way is remarkable. + +Hint: it is possible to switch to a different key based on the current Unix +time, in this way it is possible to have just a relatively small amount of +samples per key, to avoid dealing with very big keys, and to make this pattern +more friendly to be distributed across many Redis instances. + +An example sampling the temperature of a sensor using fixed-size strings (using +a binary format is better in real implementations). + +```cli +APPEND ts "0043" +APPEND ts "0035" +GETRANGE ts 0 3 +GETRANGE ts 4 7 +``` diff --git a/iredis/data/commands/auth.md b/iredis/data/commands/auth.md new file mode 100644 index 0000000..4b75171 --- /dev/null +++ b/iredis/data/commands/auth.md @@ -0,0 +1,42 @@ +The AUTH command authenticates the current connection in two cases: + +1. If the Redis server is password protected via the `requirepass` option. +2. If a Redis 6.0 instance, or greater, is using the + [Redis ACL system](/topics/acl). + +Redis versions prior of Redis 6 were only able to understand the one argument +version of the command: + + AUTH <password> + +This form just authenticates against the password set with `requirepass`. In +this configuration Redis will deny any command executed by the just connected +clients, unless the connection gets authenticated via `AUTH`. + +If the password provided via AUTH matches the password in the configuration +file, the server replies with the `OK` status code and starts accepting +commands. Otherwise, an error is returned and the clients needs to try a new +password. + +When Redis ACLs are used, the command should be given in an extended way: + + AUTH <username> <password> + +In order to authenticate the current connection with one of the connections +defined in the ACL list (see `ACL SETUSER`) and the official +[ACL guide](/topics/acl) for more information. + +When ACLs are used, the single argument form of the command, where only the +password is specified, assumes that the implicit username is "default". + +## Security notice + +Because of the high performance nature of Redis, it is possible to try a lot of +passwords in parallel in very short time, so make sure to generate a strong and +very long password so that this attack is infeasible. A good way to generate +strong passwords is via the `ACL GENPASS` command. + +@return + +@simple-string-reply or an error if the password, or username/password pair, is +invalid. diff --git a/iredis/data/commands/bgrewriteaof.md b/iredis/data/commands/bgrewriteaof.md new file mode 100644 index 0000000..2ebaa89 --- /dev/null +++ b/iredis/data/commands/bgrewriteaof.md @@ -0,0 +1,37 @@ +Instruct Redis to start an [Append Only File][tpaof] rewrite process. The +rewrite will create a small optimized version of the current Append Only File. + +[tpaof]: /topics/persistence#append-only-file + +If `BGREWRITEAOF` fails, no data gets lost as the old AOF will be untouched. + +The rewrite will be only triggered by Redis if there is not already a background +process doing persistence. + +Specifically: + +- If a Redis child is creating a snapshot on disk, the AOF rewrite is + _scheduled_ but not started until the saving child producing the RDB file + terminates. In this case the `BGREWRITEAOF` will still return an positive + status reply, but with an appropriate message. You can check if an AOF rewrite + is scheduled looking at the `INFO` command as of Redis 2.6 or successive + versions. +- If an AOF rewrite is already in progress the command returns an error and no + AOF rewrite will be scheduled for a later time. +- If the AOF rewrite could start, but the attempt at starting it fails (for + instance because of an error in creating the child process), an error is + returned to the caller. + +Since Redis 2.4 the AOF rewrite is automatically triggered by Redis, however the +`BGREWRITEAOF` command can be used to trigger a rewrite at any time. + +Please refer to the [persistence documentation][tp] for detailed information. + +[tp]: /topics/persistence + +@return + +@simple-string-reply: A simple string reply indicating that the rewriting +started or is about to start ASAP, when the call is executed with success. + +The command may reply with an error in certain cases, as documented above. diff --git a/iredis/data/commands/bgsave.md b/iredis/data/commands/bgsave.md new file mode 100644 index 0000000..f04d71b --- /dev/null +++ b/iredis/data/commands/bgsave.md @@ -0,0 +1,28 @@ +Save the DB in background. + +Normally the OK code is immediately returned. Redis forks, the parent continues +to serve the clients, the child saves the DB on disk then exits. + +An error is returned if there is already a background save running or if there +is another non-background-save process running, specifically an in-progress AOF +rewrite. + +If `BGSAVE SCHEDULE` is used, the command will immediately return `OK` when an +AOF rewrite is in progress and schedule the background save to run at the next +opportunity. + +A client may be able to check if the operation succeeded using the `LASTSAVE` +command. + +Please refer to the [persistence documentation][tp] for detailed information. + +[tp]: /topics/persistence + +@return + +@simple-string-reply: `Background saving started` if `BGSAVE` started correctly +or `Background saving scheduled` when used with the `SCHEDULE` subcommand. + +@history + +- `>= 3.2.2`: Added the `SCHEDULE` option. diff --git a/iredis/data/commands/bitcount.md b/iredis/data/commands/bitcount.md new file mode 100644 index 0000000..680aae1 --- /dev/null +++ b/iredis/data/commands/bitcount.md @@ -0,0 +1,66 @@ +Count the number of set bits (population counting) in a string. + +By default all the bytes contained in the string are examined. It is possible to +specify the counting operation only in an interval passing the additional +arguments _start_ and _end_. + +Like for the `GETRANGE` command start and end can contain negative values in +order to index bytes starting from the end of the string, where -1 is the last +byte, -2 is the penultimate, and so forth. + +Non-existent keys are treated as empty strings, so the command will return zero. + +@return + +@integer-reply + +The number of bits set to 1. + +@examples + +```cli +SET mykey "foobar" +BITCOUNT mykey +BITCOUNT mykey 0 0 +BITCOUNT mykey 1 1 +``` + +## Pattern: real-time metrics using bitmaps + +Bitmaps are a very space-efficient representation of certain kinds of +information. One example is a Web application that needs the history of user +visits, so that for instance it is possible to determine what users are good +targets of beta features. + +Using the `SETBIT` command this is trivial to accomplish, identifying every day +with a small progressive integer. For instance day 0 is the first day the +application was put online, day 1 the next day, and so forth. + +Every time a user performs a page view, the application can register that in the +current day the user visited the web site using the `SETBIT` command setting the +bit corresponding to the current day. + +Later it will be trivial to know the number of single days the user visited the +web site simply calling the `BITCOUNT` command against the bitmap. + +A similar pattern where user IDs are used instead of days is described in the +article called "[Fast easy realtime metrics using Redis +bitmaps][hbgc212fermurb]". + +[hbgc212fermurb]: + http://blog.getspool.com/2011/11/29/fast-easy-realtime-metrics-using-redis-bitmaps + +## Performance considerations + +In the above example of counting days, even after 10 years the application is +online we still have just `365*10` bits of data per user, that is just 456 bytes +per user. With this amount of data `BITCOUNT` is still as fast as any other O(1) +Redis command like `GET` or `INCR`. + +When the bitmap is big, there are two alternatives: + +- Taking a separated key that is incremented every time the bitmap is modified. + This can be very efficient and atomic using a small Redis Lua script. +- Running the bitmap incrementally using the `BITCOUNT` _start_ and _end_ + optional parameters, accumulating the results client-side, and optionally + caching the result into a key. diff --git a/iredis/data/commands/bitfield.md b/iredis/data/commands/bitfield.md new file mode 100644 index 0000000..80563fa --- /dev/null +++ b/iredis/data/commands/bitfield.md @@ -0,0 +1,155 @@ +The command treats a Redis string as a array of bits, and is capable of +addressing specific integer fields of varying bit widths and arbitrary non +(necessary) aligned offset. In practical terms using this command you can set, +for example, a signed 5 bits integer at bit offset 1234 to a specific value, +retrieve a 31 bit unsigned integer from offset 4567. Similarly the command +handles increments and decrements of the specified integers, providing +guaranteed and well specified overflow and underflow behavior that the user can +configure. + +`BITFIELD` is able to operate with multiple bit fields in the same command call. +It takes a list of operations to perform, and returns an array of replies, where +each array matches the corresponding operation in the list of arguments. + +For example the following command increments an 5 bit signed integer at bit +offset 100, and gets the value of the 4 bit unsigned integer at bit offset 0: + + > BITFIELD mykey INCRBY i5 100 1 GET u4 0 + 1) (integer) 1 + 2) (integer) 0 + +Note that: + +1. Addressing with `GET` bits outside the current string length (including the + case the key does not exist at all), results in the operation to be performed + like the missing part all consists of bits set to 0. +2. Addressing with `SET` or `INCRBY` bits outside the current string length will + enlarge the string, zero-padding it, as needed, for the minimal length + needed, according to the most far bit touched. + +## Supported subcommands and integer types + +The following is the list of supported commands. + +- **GET** `<type>` `<offset>` -- Returns the specified bit field. +- **SET** `<type>` `<offset>` `<value>` -- Set the specified bit field and + returns its old value. +- **INCRBY** `<type>` `<offset>` `<increment>` -- Increments or decrements (if a + negative increment is given) the specified bit field and returns the new + value. + +There is another subcommand that only changes the behavior of successive +`INCRBY` subcommand calls by setting the overflow behavior: + +- **OVERFLOW** `[WRAP|SAT|FAIL]` + +Where an integer type is expected, it can be composed by prefixing with `i` for +signed integers and `u` for unsigned integers with the number of bits of our +integer type. So for example `u8` is an unsigned integer of 8 bits and `i16` is +a signed integer of 16 bits. + +The supported types are up to 64 bits for signed integers, and up to 63 bits for +unsigned integers. This limitation with unsigned integers is due to the fact +that currently the Redis protocol is unable to return 64 bit unsigned integers +as replies. + +## Bits and positional offsets + +There are two ways in order to specify offsets in the bitfield command. If a +number without any prefix is specified, it is used just as a zero based bit +offset inside the string. + +However if the offset is prefixed with a `#` character, the specified offset is +multiplied by the integer type width, so for example: + + BITFIELD mystring SET i8 #0 100 SET i8 #1 200 + +Will set the first i8 integer at offset 0 and the second at offset 8. This way +you don't have to do the math yourself inside your client if what you want is a +plain array of integers of a given size. + +## Overflow control + +Using the `OVERFLOW` command the user is able to fine-tune the behavior of the +increment or decrement overflow (or underflow) by specifying one of the +following behaviors: + +- **WRAP**: wrap around, both with signed and unsigned integers. In the case of + unsigned integers, wrapping is like performing the operation modulo the + maximum value the integer can contain (the C standard behavior). With signed + integers instead wrapping means that overflows restart towards the most + negative value and underflows towards the most positive ones, so for example + if an `i8` integer is set to the value 127, incrementing it by 1 will yield + `-128`. +- **SAT**: uses saturation arithmetic, that is, on underflows the value is set + to the minimum integer value, and on overflows to the maximum integer value. + For example incrementing an `i8` integer starting from value 120 with an + increment of 10, will result into the value 127, and further increments will + always keep the value at 127. The same happens on underflows, but towards the + value is blocked at the most negative value. +- **FAIL**: in this mode no operation is performed on overflows or underflows + detected. The corresponding return value is set to NULL to signal the + condition to the caller. + +Note that each `OVERFLOW` statement only affects the `INCRBY` commands that +follow it in the list of subcommands, up to the next `OVERFLOW` statement. + +By default, **WRAP** is used if not otherwise specified. + + > BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 + 1) (integer) 1 + 2) (integer) 1 + > BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 + 1) (integer) 2 + 2) (integer) 2 + > BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 + 1) (integer) 3 + 2) (integer) 3 + > BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 + 1) (integer) 0 + 2) (integer) 3 + +## Return value + +The command returns an array with each entry being the corresponding result of +the sub command given at the same position. `OVERFLOW` subcommands don't count +as generating a reply. + +The following is an example of `OVERFLOW FAIL` returning NULL. + + > BITFIELD mykey OVERFLOW FAIL incrby u2 102 1 + 1) (nil) + +## Motivations + +The motivation for this command is that the ability to store many small integers +as a single large bitmap (or segmented over a few keys to avoid having huge +keys) is extremely memory efficient, and opens new use cases for Redis to be +applied, especially in the field of real time analytics. This use cases are +supported by the ability to specify the overflow in a controlled way. + +Fun fact: Reddit's 2017 April fools' project +[r/place](https://reddit.com/r/place) was +[built using the Redis BITFIELD command](https://redditblog.com/2017/04/13/how-we-built-rplace/) +in order to take an in-memory representation of the collaborative canvas. + +## Performance considerations + +Usually `BITFIELD` is a fast command, however note that addressing far bits of +currently short strings will trigger an allocation that may be more costly than +executing the command on bits already existing. + +## Orders of bits + +The representation used by `BITFIELD` considers the bitmap as having the bit +number 0 to be the most significant bit of the first byte, and so forth, so for +example setting a 5 bits unsigned integer to value 23 at offset 7 into a bitmap +previously set to all zeroes, will produce the following representation: + + +--------+--------+ + |00000001|01110000| + +--------+--------+ + +When offsets and integer sizes are aligned to bytes boundaries, this is the same +as big endian, however when such alignment does not exist, its important to also +understand how the bits inside a byte are ordered. diff --git a/iredis/data/commands/bitop.md b/iredis/data/commands/bitop.md new file mode 100644 index 0000000..656befa --- /dev/null +++ b/iredis/data/commands/bitop.md @@ -0,0 +1,61 @@ +Perform a bitwise operation between multiple keys (containing string values) and +store the result in the destination key. + +The `BITOP` command supports four bitwise operations: **AND**, **OR**, **XOR** +and **NOT**, thus the valid forms to call the command are: + +- `BITOP AND destkey srckey1 srckey2 srckey3 ... srckeyN` +- `BITOP OR destkey srckey1 srckey2 srckey3 ... srckeyN` +- `BITOP XOR destkey srckey1 srckey2 srckey3 ... srckeyN` +- `BITOP NOT destkey srckey` + +As you can see **NOT** is special as it only takes an input key, because it +performs inversion of bits so it only makes sense as an unary operator. + +The result of the operation is always stored at `destkey`. + +## Handling of strings with different lengths + +When an operation is performed between strings having different lengths, all the +strings shorter than the longest string in the set are treated as if they were +zero-padded up to the length of the longest string. + +The same holds true for non-existent keys, that are considered as a stream of +zero bytes up to the length of the longest string. + +@return + +@integer-reply + +The size of the string stored in the destination key, that is equal to the size +of the longest input string. + +@examples + +```cli +SET key1 "foobar" +SET key2 "abcdef" +BITOP AND dest key1 key2 +GET dest +``` + +## Pattern: real time metrics using bitmaps + +`BITOP` is a good complement to the pattern documented in the `BITCOUNT` command +documentation. Different bitmaps can be combined in order to obtain a target +bitmap where the population counting operation is performed. + +See the article called "[Fast easy realtime metrics using Redis +bitmaps][hbgc212fermurb]" for a interesting use cases. + +[hbgc212fermurb]: + http://blog.getspool.com/2011/11/29/fast-easy-realtime-metrics-using-redis-bitmaps + +## Performance considerations + +`BITOP` is a potentially slow command as it runs in O(N) time. Care should be +taken when running it against long input strings. + +For real-time metrics and statistics involving large inputs a good approach is +to use a replica (with read-only option disabled) where the bit-wise operations +are performed to avoid blocking the master instance. diff --git a/iredis/data/commands/bitpos.md b/iredis/data/commands/bitpos.md new file mode 100644 index 0000000..56dc588 --- /dev/null +++ b/iredis/data/commands/bitpos.md @@ -0,0 +1,60 @@ +Return the position of the first bit set to 1 or 0 in a string. + +The position is returned, thinking of the string as an array of bits from left +to right, where the first byte's most significant bit is at position 0, the +second byte's most significant bit is at position 8, and so forth. + +The same bit position convention is followed by `GETBIT` and `SETBIT`. + +By default, all the bytes contained in the string are examined. It is possible +to look for bits only in a specified interval passing the additional arguments +_start_ and _end_ (it is possible to just pass _start_, the operation will +assume that the end is the last byte of the string. However there are semantic +differences as explained later). The range is interpreted as a range of bytes +and not a range of bits, so `start=0` and `end=2` means to look at the first +three bytes. + +Note that bit positions are returned always as absolute values starting from bit +zero even when _start_ and _end_ are used to specify a range. + +Like for the `GETRANGE` command start and end can contain negative values in +order to index bytes starting from the end of the string, where -1 is the last +byte, -2 is the penultimate, and so forth. + +Non-existent keys are treated as empty strings. + +@return + +@integer-reply + +The command returns the position of the first bit set to 1 or 0 according to the +request. + +If we look for set bits (the bit argument is 1) and the string is empty or +composed of just zero bytes, -1 is returned. + +If we look for clear bits (the bit argument is 0) and the string only contains +bit set to 1, the function returns the first bit not part of the string on the +right. So if the string is three bytes set to the value `0xff` the command +`BITPOS key 0` will return 24, since up to bit 23 all the bits are 1. + +Basically, the function considers the right of the string as padded with zeros +if you look for clear bits and specify no range or the _start_ argument +**only**. + +However, this behavior changes if you are looking for clear bits and specify a +range with both **start** and **end**. If no clear bit is found in the specified +range, the function returns -1 as the user specified a clear range and there are +no 0 bits in that range. + +@examples + +```cli +SET mykey "\xff\xf0\x00" +BITPOS mykey 0 +SET mykey "\x00\xff\xf0" +BITPOS mykey 1 0 +BITPOS mykey 1 2 +set mykey "\x00\x00\x00" +BITPOS mykey 1 +``` diff --git a/iredis/data/commands/blpop.md b/iredis/data/commands/blpop.md new file mode 100644 index 0000000..b48ace7 --- /dev/null +++ b/iredis/data/commands/blpop.md @@ -0,0 +1,182 @@ +`BLPOP` is a blocking list pop primitive. It is the blocking version of `LPOP` +because it blocks the connection when there are no elements to pop from any of +the given lists. An element is 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. + +## Non-blocking behavior + +When `BLPOP` is called, if at least one of the specified keys contains a +non-empty list, an element is popped from the head of the list and returned to +the caller together with the `key` it was popped from. + +Keys are checked in the order that they are given. Let's say that the key +`list1` doesn't exist and `list2` and `list3` hold non-empty lists. Consider the +following command: + +``` +BLPOP list1 list2 list3 0 +``` + +`BLPOP` guarantees to return an element from the list stored at `list2` (since +it is the first non empty list when checking `list1`, `list2` and `list3` in +that order). + +## Blocking behavior + +If none of the specified keys exist, `BLPOP` blocks the connection until another +client performs an `LPUSH` or `RPUSH` operation against one of the keys. + +Once new data is present on one of the lists, the client returns with the name +of the key unblocking it and the popped value. + +When `BLPOP` causes a client to block and a non-zero timeout is specified, the +client will unblock returning a `nil` multi-bulk value when the specified +timeout has expired without a push operation against at least one of the +specified keys. + +**The timeout argument is interpreted as an integer value specifying the maximum +number of seconds to block**. A timeout of zero can be used to block +indefinitely. + +## What key is served first? What client? What element? Priority ordering details. + +- If the client tries to blocks for multiple keys, but at least one key contains + elements, the returned key / element pair is the first key from left to right + that has one or more elements. In this case the client is not blocked. So for + instance `BLPOP key1 key2 key3 key4 0`, assuming that both `key2` and `key4` + are non-empty, will always return an element from `key2`. +- If multiple clients are blocked for the same key, the first client to be + served is the one that was waiting for more time (the first that blocked for + the key). Once a client is unblocked it does not retain any priority, when it + blocks again with the next call to `BLPOP` it will be served accordingly to + the number of clients already blocked for the same key, that will all be + served before it (from the first to the last that blocked). +- When a client is blocking for multiple keys at the same time, and elements are + available at the same time in multiple keys (because of a transaction or a Lua + script added elements to multiple lists), the client will be unblocked using + the first key that received a push operation (assuming it has enough elements + to serve our client, as there may be other clients as well waiting for this + key). Basically after the execution of every command Redis will run a list of + all the keys that received data AND that have at least a client blocked. The + list is ordered by new element arrival time, from the first key that received + data to the last. For every key processed, Redis will serve all the clients + waiting for that key in a FIFO fashion, as long as there are elements in this + key. When the key is empty or there are no longer clients waiting for this + key, the next key that received new data in the previous command / transaction + / script is processed, and so forth. + +## Behavior of `!BLPOP` when multiple elements are pushed inside a list. + +There are times when a list can receive multiple elements in the context of the +same conceptual command: + +- Variadic push operations such as `LPUSH mylist a b c`. +- After an `EXEC` of a `MULTI` block with multiple push operations against the + same list. +- Executing a Lua Script with Redis 2.6 or newer. + +When multiple elements are pushed inside a list where there are clients +blocking, the behavior is different for Redis 2.4 and Redis 2.6 or newer. + +For Redis 2.6 what happens is that the command performing multiple pushes is +executed, and _only after_ the execution of the command the blocked clients are +served. Consider this sequence of commands. + + Client A: BLPOP foo 0 + Client B: LPUSH foo a b c + +If the above condition happens using a Redis 2.6 server or greater, Client **A** +will be served with the `c` element, because after the `LPUSH` command the list +contains `c,b,a`, so taking an element from the left means to return `c`. + +Instead Redis 2.4 works in a different way: clients are served _in the context_ +of the push operation, so as long as `LPUSH foo a b c` starts pushing the first +element to the list, it will be delivered to the Client **A**, that will receive +`a` (the first element pushed). + +The behavior of Redis 2.4 creates a lot of problems when replicating or +persisting data into the AOF file, so the much more generic and semantically +simpler behavior was introduced into Redis 2.6 to prevent problems. + +Note that for the same reason a Lua script or a `MULTI/EXEC` block may push +elements into a list and afterward **delete the list**. In this case the blocked +clients will not be served at all and will continue to be blocked as long as no +data is present on the list after the execution of a single command, +transaction, or script. + +## `!BLPOP` inside a `!MULTI` / `!EXEC` transaction + +`BLPOP` can be used with pipelining (sending multiple commands and reading the +replies in batch), however this setup makes sense almost solely when it is the +last command of the pipeline. + +Using `BLPOP` inside a `MULTI` / `EXEC` block does not make a lot of sense as it +would require blocking the entire server in order to execute the block +atomically, which in turn does not allow other clients to perform a push +operation. For this reason the behavior of `BLPOP` inside `MULTI` / `EXEC` when +the list is empty is to return a `nil` multi-bulk reply, which is the same thing +that happens when the timeout is reached. + +If you like science fiction, think of time flowing at infinite speed inside a +`MULTI` / `EXEC` block... + +@return + +@array-reply: specifically: + +- A `nil` multi-bulk when no element could be popped and the timeout expired. +- A two-element multi-bulk with the first element being the name of the key + where an element was popped and the second element being the value of the + popped element. + +@examples + +``` +redis> DEL list1 list2 +(integer) 0 +redis> RPUSH list1 a b c +(integer) 3 +redis> BLPOP list1 list2 0 +1) "list1" +2) "a" +``` + +## Reliable queues + +When `BLPOP` returns an element to the client, it also removes the element from +the list. This means that the element only exists in the context of the client: +if the client crashes while processing the returned element, it is lost forever. + +This can be a problem with some application where we want a more reliable +messaging system. When this is the case, please check the `BRPOPLPUSH` command, +that is a variant of `BLPOP` that adds the returned element to a target list +before returning it to the client. + +## Pattern: Event notification + +Using blocking list operations it is possible to mount different blocking +primitives. For instance for some application you may need to block waiting for +elements into a Redis Set, so that as far as a new element is added to the Set, +it is possible to retrieve it without resort to polling. This would require a +blocking version of `SPOP` that is not available, but using blocking list +operations we can easily accomplish this task. + +The consumer will do: + +``` +LOOP forever + WHILE SPOP(key) returns elements + ... process elements ... + END + BRPOP helper_key +END +``` + +While in the producer side we'll use simply: + +``` +MULTI +SADD key element +LPUSH helper_key x +EXEC +``` diff --git a/iredis/data/commands/brpop.md b/iredis/data/commands/brpop.md new file mode 100644 index 0000000..e0bb650 --- /dev/null +++ b/iredis/data/commands/brpop.md @@ -0,0 +1,31 @@ +`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. + +See the [BLPOP documentation][cb] for the exact semantics, since `BRPOP` is +identical to `BLPOP` with the only difference being that it pops elements from +the tail of a list instead of popping from the head. + +[cb]: /commands/blpop + +@return + +@array-reply: specifically: + +- A `nil` multi-bulk when no element could be popped and the timeout expired. +- A two-element multi-bulk with the first element being the name of the key + where an element was popped and the second element being the value of the + popped element. + +@examples + +``` +redis> DEL list1 list2 +(integer) 0 +redis> RPUSH list1 a b c +(integer) 3 +redis> BRPOP list1 list2 0 +1) "list1" +2) "c" +``` diff --git a/iredis/data/commands/brpoplpush.md b/iredis/data/commands/brpoplpush.md new file mode 100644 index 0000000..1c3a9b3 --- /dev/null +++ b/iredis/data/commands/brpoplpush.md @@ -0,0 +1,21 @@ +`BRPOPLPUSH` is the blocking variant of `RPOPLPUSH`. When `source` contains +elements, this command behaves exactly like `RPOPLPUSH`. When used inside a +`MULTI`/`EXEC` block, this command behaves exactly like `RPOPLPUSH`. When +`source` is empty, Redis will block the connection until another client pushes +to it or until `timeout` is reached. A `timeout` of zero can be used to block +indefinitely. + +See `RPOPLPUSH` for more information. + +@return + +@bulk-string-reply: the element being popped from `source` and pushed to +`destination`. If `timeout` is reached, a @nil-reply is returned. + +## Pattern: Reliable queue + +Please see the pattern description in the `RPOPLPUSH` documentation. + +## Pattern: Circular list + +Please see the pattern description in the `RPOPLPUSH` documentation. diff --git a/iredis/data/commands/bzpopmax.md b/iredis/data/commands/bzpopmax.md new file mode 100644 index 0000000..a061b0e --- /dev/null +++ b/iredis/data/commands/bzpopmax.md @@ -0,0 +1,37 @@ +`BZPOPMAX` is the blocking variant of the sorted set `ZPOPMAX` primitive. + +It is the blocking version because it blocks the connection when there are no +members to pop from any of the given sorted sets. A member with the highest +score is popped from first sorted set that is non-empty, with the given keys +being checked in the order that they are given. + +The `timeout` argument is interpreted as an integer value specifying the maximum +number of seconds to block. A timeout of zero can be used to block indefinitely. + +See the [BZPOPMIN documentation][cb] for the exact semantics, since `BZPOPMAX` +is identical to `BZPOPMIN` with the only difference being that it pops members +with the highest scores instead of popping the ones with the lowest scores. + +[cb]: /commands/bzpopmin + +@return + +@array-reply: specifically: + +- A `nil` multi-bulk when no element could be popped and the timeout expired. +- A three-element multi-bulk with the first element being the name of the key + where a member was popped, the second element is the popped member itself, and + the third element is the score of the popped element. + +@examples + +``` +redis> DEL zset1 zset2 +(integer) 0 +redis> ZADD zset1 0 a 1 b 2 c +(integer) 3 +redis> BZPOPMAX zset1 zset2 0 +1) "zset1" +2) "c" +3) "2" +``` diff --git a/iredis/data/commands/bzpopmin.md b/iredis/data/commands/bzpopmin.md new file mode 100644 index 0000000..118a821 --- /dev/null +++ b/iredis/data/commands/bzpopmin.md @@ -0,0 +1,37 @@ +`BZPOPMIN` is the blocking variant of the sorted set `ZPOPMIN` primitive. + +It is the blocking version because it blocks the connection when there are no +members to pop from any of the given sorted sets. A member with the lowest score +is popped from first sorted set that is non-empty, with the given keys being +checked in the order that they are given. + +The `timeout` argument is interpreted as an integer value specifying the maximum +number of seconds to block. A timeout of zero can be used to block indefinitely. + +See the [BLPOP documentation][cl] for the exact semantics, since `BZPOPMIN` is +identical to `BLPOP` with the only difference being the data structure being +popped from. + +[cl]: /commands/blpop + +@return + +@array-reply: specifically: + +- A `nil` multi-bulk when no element could be popped and the timeout expired. +- A three-element multi-bulk with the first element being the name of the key + where a member was popped, the second element is the popped member itself, and + the third element is the score of the popped element. + +@examples + +``` +redis> DEL zset1 zset2 +(integer) 0 +redis> ZADD zset1 0 a 1 b 2 c +(integer) 3 +redis> BZPOPMIN zset1 zset2 0 +1) "zset1" +2) "a" +3) "0" +``` diff --git a/iredis/data/commands/client-caching.md b/iredis/data/commands/client-caching.md new file mode 100644 index 0000000..7bbb439 --- /dev/null +++ b/iredis/data/commands/client-caching.md @@ -0,0 +1,19 @@ +This command controls the tracking of the keys in the next command executed by +the connection, when tracking is enabled in `OPTIN` or `OPTOUT` mode. Please +check the [client side caching documentation](/topics/client-side-caching) for +background informations. + +When tracking is enabled Redis, using the `CLIENT TRACKING` command, it is +possible to specify the `OPTIN` or `OPTOUT` options, so that keys in read only +commands are not automatically remembered by the server to be invalidated later. +When we are in `OPTIN` mode, we can enable the tracking of the keys in the next +command by calling `CLIENT CACHING yes` immediately before it. Similarly when we +are in `OPTOUT` mode, and keys are normally tracked, we can avoid the keys in +the next command to be tracked using `CLIENT CACHING no`. + +Basically the command sets a state in the connection, that is valid only for the +next command execution, that will modify the behavior of client tracking. + +@return + +@simple-string-reply: `OK` or an error if the argument is not yes or no. diff --git a/iredis/data/commands/client-getname.md b/iredis/data/commands/client-getname.md new file mode 100644 index 0000000..e91d8ae --- /dev/null +++ b/iredis/data/commands/client-getname.md @@ -0,0 +1,7 @@ +The `CLIENT GETNAME` returns the name of the current connection as set by +`CLIENT SETNAME`. Since every new connection starts without an associated name, +if no name was assigned a null bulk reply is returned. + +@return + +@bulk-string-reply: The connection name, or a null bulk reply if no name is set. diff --git a/iredis/data/commands/client-getredir.md b/iredis/data/commands/client-getredir.md new file mode 100644 index 0000000..ddaa1b7 --- /dev/null +++ b/iredis/data/commands/client-getredir.md @@ -0,0 +1,13 @@ +This command returns the client ID we are redirecting our +[tracking](/topics/client-side-caching) notifications to. We set a client to +redirect to when using `CLIENT TRACKING` to enable tracking. However in order to +avoid forcing client libraries implementations to remember the ID notifications +are redirected to, this command exists in order to improve introspection and +allow clients to check later if redirection is active and towards which client +ID. + +@return + +@integer-reply: the ID of the client we are redirecting the notifications to. +The command returns `-1` if client tracking is not enabled, or `0` if client +tracking is enabled but we are not redirecting the notifications to any client. diff --git a/iredis/data/commands/client-id.md b/iredis/data/commands/client-id.md new file mode 100644 index 0000000..d242d6f --- /dev/null +++ b/iredis/data/commands/client-id.md @@ -0,0 +1,25 @@ +The command just returns the ID of the current connection. Every connection ID +has certain guarantees: + +1. It is never repeated, so if `CLIENT ID` returns the same number, the caller + can be sure that the underlying client did not disconnect and reconnect the + connection, but it is still the same connection. +2. The ID is monotonically incremental. If the ID of a connection is greater + than the ID of another connection, it is guaranteed that the second + connection was established with the server at a later time. + +This command is especially useful together with `CLIENT UNBLOCK` which was +introduced also in Redis 5 together with `CLIENT ID`. Check the `CLIENT UNBLOCK` +command page for a pattern involving the two commands. + +@examples + +```cli +CLIENT ID +``` + +@return + +@integer-reply + +The id of the client. diff --git a/iredis/data/commands/client-kill.md b/iredis/data/commands/client-kill.md new file mode 100644 index 0000000..9cdb054 --- /dev/null +++ b/iredis/data/commands/client-kill.md @@ -0,0 +1,73 @@ +The `CLIENT KILL` command closes a given client connection. Up to Redis 2.8.11 +it was possible to close a connection only by client address, using the +following form: + + CLIENT KILL addr:port + +The `ip:port` should match a line returned by the `CLIENT LIST` command (`addr` +field). + +However starting with Redis 2.8.12 or greater, the command accepts the following +form: + + CLIENT KILL <filter> <value> ... ... <filter> <value> + +With the new form it is possible to kill clients by different attributes instead +of killing just by address. The following filters are available: + +- `CLIENT KILL ADDR ip:port`. This is exactly the same as the old + three-arguments behavior. +- `CLIENT KILL ID client-id`. Allows to kill a client by its unique `ID` field, + which was introduced in the `CLIENT LIST` command starting from Redis 2.8.12. +- `CLIENT KILL TYPE type`, where _type_ is one of `normal`, `master`, `slave` + and `pubsub` (the `master` type is available from v3.2). This closes the + connections of **all the clients** in the specified class. Note that clients + blocked into the `MONITOR` command are considered to belong to the `normal` + class. +- `CLIENT KILL USER username`. Closes all the connections that are authenticated + with the specified [ACL](/topics/acl) username, however it returns an error if + the username does not map to an existing ACL user. +- `CLIENT KILL SKIPME yes/no`. By default this option is set to `yes`, that is, + the client calling the command will not get killed, however setting this + option to `no` will have the effect of also killing the client calling the + command. + +**Note: starting with Redis 5 the project is no longer using the slave word. You +can use `TYPE replica` instead, however the old form is still supported for +backward compatibility.** + +It is possible to provide multiple filters at the same time. The command will +handle multiple filters via logical AND. For example: + + CLIENT KILL addr 127.0.0.1:12345 type pubsub + +is valid and will kill only a pubsub client with the specified address. This +format containing multiple filters is rarely useful currently. + +When the new form is used the command no longer returns `OK` or an error, but +instead the number of killed clients, that may be zero. + +## CLIENT KILL and Redis Sentinel + +Recent versions of Redis Sentinel (Redis 2.8.12 or greater) use CLIENT KILL in +order to kill clients when an instance is reconfigured, in order to force +clients to perform the handshake with one Sentinel again and update its +configuration. + +## Notes + +Due to the single-threaded nature of Redis, it is not possible to kill a client +connection while it is executing a command. From the client point of view, the +connection can never be closed in the middle of the execution of a command. +However, the client will notice the connection has been closed only when the +next command is sent (and results in network error). + +@return + +When called with the three arguments format: + +@simple-string-reply: `OK` if the connection exists and has been closed + +When called with the filter / value format: + +@integer-reply: the number of clients killed. diff --git a/iredis/data/commands/client-list.md b/iredis/data/commands/client-list.md new file mode 100644 index 0000000..ea0b775 --- /dev/null +++ b/iredis/data/commands/client-list.md @@ -0,0 +1,70 @@ +The `CLIENT LIST` command returns information and statistics about the client +connections server in a mostly human readable format. + +As of v5.0, the optional `TYPE type` subcommand can be used to filter the list +by clients' type, where _type_ is one of `normal`, `master`, `replica` and +`pubsub`. Note that clients blocked into the `MONITOR` command are considered to +belong to the `normal` class. + +@return + +@bulk-string-reply: a unique string, formatted as follows: + +- One client connection per line (separated by LF) +- Each line is composed of a succession of `property=value` fields separated by + a space character. + +Here is the meaning of the fields: + +- `id`: an unique 64-bit client ID (introduced in Redis 2.8.12). +- `name`: the name set by the client with `CLIENT SETNAME` +- `addr`: address/port of the client +- `fd`: file descriptor corresponding to the socket +- `age`: total duration of the connection in seconds +- `idle`: idle time of the connection in seconds +- `flags`: client flags (see below) +- `db`: current database ID +- `sub`: number of channel subscriptions +- `psub`: number of pattern matching subscriptions +- `multi`: number of commands in a MULTI/EXEC context +- `qbuf`: query buffer length (0 means no query pending) +- `qbuf-free`: free space of the query buffer (0 means the buffer is full) +- `obl`: output buffer length +- `oll`: output list length (replies are queued in this list when the buffer is + full) +- `omem`: output buffer memory usage +- `events`: file descriptor events (see below) +- `cmd`: last command played + +The client flags can be a combination of: + +``` +A: connection to be closed ASAP +b: the client is waiting in a blocking operation +c: connection to be closed after writing entire reply +d: a watched keys has been modified - EXEC will fail +i: the client is waiting for a VM I/O (deprecated) +M: the client is a master +N: no specific flag set +O: the client is a client in MONITOR mode +P: the client is a Pub/Sub subscriber +r: the client is in readonly mode against a cluster node +S: the client is a replica node connection to this instance +u: the client is unblocked +U: the client is connected via a Unix domain socket +x: the client is in a MULTI/EXEC context +``` + +The file descriptor events can be: + +``` +r: the client socket is readable (event loop) +w: the client socket is writable (event loop) +``` + +## Notes + +New fields are regularly added for debugging purpose. Some could be removed in +the future. A version safe Redis client using this command should parse the +output accordingly (i.e. handling gracefully missing fields, skipping unknown +fields). diff --git a/iredis/data/commands/client-pause.md b/iredis/data/commands/client-pause.md new file mode 100644 index 0000000..bdbdda0 --- /dev/null +++ b/iredis/data/commands/client-pause.md @@ -0,0 +1,38 @@ +`CLIENT PAUSE` is a connections control command able to suspend all the Redis +clients for the specified amount of time (in milliseconds). + +The command performs the following actions: + +- It stops processing all the pending commands from normal and pub/sub clients. + However interactions with replicas will continue normally. +- However it returns OK to the caller ASAP, so the `CLIENT PAUSE` command + execution is not paused by itself. +- When the specified amount of time has elapsed, all the clients are unblocked: + this will trigger the processing of all the commands accumulated in the query + buffer of every client during the pause. + +This command is useful as it makes able to switch clients from a Redis instance +to another one in a controlled way. For example during an instance upgrade the +system administrator could do the following: + +- Pause the clients using `CLIENT PAUSE` +- Wait a few seconds to make sure the replicas processed the latest replication + stream from the master. +- Turn one of the replicas into a master. +- Reconfigure clients to connect with the new master. + +It is possible to send `CLIENT PAUSE` in a MULTI/EXEC block together with the +`INFO replication` command in order to get the current master offset at the time +the clients are blocked. This way it is possible to wait for a specific offset +in the replica side in order to make sure all the replication stream was +processed. + +Since Redis 3.2.10 / 4.0.0, this command also prevents keys to be evicted or +expired during the time clients are paused. This way the dataset is guaranteed +to be static not just from the point of view of clients not being able to write, +but also from the point of view of internal operations. + +@return + +@simple-string-reply: The command returns OK or an error if the timeout is +invalid. diff --git a/iredis/data/commands/client-reply.md b/iredis/data/commands/client-reply.md new file mode 100644 index 0000000..fe8ed94 --- /dev/null +++ b/iredis/data/commands/client-reply.md @@ -0,0 +1,21 @@ +Sometimes it can be useful for clients to completely disable replies from the +Redis server. For example when the client sends fire and forget commands or +performs a mass loading of data, or in caching contexts where new data is +streamed constantly. In such contexts to use server time and bandwidth in order +to send back replies to clients, which are going to be ignored, is considered +wasteful. + +The `CLIENT REPLY` command controls whether the server will reply the client's +commands. The following modes are available: + +- `ON`. This is the default mode in which the server returns a reply to every + command. +- `OFF`. In this mode the server will not reply to client commands. +- `SKIP`. This mode skips the reply of command immediately after it. + +@return + +When called with either `OFF` or `SKIP` subcommands, no reply is made. When +called with `ON`: + +@simple-string-reply: `OK`. diff --git a/iredis/data/commands/client-setname.md b/iredis/data/commands/client-setname.md new file mode 100644 index 0000000..0155a42 --- /dev/null +++ b/iredis/data/commands/client-setname.md @@ -0,0 +1,28 @@ +The `CLIENT SETNAME` command assigns a name to the current connection. + +The assigned name is displayed in the output of `CLIENT LIST` so that it is +possible to identify the client that performed a given connection. + +For instance when Redis is used in order to implement a queue, producers and +consumers of messages may want to set the name of the connection according to +their role. + +There is no limit to the length of the name that can be assigned if not the +usual limits of the Redis string type (512 MB). However it is not possible to +use spaces in the connection name as this would violate the format of the +`CLIENT LIST` reply. + +It is possible to entirely remove the connection name setting it to the empty +string, that is not a valid connection name since it serves to this specific +purpose. + +The connection name can be inspected using `CLIENT GETNAME`. + +Every new connection starts without an assigned name. + +Tip: setting names to connections is a good way to debug connection leaks due to +bugs in the application using Redis. + +@return + +@simple-string-reply: `OK` if the connection name was successfully set. diff --git a/iredis/data/commands/client-tracking.md b/iredis/data/commands/client-tracking.md new file mode 100644 index 0000000..197c0aa --- /dev/null +++ b/iredis/data/commands/client-tracking.md @@ -0,0 +1,54 @@ +This command enables the tracking feature of the Redis server, that is used for +[server assisted client side caching](/topics/client-side-caching). + +When tracking is enabled Redis remembers the keys that the connection requested, +in order to send later invalidation messages when such keys are modified. +Invalidation messages are sent in the same connection (only available when the +RESP3 protocol is used) or redirected in a different connection (available also +with RESP2 and Pub/Sub). A special _broadcasting_ mode is available where +clients participating in this protocol receive every notification just +subscribing to given key prefixes, regardless of the keys that they requested. +Given the complexity of the argument please refer to +[the main client side caching documentation](/topics/client-side-caching) for +the details. This manual page is only a reference for the options of this +subcommand. + +In order to enable tracking, use: + + CLIENT TRACKING on ... options ... + +The feature will remain active in the current connection for all its life, +unless tracking is turned on with `CLIENT TRACKING off` at some point. + +The following are the list of options that modify the behavior of the command +when enabling tracking: + +- `REDIRECT <id>`: send redirection messages to the connection with the + specified ID. The connection must exist, you can get the ID of such connection + using `CLIENT ID`. If the connection we are redirecting to is terminated, when + in RESP3 mode the connection with tracking enabled will receive + `tracking-redir-broken` push messages in order to signal the condition. +- `BCAST`: enable tracking in broadcasting mode. In this mode invalidation + messages are reported for all the prefixes specified, regardless of the keys + requested by the connection. Instead when the broadcasting mode is not + enabled, Redis will track which keys are fetched using read-only commands, and + will report invalidation messages only for such keys. +- `PREFIX <prefix>`: for broadcasting, register a given key prefix, so that + notifications will be provided only for keys starting with this string. This + option can be given multiple times to register multiple prefixes. If + broadcasting is enabled without this option, Redis will send notifications for + every key. +- `OPTIN`: when broadcasting is NOT active, normally don't track keys in read + only commands, unless they are called immediately after a `CLIENT CACHING yes` + command. +- `OPTOUT`: when broadcasting is NOT active, normally track keys in read only + commands, unless they are called immediately after a `CLIENT CACHING no` + command. +- `NOLOOP`: don't send notifications about keys modified by this connection + itself. + +@return + +@simple-string-reply: `OK` if the connection was successfully put in tracking +mode or if the tracking mode was successfully disabled. Otherwise an error is +returned. diff --git a/iredis/data/commands/client-unblock.md b/iredis/data/commands/client-unblock.md new file mode 100644 index 0000000..dae5fb3 --- /dev/null +++ b/iredis/data/commands/client-unblock.md @@ -0,0 +1,63 @@ +This command can unblock, from a different connection, a client blocked in a +blocking operation, such as for instance `BRPOP` or `XREAD` or `WAIT`. + +By default the client is unblocked as if the timeout of the command was reached, +however if an additional (and optional) argument is passed, it is possible to +specify the unblocking behavior, that can be **TIMEOUT** (the default) or +**ERROR**. If **ERROR** is specified, the behavior is to unblock the client +returning as error the fact that the client was force-unblocked. Specifically +the client will receive the following error: + + -UNBLOCKED client unblocked via CLIENT UNBLOCK + +Note: of course as usually it is not guaranteed that the error text remains the +same, however the error code will remain `-UNBLOCKED`. + +This command is useful especially when we are monitoring many keys with a +limited number of connections. For instance we may want to monitor multiple +streams with `XREAD` without using more than N connections. However at some +point the consumer process is informed that there is one more stream key to +monitor. In order to avoid using more connections, the best behavior would be to +stop the blocking command from one of the connections in the pool, add the new +key, and issue the blocking command again. + +To obtain this behavior the following pattern is used. The process uses an +additional _control connection_ in order to send the `CLIENT UNBLOCK` command if +needed. In the meantime, before running the blocking operation on the other +connections, the process runs `CLIENT ID` in order to get the ID associated with +that connection. When a new key should be added, or when a key should no longer +be monitored, the relevant connection blocking command is aborted by sending +`CLIENT UNBLOCK` in the control connection. The blocking command will return and +can be finally reissued. + +This example shows the application in the context of Redis streams, however the +pattern is a general one and can be applied to other cases. + +@example + +``` +Connection A (blocking connection): +> CLIENT ID +2934 +> BRPOP key1 key2 key3 0 +(client is blocked) + +... Now we want to add a new key ... + +Connection B (control connection): +> CLIENT UNBLOCK 2934 +1 + +Connection A (blocking connection): +... BRPOP reply with timeout ... +NULL +> BRPOP key1 key2 key3 key4 0 +(client is blocked again) +``` + +@return + +@integer-reply, specifically: + +- `1` if the client was unblocked successfully. +- `0` if the client wasn't unblocked. diff --git a/iredis/data/commands/cluster-addslots.md b/iredis/data/commands/cluster-addslots.md new file mode 100644 index 0000000..f5668e5 --- /dev/null +++ b/iredis/data/commands/cluster-addslots.md @@ -0,0 +1,55 @@ +This command is useful in order to modify a node's view of the cluster +configuration. Specifically it assigns a set of hash slots to the node receiving +the command. If the command is successful, the node will map the specified hash +slots to itself, and will start broadcasting the new configuration. + +However note that: + +1. The command only works if all the specified slots are, from the point of view + of the node receiving the command, currently not assigned. A node will refuse + to take ownership for slots that already belong to some other node (including + itself). +2. The command fails if the same slot is specified multiple times. +3. As a side effect of the command execution, if a slot among the ones specified + as argument is set as `importing`, this state gets cleared once the node + assigns the (previously unbound) slot to itself. + +## Example + +For example the following command assigns slots 1 2 3 to the node receiving the +command: + + > CLUSTER ADDSLOTS 1 2 3 + OK + +However trying to execute it again results into an error since the slots are +already assigned: + + > CLUSTER ADDSLOTS 1 2 3 + ERR Slot 1 is already busy + +## Usage in Redis Cluster + +This command only works in cluster mode and is useful in the following Redis +Cluster operations: + +1. To create a new cluster ADDSLOTS is used in order to initially setup master + nodes splitting the available hash slots among them. +2. In order to fix a broken cluster where certain slots are unassigned. + +## Information about slots propagation and warnings + +Note that once a node assigns a set of slots to itself, it will start +propagating this information in heartbeat packet headers. However the other +nodes will accept the information only if they have the slot as not already +bound with another node, or if the configuration epoch of the node advertising +the new hash slot, is greater than the node currently listed in the table. + +This means that this command should be used with care only by applications +orchestrating Redis Cluster, like `redis-trib`, and the command if used out of +the right context can leave the cluster in a wrong state or cause data loss. + +@return + +@simple-string-reply: `OK` if the command was successful. Otherwise an error is +returned. diff --git a/iredis/data/commands/cluster-bumpepoch.md b/iredis/data/commands/cluster-bumpepoch.md new file mode 100644 index 0000000..16a94a4 --- /dev/null +++ b/iredis/data/commands/cluster-bumpepoch.md @@ -0,0 +1,15 @@ +Advances the cluster config epoch. + +The `CLUSTER BUMPEPOCH` command triggers an increment to the cluster's config +epoch from the connected node. The epoch will be incremented if the node's +config epoch is zero, or if it is less than the cluster's greatest epoch. + +**Note:** config epoch management is performed internally by the cluster, and +relies on obtaining a consensus of nodes. The `CLUSTER BUMPEPOCH` attempts to +increment the config epoch **WITHOUT** getting the consensus, so using it may +violate the "last failover wins" rule. Use it with caution. + +@return + +@simple-string-reply: `BUMPED` if the epoch was incremented, or `STILL` if the +node already has the greatest config epoch in the cluster. diff --git a/iredis/data/commands/cluster-count-failure-reports.md b/iredis/data/commands/cluster-count-failure-reports.md new file mode 100644 index 0000000..bb3c937 --- /dev/null +++ b/iredis/data/commands/cluster-count-failure-reports.md @@ -0,0 +1,34 @@ +The command returns the number of _failure reports_ for the specified node. +Failure reports are the way Redis Cluster uses in order to promote a `PFAIL` +state, that means a node is not reachable, to a `FAIL` state, that means that +the majority of masters in the cluster agreed within a window of time that the +node is not reachable. + +A few more details: + +- A node flags another node with `PFAIL` when the node is not reachable for a + time greater than the configured _node timeout_, which is a fundamental + configuration parameter of a Redis Cluster. +- Nodes in `PFAIL` state are provided in gossip sections of heartbeat packets. +- Every time a node processes gossip packets from other nodes, it creates (and + refreshes the TTL if needed) **failure reports**, remembering that a given + node said another given node is in `PFAIL` condition. +- Each failure report has a time to live of two times the _node timeout_ time. +- If at a given time a node has another node flagged with `PFAIL`, and at the + same time collected the majority of other master nodes _failure reports_ about + this node (including itself if it is a master), then it elevates the failure + state of the node from `PFAIL` to `FAIL`, and broadcasts a message forcing all + the nodes that can be reached to flag the node as `FAIL`. + +This command returns the number of failure reports for the current node which +are currently not expired (so received within two times the _node timeout_ +time). The count does not include what the node we are asking this count +believes about the node ID we pass as argument, the count _only_ includes the +failure reports the node received from other nodes. + +This command is mainly useful for debugging, when the failure detector of Redis +Cluster is not operating as we believe it should. + +@return + +@integer-reply: the number of active failure reports for the node. diff --git a/iredis/data/commands/cluster-countkeysinslot.md b/iredis/data/commands/cluster-countkeysinslot.md new file mode 100644 index 0000000..92ec7c6 --- /dev/null +++ b/iredis/data/commands/cluster-countkeysinslot.md @@ -0,0 +1,13 @@ +Returns the number of keys in the specified Redis Cluster hash slot. The command +only queries the local data set, so contacting a node that is not serving the +specified hash slot will always result in a count of zero being returned. + +``` +> CLUSTER COUNTKEYSINSLOT 7000 +(integer) 50341 +``` + +@return + +@integer-reply: The number of keys in the specified hash slot, or an error if +the hash slot is invalid. diff --git a/iredis/data/commands/cluster-delslots.md b/iredis/data/commands/cluster-delslots.md new file mode 100644 index 0000000..1924158 --- /dev/null +++ b/iredis/data/commands/cluster-delslots.md @@ -0,0 +1,47 @@ +In Redis Cluster, each node keeps track of which master is serving a particular +hash slot. + +The `DELSLOTS` command asks a particular Redis Cluster node to forget which +master is serving the hash slots specified as arguments. + +In the context of a node that has received a `DELSLOTS` command and has +consequently removed the associations for the passed hash slots, we say those +hash slots are _unbound_. Note that the existence of unbound hash slots occurs +naturally when a node has not been configured to handle them (something that can +be done with the `ADDSLOTS` command) and if it has not received any information +about who owns those hash slots (something that it can learn from heartbeat or +update messages). + +If a node with unbound hash slots receives a heartbeat packet from another node +that claims to be the owner of some of those hash slots, the association is +established instantly. Moreover, if a heartbeat or update message is received +with a configuration epoch greater than the node's own, the association is +re-established. + +However, note that: + +1. The command only works if all the specified slots are already associated with + some node. +2. The command fails if the same slot is specified multiple times. +3. As a side effect of the command execution, the node may go into _down_ state + because not all hash slots are covered. + +## Example + +The following command removes the association for slots 5000 and 5001 from the +node receiving the command: + + > CLUSTER DELSLOTS 5000 5001 + OK + +## Usage in Redis Cluster + +This command only works in cluster mode and may be useful for debugging and in +order to manually orchestrate a cluster configuration when a new cluster is +created. It is currently not used by `redis-trib`, and mainly exists for API +completeness. + +@return + +@simple-string-reply: `OK` if the command was successful. Otherwise an error is +returned. diff --git a/iredis/data/commands/cluster-failover.md b/iredis/data/commands/cluster-failover.md new file mode 100644 index 0000000..c811c04 --- /dev/null +++ b/iredis/data/commands/cluster-failover.md @@ -0,0 +1,81 @@ +This command, that can only be sent to a Redis Cluster replica node, forces the +replica to start a manual failover of its master instance. + +A manual failover is a special kind of failover that is usually executed when +there are no actual failures, but we wish to swap the current master with one of +its replicas (which is the node we send the command to), in a safe way, without +any window for data loss. It works in the following way: + +1. The replica tells the master to stop processing queries from clients. +2. The master replies to the replica with the current _replication offset_. +3. The replica waits for the replication offset to match on its side, to make + sure it processed all the data from the master before it continues. +4. The replica starts a failover, obtains a new configuration epoch from the + majority of the masters, and broadcasts the new configuration. +5. The old master receives the configuration update: unblocks its clients and + starts replying with redirection messages so that they'll continue the chat + with the new master. + +This way clients are moved away from the old master to the new master atomically +and only when the replica that is turning into the new master has processed all +of the replication stream from the old master. + +## FORCE option: manual failover when the master is down + +The command behavior can be modified by two options: **FORCE** and **TAKEOVER**. + +If the **FORCE** option is given, the replica does not perform any handshake +with the master, that may be not reachable, but instead just starts a failover +ASAP starting from point 4. This is useful when we want to start a manual +failover while the master is no longer reachable. + +However using **FORCE** we still need the majority of masters to be available in +order to authorize the failover and generate a new configuration epoch for the +replica that is going to become master. + +## TAKEOVER option: manual failover without cluster consensus + +There are situations where this is not enough, and we want a replica to failover +without any agreement with the rest of the cluster. A real world use case for +this is to mass promote replicas in a different data center to masters in order +to perform a data center switch, while all the masters are down or partitioned +away. + +The **TAKEOVER** option implies everything **FORCE** implies, but also does not +uses any cluster authorization in order to failover. A replica receiving +`CLUSTER FAILOVER TAKEOVER` will instead: + +1. Generate a new `configEpoch` unilaterally, just taking the current greatest + epoch available and incrementing it if its local configuration epoch is not + already the greatest. +2. Assign itself all the hash slots of its master, and propagate the new + configuration to every node which is reachable ASAP, and eventually to every + other node. + +Note that **TAKEOVER violates the last-failover-wins principle** of Redis +Cluster, since the configuration epoch generated by the replica violates the +normal generation of configuration epochs in several ways: + +1. There is no guarantee that it is actually the higher configuration epoch, + since, for example, we can use the **TAKEOVER** option within a minority, nor + any message exchange is performed to generate the new configuration epoch. +2. If we generate a configuration epoch which happens to collide with another + instance, eventually our configuration epoch, or the one of another instance + with our same epoch, will be moved away using the _configuration epoch + collision resolution algorithm_. + +Because of this the **TAKEOVER** option should be used with care. + +## Implementation details and notes + +`CLUSTER FAILOVER`, unless the **TAKEOVER** option is specified, does not +execute a failover synchronously, it only _schedules_ a manual failover, +bypassing the failure detection stage, so to check if the failover actually +happened, `CLUSTER NODES` or other means should be used in order to verify that +the state of the cluster changes after some time the command was sent. + +@return + +@simple-string-reply: `OK` if the command was accepted and a manual failover is +going to be attempted. An error if the operation cannot be executed, for example +if we are talking with a node which is already a master. diff --git a/iredis/data/commands/cluster-flushslots.md b/iredis/data/commands/cluster-flushslots.md new file mode 100644 index 0000000..74204d1 --- /dev/null +++ b/iredis/data/commands/cluster-flushslots.md @@ -0,0 +1,8 @@ +Deletes all slots from a node. + +The `CLUSTER FLUSHSLOTS` deletes all information about slots from the connected +node. It can only be called when the database is empty. + +@reply + +@simple-string-reply: `OK` diff --git a/iredis/data/commands/cluster-forget.md b/iredis/data/commands/cluster-forget.md new file mode 100644 index 0000000..7926091 --- /dev/null +++ b/iredis/data/commands/cluster-forget.md @@ -0,0 +1,59 @@ +The command is used in order to remove a node, specified via its node ID, from +the set of _known nodes_ of the Redis Cluster node receiving the command. In +other words the specified node is removed from the _nodes table_ of the node +receiving the command. + +Because when a given node is part of the cluster, all the other nodes +participating in the cluster knows about it, in order for a node to be +completely removed from a cluster, the `CLUSTER FORGET` command must be sent to +all the remaining nodes, regardless of the fact they are masters or replicas. + +However the command cannot simply drop the node from the internal node table of +the node receiving the command, it also implements a ban-list, not allowing the +same node to be added again as a side effect of processing the _gossip section_ +of the heartbeat packets received from other nodes. + +## Details on why the ban-list is needed + +In the following example we'll show why the command must not just remove a given +node from the nodes table, but also prevent it for being re-inserted again for +some time. + +Let's assume we have four nodes, A, B, C and D. In order to end with just a +three nodes cluster A, B, C we may follow these steps: + +1. Reshard all the hash slots from D to nodes A, B, C. +2. D is now empty, but still listed in the nodes table of A, B and C. +3. We contact A, and send `CLUSTER FORGET D`. +4. B sends node A a heartbeat packet, where node D is listed. +5. A does no longer known node D (see step 3), so it starts an handshake with D. +6. D ends re-added in the nodes table of A. + +As you can see in this way removing a node is fragile, we need to send +`CLUSTER FORGET` commands to all the nodes ASAP hoping there are no gossip +sections processing in the meantime. Because of this problem the command +implements a ban-list with an expire time for each entry. + +So what the command really does is: + +1. The specified node gets removed from the nodes table. +2. The node ID of the removed node gets added to the ban-list, for 1 minute. +3. The node will skip all the node IDs listed in the ban-list when processing + gossip sections received in heartbeat packets from other nodes. + +This way we have a 60 second window to inform all the nodes in the cluster that +we want to remove a node. + +## Special conditions not allowing the command execution + +The command does not succeed and returns an error in the following cases: + +1. The specified node ID is not found in the nodes table. +2. The node receiving the command is a replica, and the specified node ID + identifies its current master. +3. The node ID identifies the same node we are sending the command to. + +@return + +@simple-string-reply: `OK` if the command was executed successfully, otherwise +an error is returned. diff --git a/iredis/data/commands/cluster-getkeysinslot.md b/iredis/data/commands/cluster-getkeysinslot.md new file mode 100644 index 0000000..9faa62d --- /dev/null +++ b/iredis/data/commands/cluster-getkeysinslot.md @@ -0,0 +1,20 @@ +The command returns an array of keys names stored in the contacted node and +hashing to the specified hash slot. The maximum number of keys to return is +specified via the `count` argument, so that it is possible for the user of this +API to batch-processing keys. + +The main usage of this command is during rehashing of cluster slots from one +node to another. The way the rehashing is performed is exposed in the Redis +Cluster specification, or in a more simple to digest form, as an appendix of the +`CLUSTER SETSLOT` command documentation. + +``` +> CLUSTER GETKEYSINSLOT 7000 3 +"47344|273766|70329104160040|key_39015" +"47344|273766|70329104160040|key_89793" +"47344|273766|70329104160040|key_92937" +``` + +@return + +@array-reply: From 0 to _count_ key names in a Redis array reply. diff --git a/iredis/data/commands/cluster-info.md b/iredis/data/commands/cluster-info.md new file mode 100644 index 0000000..550dd0c --- /dev/null +++ b/iredis/data/commands/cluster-info.md @@ -0,0 +1,56 @@ +`CLUSTER INFO` provides `INFO` style information about Redis Cluster vital +parameters. The following is a sample output, followed by the description of +each field reported. + +``` +cluster_state:ok +cluster_slots_assigned:16384 +cluster_slots_ok:16384 +cluster_slots_pfail:0 +cluster_slots_fail:0 +cluster_known_nodes:6 +cluster_size:3 +cluster_current_epoch:6 +cluster_my_epoch:2 +cluster_stats_messages_sent:1483972 +cluster_stats_messages_received:1483968 +``` + +- `cluster_state`: State is `ok` if the node is able to receive queries. `fail` + if there is at least one hash slot which is unbound (no node associated), in + error state (node serving it is flagged with FAIL flag), or if the majority of + masters can't be reached by this node. +- `cluster_slots_assigned`: Number of slots which are associated to some node + (not unbound). This number should be 16384 for the node to work properly, + which means that each hash slot should be mapped to a node. +- `cluster_slots_ok`: Number of hash slots mapping to a node not in `FAIL` or + `PFAIL` state. +- `cluster_slots_pfail`: Number of hash slots mapping to a node in `PFAIL` + state. Note that those hash slots still work correctly, as long as the `PFAIL` + state is not promoted to `FAIL` by the failure detection algorithm. `PFAIL` + only means that we are currently not able to talk with the node, but may be + just a transient error. +- `cluster_slots_fail`: Number of hash slots mapping to a node in `FAIL` state. + If this number is not zero the node is not able to serve queries unless + `cluster-require-full-coverage` is set to `no` in the configuration. +- `cluster_known_nodes`: The total number of known nodes in the cluster, + including nodes in `HANDSHAKE` state that may not currently be proper members + of the cluster. +- `cluster_size`: The number of master nodes serving at least one hash slot in + the cluster. +- `cluster_current_epoch`: The local `Current Epoch` variable. This is used in + order to create unique increasing version numbers during fail overs. +- `cluster_my_epoch`: The `Config Epoch` of the node we are talking with. This + is the current configuration version assigned to this node. +- `cluster_stats_messages_sent`: Number of messages sent via the cluster + node-to-node binary bus. +- `cluster_stats_messages_received`: Number of messages received via the cluster + node-to-node binary bus. + +More information about the Current Epoch and Config Epoch variables are +available in the Redis Cluster specification document. + +@return + +@bulk-string-reply: A map between named fields and values in the form of +`<field>:<value>` lines separated by newlines composed by the two bytes `CRLF`. diff --git a/iredis/data/commands/cluster-keyslot.md b/iredis/data/commands/cluster-keyslot.md new file mode 100644 index 0000000..5f08e79 --- /dev/null +++ b/iredis/data/commands/cluster-keyslot.md @@ -0,0 +1,32 @@ +Returns an integer identifying the hash slot the specified key hashes to. This +command is mainly useful for debugging and testing, since it exposes via an API +the underlying Redis implementation of the hashing algorithm. Example use cases +for this command: + +1. Client libraries may use Redis in order to test their own hashing algorithm, + generating random keys and hashing them with both their local implementation + and using Redis `CLUSTER KEYSLOT` command, then checking if the result is the + same. +2. Humans may use this command in order to check what is the hash slot, and then + the associated Redis Cluster node, responsible for a given key. + +## Example + +``` +> CLUSTER KEYSLOT somekey +11058 +> CLUSTER KEYSLOT foo{hash_tag} +(integer) 2515 +> CLUSTER KEYSLOT bar{hash_tag} +(integer) 2515 +``` + +Note that the command implements the full hashing algorithm, including support +for **hash tags**, that is the special property of Redis Cluster key hashing +algorithm, of hashing just what is between `{` and `}` if such a pattern is +found inside the key name, in order to force multiple keys to be handled by the +same node. + +@return + +@integer-reply: The hash slot number. diff --git a/iredis/data/commands/cluster-meet.md b/iredis/data/commands/cluster-meet.md new file mode 100644 index 0000000..3402faa --- /dev/null +++ b/iredis/data/commands/cluster-meet.md @@ -0,0 +1,55 @@ +`CLUSTER MEET` is used in order to connect different Redis nodes with cluster +support enabled, into a working cluster. + +The basic idea is that nodes by default don't trust each other, and are +considered unknown, so that it is unlikely that different cluster nodes will mix +into a single one because of system administration errors or network addresses +modifications. + +So in order for a given node to accept another one into the list of nodes +composing a Redis Cluster, there are only two ways: + +1. The system administrator sends a `CLUSTER MEET` command to force a node to + meet another one. +2. An already known node sends a list of nodes in the gossip section that we are + not aware of. If the receiving node trusts the sending node as a known node, + it will process the gossip section and send an handshake to the nodes that + are still not known. + +Note that Redis Cluster needs to form a full mesh (each node is connected with +each other node), but in order to create a cluster, there is no need to send all +the `CLUSTER MEET` commands needed to form the full mesh. What matter is to send +enough `CLUSTER MEET` messages so that each node can reach each other node +through a _chain of known nodes_. Thanks to the exchange of gossip information +in heartbeat packets, the missing links will be created. + +So, if we link node A with node B via `CLUSTER MEET`, and B with C, A and C will +find their ways to handshake and create a link. + +Another example: if we imagine a cluster formed of the following four nodes +called A, B, C and D, we may send just the following set of commands to A: + +1. `CLUSTER MEET B-ip B-port` +2. `CLUSTER MEET C-ip C-port` +3. `CLUSTER MEET D-ip D-port` + +As a side effect of `A` knowing and being known by all the other nodes, it will +send gossip sections in the heartbeat packets that will allow each other node to +create a link with each other one, forming a full mesh in a matter of seconds, +even if the cluster is large. + +Moreover `CLUSTER MEET` does not need to be reciprocal. If I send the command to +A in order to join B, I don't need to also send it to B in order to join A. + +## Implementation details: MEET and PING packets + +When a given node receives a `CLUSTER MEET` message, the node specified in the +command still does not know the node we sent the command to. So in order for the +node to force the receiver to accept it as a trusted node, it sends a `MEET` +packet instead of a `PING` packet. The two packets have exactly the same format, +but the former forces the receiver to acknowledge the node as trusted. + +@return + +@simple-string-reply: `OK` if the command was successful. If the address or port +specified are invalid an error is returned. diff --git a/iredis/data/commands/cluster-myid.md b/iredis/data/commands/cluster-myid.md new file mode 100644 index 0000000..1ff5c0f --- /dev/null +++ b/iredis/data/commands/cluster-myid.md @@ -0,0 +1,8 @@ +Returns the node's id. + +The `CLUSTER MYID` command returns the unique, auto-generated identifier that is +associated with the connected cluster node. + +@return + +@bulk-string-reply: The node id. diff --git a/iredis/data/commands/cluster-nodes.md b/iredis/data/commands/cluster-nodes.md new file mode 100644 index 0000000..435ce2b --- /dev/null +++ b/iredis/data/commands/cluster-nodes.md @@ -0,0 +1,147 @@ +Each node in a Redis Cluster has its view of the current cluster configuration, +given by the set of known nodes, the state of the connection we have with such +nodes, their flags, properties and assigned slots, and so forth. + +`CLUSTER NODES` provides all this information, that is, the current cluster +configuration of the node we are contacting, in a serialization format which +happens to be exactly the same as the one used by Redis Cluster itself in order +to store on disk the cluster state (however the on disk cluster state has a few +additional info appended at the end). + +Note that normally clients willing to fetch the map between Cluster hash slots +and node addresses should use `CLUSTER SLOTS` instead. `CLUSTER NODES`, that +provides more information, should be used for administrative tasks, debugging, +and configuration inspections. It is also used by `redis-trib` in order to +manage a cluster. + +## Serialization format + +The output of the command is just a space-separated CSV string, where each line +represents a node in the cluster. The following is an example of output: + +``` +07c37dfeb235213a872192d90877d0cd55635b91 127.0.0.1:30004@31004 slave e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca 0 1426238317239 4 connected +67ed2db8d677e59ec4a4cefb06858cf2a1a89fa1 127.0.0.1:30002@31002 master - 0 1426238316232 2 connected 5461-10922 +292f8b365bb7edb5e285caf0b7e6ddc7265d2f4f 127.0.0.1:30003@31003 master - 0 1426238318243 3 connected 10923-16383 +6ec23923021cf3ffec47632106199cb7f496ce01 127.0.0.1:30005@31005 slave 67ed2db8d677e59ec4a4cefb06858cf2a1a89fa1 0 1426238316232 5 connected +824fe116063bc5fcf9f4ffd895bc17aee7731ac3 127.0.0.1:30006@31006 slave 292f8b365bb7edb5e285caf0b7e6ddc7265d2f4f 0 1426238317741 6 connected +e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca 127.0.0.1:30001@31001 myself,master - 0 0 1 connected 0-5460 +``` + +Each line is composed of the following fields: + +``` +<id> <ip:port@cport> <flags> <master> <ping-sent> <pong-recv> <config-epoch> <link-state> <slot> <slot> ... <slot> +``` + +The meaning of each filed is the following: + +1. `id`: The node ID, a 40 characters random string generated when a node is + created and never changed again (unless `CLUSTER RESET HARD` is used). +2. `ip:port@cport`: The node address where clients should contact the node to + run queries. +3. `flags`: A list of comma separated flags: `myself`, `master`, `slave`, + `fail?`, `fail`, `handshake`, `noaddr`, `noflags`. Flags are explained in + detail in the next section. +4. `master`: If the node is a replica, and the master is known, the master node + ID, otherwise the "-" character. +5. `ping-sent`: Milliseconds unix time at which the currently active ping was + sent, or zero if there are no pending pings. +6. `pong-recv`: Milliseconds unix time the last pong was received. +7. `config-epoch`: The configuration epoch (or version) of the current node (or + of the current master if the node is a replica). Each time there is a + failover, a new, unique, monotonically increasing configuration epoch is + created. If multiple nodes claim to serve the same hash slots, the one with + higher configuration epoch wins. +8. `link-state`: The state of the link used for the node-to-node cluster bus. We + use this link to communicate with the node. Can be `connected` or + `disconnected`. +9. `slot`: A hash slot number or range. Starting from argument number 9, but + there may be up to 16384 entries in total (limit never reached). This is the + list of hash slots served by this node. If the entry is just a number, is + parsed as such. If it is a range, it is in the form `start-end`, and means + that the node is responsible for all the hash slots from `start` to `end` + including the start and end values. + +Meaning of the flags (field number 3): + +- `myself`: The node you are contacting. +- `master`: Node is a master. +- `slave`: Node is a replica. +- `fail?`: Node is in `PFAIL` state. Not reachable for the node you are + contacting, but still logically reachable (not in `FAIL` state). +- `fail`: Node is in `FAIL` state. It was not reachable for multiple nodes that + promoted the `PFAIL` state to `FAIL`. +- `handshake`: Untrusted node, we are handshaking. +- `noaddr`: No address known for this node. +- `noflags`: No flags at all. + +## Notes on published config epochs + +Replicas broadcast their master's config epochs (in order to get an `UPDATE` +message if they are found to be stale), so the real config epoch of the replica +(which is meaningless more or less, since they don't serve hash slots) can be +only obtained checking the node flagged as `myself`, which is the entry of the +node we are asking to generate `CLUSTER NODES` output. The other replicas epochs +reflect what they publish in heartbeat packets, which is, the configuration +epoch of the masters they are currently replicating. + +## Special slot entries + +Normally hash slots associated to a given node are in one of the following +formats, as already explained above: + +1. Single number: 3894 +2. Range: 3900-4000 + +However node hash slots can be in a special state, used in order to communicate +errors after a node restart (mismatch between the keys in the AOF/RDB file, and +the node hash slots configuration), or when there is a resharding operation in +progress. This two states are **importing** and **migrating**. + +The meaning of the two states is explained in the Redis Specification, however +the gist of the two states is the following: + +- **Importing** slots are yet not part of the nodes hash slot, there is a + migration in progress. The node will accept queries about these slots only if + the `ASK` command is used. +- **Migrating** slots are assigned to the node, but are being migrated to some + other node. The node will accept queries if all the keys in the command exist + already, otherwise it will emit what is called an **ASK redirection**, to + force new keys creation directly in the importing node. + +Importing and migrating slots are emitted in the `CLUSTER NODES` output as +follows: + +- **Importing slot:** `[slot_number-<-importing_from_node_id]` +- **Migrating slot:** `[slot_number->-migrating_to_node_id]` + +The following are a few examples of importing and migrating slots: + +- `[93-<-292f8b365bb7edb5e285caf0b7e6ddc7265d2f4f]` +- `[1002-<-67ed2db8d677e59ec4a4cefb06858cf2a1a89fa1]` +- `[77->-e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca]` +- `[16311->-292f8b365bb7edb5e285caf0b7e6ddc7265d2f4f]` + +Note that the format does not have any space, so `CLUSTER NODES` output format +is plain CSV with space as separator even when this special slots are emitted. +However a complete parser for the format should be able to handle them. + +Note that: + +1. Migration and importing slots are only added to the node flagged as `myself`. + This information is local to a node, for its own slots. +2. Importing and migrating slots are provided as **additional info**. If the + node has a given hash slot assigned, it will be also a plain number in the + list of hash slots, so clients that don't have a clue about hash slots + migrations can just skip this special fields. + +@return + +@bulk-string-reply: The serialized cluster configuration. + +**A note about the word slave used in this man page and command name**: Starting +with Redis 5, if not for backward compatibility, the Redis project no longer +uses the word slave. Unfortunately in this command the word slave is part of the +protocol, so we'll be able to remove such occurrences only when this API will be +naturally deprecated. diff --git a/iredis/data/commands/cluster-replicas.md b/iredis/data/commands/cluster-replicas.md new file mode 100644 index 0000000..678638a --- /dev/null +++ b/iredis/data/commands/cluster-replicas.md @@ -0,0 +1,16 @@ +The command provides a list of replica nodes replicating from the specified +master node. The list is provided in the same format used by `CLUSTER NODES` +(please refer to its documentation for the specification of the format). + +The command will fail if the specified node is not known or if it is not a +master according to the node table of the node receiving the command. + +Note that if a replica is added, moved, or removed from a given master node, and +we ask `CLUSTER REPLICAS` to a node that has not yet received the configuration +update, it may show stale information. However eventually (in a matter of +seconds if there are no network partitions) all the nodes will agree about the +set of nodes associated with a given master. + +@return + +The command returns data in the same format as `CLUSTER NODES`. diff --git a/iredis/data/commands/cluster-replicate.md b/iredis/data/commands/cluster-replicate.md new file mode 100644 index 0000000..ab5cf5c --- /dev/null +++ b/iredis/data/commands/cluster-replicate.md @@ -0,0 +1,29 @@ +The command reconfigures a node as a replica of the specified master. If the +node receiving the command is an _empty master_, as a side effect of the +command, the node role is changed from master to replica. + +Once a node is turned into the replica of another master node, there is no need +to inform the other cluster nodes about the change: heartbeat packets exchanged +between nodes will propagate the new configuration automatically. + +A replica will always accept the command, assuming that: + +1. The specified node ID exists in its nodes table. +2. The specified node ID does not identify the instance we are sending the + command to. +3. The specified node ID is a master. + +If the node receiving the command is not already a replica, but is a master, the +command will only succeed, and the node will be converted into a replica, only +if the following additional conditions are met: + +1. The node is not serving any hash slots. +2. The node is empty, no keys are stored at all in the key space. + +If the command succeeds the new replica will immediately try to contact its +master in order to replicate from it. + +@return + +@simple-string-reply: `OK` if the command was executed successfully, otherwise +an error is returned. diff --git a/iredis/data/commands/cluster-reset.md b/iredis/data/commands/cluster-reset.md new file mode 100644 index 0000000..4186725 --- /dev/null +++ b/iredis/data/commands/cluster-reset.md @@ -0,0 +1,29 @@ +Reset a Redis Cluster node, in a more or less drastic way depending on the reset +type, that can be **hard** or **soft**. Note that this command **does not work +for masters if they hold one or more keys**, in that case to completely reset a +master node keys must be removed first, e.g. by using `FLUSHALL` first, and then +`CLUSTER RESET`. + +Effects on the node: + +1. All the other nodes in the cluster are forgotten. +2. All the assigned / open slots are reset, so the slots-to-nodes mapping is + totally cleared. +3. If the node is a replica it is turned into an (empty) master. Its dataset is + flushed, so at the end the node will be an empty master. +4. **Hard reset only**: a new Node ID is generated. +5. **Hard reset only**: `currentEpoch` and `configEpoch` vars are set to 0. +6. The new configuration is persisted on disk in the node cluster configuration + file. + +This command is mainly useful to re-provision a Redis Cluster node in order to +be used in the context of a new, different cluster. The command is also +extensively used by the Redis Cluster testing framework in order to reset the +state of the cluster every time a new test unit is executed. + +If no reset type is specified, the default is **soft**. + +@return + +@simple-string-reply: `OK` if the command was successful. Otherwise an error is +returned. diff --git a/iredis/data/commands/cluster-saveconfig.md b/iredis/data/commands/cluster-saveconfig.md new file mode 100644 index 0000000..115d88a --- /dev/null +++ b/iredis/data/commands/cluster-saveconfig.md @@ -0,0 +1,15 @@ +Forces a node to save the `nodes.conf` configuration on disk. Before to return +the command calls `fsync(2)` in order to make sure the configuration is flushed +on the computer disk. + +This command is mainly used in the event a `nodes.conf` node state file gets +lost / deleted for some reason, and we want to generate it again from scratch. +It can also be useful in case of mundane alterations of a node cluster +configuration via the `CLUSTER` command in order to ensure the new configuration +is persisted on disk, however all the commands should normally be able to auto +schedule to persist the configuration on disk when it is important to do so for +the correctness of the system in the event of a restart. + +@return + +@simple-string-reply: `OK` or an error if the operation fails. diff --git a/iredis/data/commands/cluster-set-config-epoch.md b/iredis/data/commands/cluster-set-config-epoch.md new file mode 100644 index 0000000..b64fffa --- /dev/null +++ b/iredis/data/commands/cluster-set-config-epoch.md @@ -0,0 +1,25 @@ +This command sets a specific _config epoch_ in a fresh node. It only works when: + +1. The nodes table of the node is empty. +2. The node current _config epoch_ is zero. + +These prerequisites are needed since usually, manually altering the +configuration epoch of a node is unsafe, we want to be sure that the node with +the higher configuration epoch value (that is the last that failed over) wins +over other nodes in claiming the hash slots ownership. + +However there is an exception to this rule, and it is when a new cluster is +created from scratch. Redis Cluster _config epoch collision resolution_ +algorithm can deal with new nodes all configured with the same configuration at +startup, but this process is slow and should be the exception, only to make sure +that whatever happens, two more nodes eventually always move away from the state +of having the same configuration epoch. + +So, using `CONFIG SET-CONFIG-EPOCH`, when a new cluster is created, we can +assign a different progressive configuration epoch to each node before joining +the cluster together. + +@return + +@simple-string-reply: `OK` if the command was executed successfully, otherwise +an error is returned. diff --git a/iredis/data/commands/cluster-setslot.md b/iredis/data/commands/cluster-setslot.md new file mode 100644 index 0000000..6e5ecf3 --- /dev/null +++ b/iredis/data/commands/cluster-setslot.md @@ -0,0 +1,132 @@ +`CLUSTER SETSLOT` is responsible of changing the state of a hash slot in the +receiving node in different ways. It can, depending on the subcommand used: + +1. `MIGRATING` subcommand: Set a hash slot in _migrating_ state. +2. `IMPORTING` subcommand: Set a hash slot in _importing_ state. +3. `STABLE` subcommand: Clear any importing / migrating state from hash slot. +4. `NODE` subcommand: Bind the hash slot to a different node. + +The command with its set of subcommands is useful in order to start and end +cluster live resharding operations, which are accomplished by setting a hash +slot in migrating state in the source node, and importing state in the +destination node. + +Each subcommand is documented below. At the end you'll find a description of how +live resharding is performed using this command and other related commands. + +## CLUSTER SETSLOT `<slot>` MIGRATING `<destination-node-id>` + +This subcommand sets a slot to _migrating_ state. In order to set a slot in this +state, the node receiving the command must be the hash slot owner, otherwise an +error is returned. + +When a slot is set in migrating state, the node changes behavior in the +following way: + +1. If a command is received about an existing key, the command is processed as + usually. +2. If a command is received about a key that does not exists, an `ASK` + redirection is emitted by the node, asking the client to retry only that + specific query into `destination-node`. In this case the client should not + update its hash slot to node mapping. +3. If the command contains multiple keys, in case none exist, the behavior is + the same as point 2, if all exist, it is the same as point 1, however if only + a partial number of keys exist, the command emits a `TRYAGAIN` error in order + for the keys interested to finish being migrated to the target node, so that + the multi keys command can be executed. + +## CLUSTER SETSLOT `<slot>` IMPORTING `<source-node-id>` + +This subcommand is the reverse of `MIGRATING`, and prepares the destination node +to import keys from the specified source node. The command only works if the +node is not already owner of the specified hash slot. + +When a slot is set in importing state, the node changes behavior in the +following way: + +1. Commands about this hash slot are refused and a `MOVED` redirection is + generated as usually, but in the case the command follows an `ASKING` + command, in this case the command is executed. + +In this way when a node in migrating state generates an `ASK` redirection, the +client contacts the target node, sends `ASKING`, and immediately after sends the +command. This way commands about non-existing keys in the old node or keys +already migrated to the target node are executed in the target node, so that: + +1. New keys are always created in the target node. During a hash slot migration + we'll have to move only old keys, not new ones. +2. Commands about keys already migrated are correctly processed in the context + of the node which is the target of the migration, the new hash slot owner, in + order to guarantee consistency. +3. Without `ASKING` the behavior is the same as usually. This guarantees that + clients with a broken hash slots mapping will not write for error in the + target node, creating a new version of a key that has yet to be migrated. + +## CLUSTER SETSLOT `<slot>` STABLE + +This subcommand just clears migrating / importing state from the slot. It is +mainly used to fix a cluster stuck in a wrong state by `redis-trib fix`. +Normally the two states are cleared automatically at the end of the migration +using the `SETSLOT ... NODE ...` subcommand as explained in the next section. + +## CLUSTER SETSLOT `<slot>` NODE `<node-id>` + +The `NODE` subcommand is the one with the most complex semantics. It associates +the hash slot with the specified node, however the command works only in +specific situations and has different side effects depending on the slot state. +The following is the set of pre-conditions and side effects of the command: + +1. If the current hash slot owner is the node receiving the command, but for + effect of the command the slot would be assigned to a different node, the + command will return an error if there are still keys for that hash slot in + the node receiving the command. +2. If the slot is in _migrating_ state, the state gets cleared when the slot is + assigned to another node. +3. If the slot was in _importing_ state in the node receiving the command, and + the command assigns the slot to this node (which happens in the target node + at the end of the resharding of a hash slot from one node to another), the + command has the following side effects: A) the _importing_ state is cleared. + B) If the node config epoch is not already the greatest of the cluster, it + generates a new one and assigns the new config epoch to itself. This way its + new hash slot ownership will win over any past configuration created by + previous failovers or slot migrations. + +It is important to note that step 3 is the only time when a Redis Cluster node +will create a new config epoch without agreement from other nodes. This only +happens when a manual configuration is operated. However it is impossible that +this creates a non-transient setup where two nodes have the same config epoch, +since Redis Cluster uses a config epoch collision resolution algorithm. + +@return + +@simple-string-reply: All the subcommands return `OK` if the command was +successful. Otherwise an error is returned. + +## Redis Cluster live resharding explained + +The `CLUSTER SETSLOT` command is an important piece used by Redis Cluster in +order to migrate all the keys contained in one hash slot from one node to +another. This is how the migration is orchestrated, with the help of other +commands as well. We'll call the node that has the current ownership of the hash +slot the `source` node, and the node where we want to migrate the `destination` +node. + +1. Set the destination node slot to _importing_ state using + `CLUSTER SETSLOT <slot> IMPORTING <source-node-id>`. +2. Set the source node slot to _migrating_ state using + `CLUSTER SETSLOT <slot> MIGRATING <destination-node-id>`. +3. Get keys from the source node with `CLUSTER GETKEYSINSLOT` command and move + them into the destination node using the `MIGRATE` command. +4. Use `CLUSTER SETSLOT <slot> NODE <destination-node-id>` in the source or + destination. + +Notes: + +- The order of step 1 and 2 is important. We want the destination node to be + ready to accept `ASK` redirections when the source node is configured to + redirect. +- Step 4 does not technically need to use `SETSLOT` in the nodes not involved in + the resharding, since the configuration will eventually propagate itself, + however it is a good idea to do so in order to stop nodes from pointing to the + wrong node for the hash slot moved as soon as possible, resulting in less + redirections to find the right node. diff --git a/iredis/data/commands/cluster-slaves.md b/iredis/data/commands/cluster-slaves.md new file mode 100644 index 0000000..0b2aa49 --- /dev/null +++ b/iredis/data/commands/cluster-slaves.md @@ -0,0 +1,22 @@ +**A note about the word slave used in this man page and command name**: Starting +with Redis 5 this command: starting with Redis version 5, if not for backward +compatibility, the Redis project no longer uses the word slave. Please use the +new command `CLUSTER REPLICAS`. The command `SLAVEOF` will continue to work for +backward compatibility. + +The command provides a list of replica nodes replicating from the specified +master node. The list is provided in the same format used by `CLUSTER NODES` +(please refer to its documentation for the specification of the format). + +The command will fail if the specified node is not known or if it is not a +master according to the node table of the node receiving the command. + +Note that if a replica is added, moved, or removed from a given master node, and +we ask `CLUSTER SLAVES` to a node that has not yet received the configuration +update, it may show stale information. However eventually (in a matter of +seconds if there are no network partitions) all the nodes will agree about the +set of nodes associated with a given master. + +@return + +The command returns data in the same format as `CLUSTER NODES`. diff --git a/iredis/data/commands/cluster-slots.md b/iredis/data/commands/cluster-slots.md new file mode 100644 index 0000000..693e6b3 --- /dev/null +++ b/iredis/data/commands/cluster-slots.md @@ -0,0 +1,102 @@ +`CLUSTER SLOTS` returns details about which cluster slots map to which Redis +instances. The command is suitable to be used by Redis Cluster client libraries +implementations in order to retrieve (or update when a redirection is received) +the map associating cluster _hash slots_ with actual nodes network coordinates +(composed of an IP address and a TCP port), so that when a command is received, +it can be sent to what is likely the right instance for the keys specified in +the command. + +## Nested Result Array + +Each nested result is: + +- Start slot range +- End slot range +- Master for slot range represented as nested IP/Port array +- First replica of master for slot range +- Second replica +- ...continues until all replicas for this master are returned. + +Each result includes all active replicas of the master instance for the listed +slot range. Failed replicas are not returned. + +The third nested reply is guaranteed to be the IP/Port pair of the master +instance for the slot range. All IP/Port pairs after the third nested reply are +replicas of the master. + +If a cluster instance has non-contiguous slots (e.g. 1-400,900,1800-6000) then +master and replica IP/Port results will be duplicated for each top-level slot +range reply. + +**Warning:** Newer versions of Redis Cluster will output, for each Redis +instance, not just the IP and port, but also the node ID as third element of the +array. In future versions there could be more elements describing the node +better. In general a client implementation should just rely on the fact that +certain parameters are at fixed positions as specified, but more parameters may +follow and should be ignored. Similarly a client library should try if possible +to cope with the fact that older versions may just have the IP and port +parameter. + +@return + +@array-reply: nested list of slot ranges with IP/Port mappings. + +### Sample Output (old version) + +``` +127.0.0.1:7001> cluster slots +1) 1) (integer) 0 + 2) (integer) 4095 + 3) 1) "127.0.0.1" + 2) (integer) 7000 + 4) 1) "127.0.0.1" + 2) (integer) 7004 +2) 1) (integer) 12288 + 2) (integer) 16383 + 3) 1) "127.0.0.1" + 2) (integer) 7003 + 4) 1) "127.0.0.1" + 2) (integer) 7007 +3) 1) (integer) 4096 + 2) (integer) 8191 + 3) 1) "127.0.0.1" + 2) (integer) 7001 + 4) 1) "127.0.0.1" + 2) (integer) 7005 +4) 1) (integer) 8192 + 2) (integer) 12287 + 3) 1) "127.0.0.1" + 2) (integer) 7002 + 4) 1) "127.0.0.1" + 2) (integer) 7006 +``` + +### Sample Output (new version, includes IDs) + +``` +127.0.0.1:30001> cluster slots +1) 1) (integer) 0 + 2) (integer) 5460 + 3) 1) "127.0.0.1" + 2) (integer) 30001 + 3) "09dbe9720cda62f7865eabc5fd8857c5d2678366" + 4) 1) "127.0.0.1" + 2) (integer) 30004 + 3) "821d8ca00d7ccf931ed3ffc7e3db0599d2271abf" +2) 1) (integer) 5461 + 2) (integer) 10922 + 3) 1) "127.0.0.1" + 2) (integer) 30002 + 3) "c9d93d9f2c0c524ff34cc11838c2003d8c29e013" + 4) 1) "127.0.0.1" + 2) (integer) 30005 + 3) "faadb3eb99009de4ab72ad6b6ed87634c7ee410f" +3) 1) (integer) 10923 + 2) (integer) 16383 + 3) 1) "127.0.0.1" + 2) (integer) 30003 + 3) "044ec91f325b7595e76dbcb18cc688b6a5b434a1" + 4) 1) "127.0.0.1" + 2) (integer) 30006 + 3) "58e6e48d41228013e5d9c1c37c5060693925e97e" +``` diff --git a/iredis/data/commands/command-count.md b/iredis/data/commands/command-count.md new file mode 100644 index 0000000..a198dd3 --- /dev/null +++ b/iredis/data/commands/command-count.md @@ -0,0 +1,11 @@ +Returns @integer-reply of number of total commands in this Redis server. + +@return + +@integer-reply: number of commands returned by `COMMAND` + +@examples + +```cli +COMMAND COUNT +``` diff --git a/iredis/data/commands/command-getkeys.md b/iredis/data/commands/command-getkeys.md new file mode 100644 index 0000000..1c591f1 --- /dev/null +++ b/iredis/data/commands/command-getkeys.md @@ -0,0 +1,21 @@ +Returns @array-reply of keys from a full Redis command. + +`COMMAND GETKEYS` is a helper command to let you find the keys from a full Redis +command. + +`COMMAND` shows some commands as having movablekeys meaning the entire command +must be parsed to discover storage or retrieval keys. You can use +`COMMAND GETKEYS` to discover key positions directly from how Redis parses the +commands. + +@return + +@array-reply: list of keys from your command. + +@examples + +```cli +COMMAND GETKEYS MSET a b c d e f +COMMAND GETKEYS EVAL "not consulted" 3 key1 key2 key3 arg1 arg2 arg3 argN +COMMAND GETKEYS SORT mylist ALPHA STORE outlist +``` diff --git a/iredis/data/commands/command-info.md b/iredis/data/commands/command-info.md new file mode 100644 index 0000000..92836e4 --- /dev/null +++ b/iredis/data/commands/command-info.md @@ -0,0 +1,18 @@ +Returns @array-reply of details about multiple Redis commands. + +Same result format as `COMMAND` except you can specify which commands get +returned. + +If you request details about non-existing commands, their return position will +be nil. + +@return + +@array-reply: nested list of command details. + +@examples + +```cli +COMMAND INFO get set eval +COMMAND INFO foo evalsha config bar +``` diff --git a/iredis/data/commands/command.md b/iredis/data/commands/command.md new file mode 100644 index 0000000..5028e70 --- /dev/null +++ b/iredis/data/commands/command.md @@ -0,0 +1,179 @@ +Returns @array-reply of details about all Redis commands. + +Cluster clients must be aware of key positions in commands so commands can go to +matching instances, but Redis commands vary between accepting one key, multiple +keys, or even multiple keys separated by other data. + +You can use `COMMAND` to cache a mapping between commands and key positions for +each command to enable exact routing of commands to cluster instances. + +## Nested Result Array + +Each top-level result contains six nested results. Each nested result is: + +- command name +- command arity specification +- nested @array-reply of command flags +- position of first key in argument list +- position of last key in argument list +- step count for locating repeating keys + +### Command Name + +Command name is the command returned as a lowercase string. + +### Command Arity + +<table style="width:50%"> +<tr><td> +<pre> +<code>1) 1) "get" + 2) (integer) 2 + 3) 1) readonly + 4) (integer) 1 + 5) (integer) 1 + 6) (integer) 1 +</code> +</pre> +</td> +<td> +<pre> +<code>1) 1) "mget" + 2) (integer) -2 + 3) 1) readonly + 4) (integer) 1 + 5) (integer) -1 + 6) (integer) 1 +</code> +</pre> +</td></tr> +</table> + +Command arity follows a simple pattern: + +- positive if command has fixed number of required arguments. +- negative if command has minimum number of required arguments, but may have + more. + +Command arity _includes_ counting the command name itself. + +Examples: + +- `GET` arity is 2 since the command only accepts one argument and always has + the format `GET _key_`. +- `MGET` arity is -2 since the command accepts at a minimum one argument, but up + to an unlimited number: `MGET _key1_ [key2] [key3] ...`. + +Also note with `MGET`, the -1 value for "last key position" means the list of +keys may have unlimited length. + +### Flags + +Command flags is @array-reply containing one or more status replies: + +- _write_ - command may result in modifications +- _readonly_ - command will never modify keys +- _denyoom_ - reject command if currently OOM +- _admin_ - server admin command +- _pubsub_ - pubsub-related command +- _noscript_ - deny this command from scripts +- _random_ - command has random results, dangerous for scripts +- _sort_for_script_ - if called from script, sort output +- _loading_ - allow command while database is loading +- _stale_ - allow command while replica has stale data +- _skip_monitor_ - do not show this command in MONITOR +- _asking_ - cluster related - accept even if importing +- _fast_ - command operates in constant or log(N) time. Used for latency + monitoring. +- _movablekeys_ - keys have no pre-determined position. You must discover keys + yourself. + +### Movable Keys + +``` +1) 1) "sort" + 2) (integer) -2 + 3) 1) write + 2) denyoom + 3) movablekeys + 4) (integer) 1 + 5) (integer) 1 + 6) (integer) 1 +``` + +Some Redis commands have no predetermined key locations. For those commands, +flag `movablekeys` is added to the command flags @array-reply. Your Redis +Cluster client needs to parse commands marked `movablekeys` to locate all +relevant key positions. + +Complete list of commands currently requiring key location parsing: + +- `SORT` - optional `STORE` key, optional `BY` weights, optional `GET` keys +- `ZUNIONSTORE` - keys stop when `WEIGHT` or `AGGREGATE` starts +- `ZINTERSTORE` - keys stop when `WEIGHT` or `AGGREGATE` starts +- `EVAL` - keys stop after `numkeys` count arguments +- `EVALSHA` - keys stop after `numkeys` count arguments + +Also see `COMMAND GETKEYS` for getting your Redis server tell you where keys are +in any given command. + +### First Key in Argument List + +For most commands the first key is position 1. Position 0 is always the command +name itself. + +### Last Key in Argument List + +Redis commands usually accept one key, two keys, or an unlimited number of keys. + +If a command accepts one key, the first key and last key positions is 1. + +If a command accepts two keys (e.g. `BRPOPLPUSH`, `SMOVE`, `RENAME`, ...) then +the last key position is the location of the last key in the argument list. + +If a command accepts an unlimited number of keys, the last key position is -1. + +### Step Count + +<table style="width:50%"> +<tr><td> +<pre> +<code>1) 1) "mset" + 2) (integer) -3 + 3) 1) write + 2) denyoom + 4) (integer) 1 + 5) (integer) -1 + 6) (integer) 2 +</code> +</pre> +</td> +<td> +<pre> +<code>1) 1) "mget" + 2) (integer) -2 + 3) 1) readonly + 4) (integer) 1 + 5) (integer) -1 + 6) (integer) 1 +</code> +</pre> +</td></tr> +</table> + +Key step count allows us to find key positions in commands like `MSET` where the +format is `MSET _key1_ _val1_ [key2] [val2] [key3] [val3]...`. + +In the case of `MSET`, keys are every other position so the step value is 2. +Compare with `MGET` above where the step value is just 1. + +@return + +@array-reply: nested list of command details. Commands are returned in random +order. + +@examples + +```cli +COMMAND +``` diff --git a/iredis/data/commands/config-get.md b/iredis/data/commands/config-get.md new file mode 100644 index 0000000..f4a4b34 --- /dev/null +++ b/iredis/data/commands/config-get.md @@ -0,0 +1,52 @@ +The `CONFIG GET` command is used to read the configuration parameters of a +running Redis server. Not all the configuration parameters are supported in +Redis 2.4, while Redis 2.6 can read the whole configuration of a server using +this command. + +The symmetric command used to alter the configuration at run time is +`CONFIG SET`. + +`CONFIG GET` takes a single argument, which is a glob-style pattern. All the +configuration parameters matching this parameter are reported as a list of +key-value pairs. Example: + +``` +redis> config get *max-*-entries* +1) "hash-max-zipmap-entries" +2) "512" +3) "list-max-ziplist-entries" +4) "512" +5) "set-max-intset-entries" +6) "512" +``` + +You can obtain a list of all the supported configuration parameters by typing +`CONFIG GET *` in an open `redis-cli` prompt. + +All the supported parameters have the same meaning of the equivalent +configuration parameter used in the [redis.conf][hgcarr22rc] file, with the +following important differences: + +[hgcarr22rc]: http://github.com/redis/redis/raw/2.8/redis.conf + +- Where bytes or other quantities are specified, it is not possible to use the + `redis.conf` abbreviated form (`10k`, `2gb` ... and so forth), everything + should be specified as a well-formed 64-bit integer, in the base unit of the + configuration directive. +- The save parameter is a single string of space-separated integers. Every pair + of integers represent a seconds/modifications threshold. + +For instance what in `redis.conf` looks like: + +``` +save 900 1 +save 300 10 +``` + +that means, save after 900 seconds if there is at least 1 change to the dataset, +and after 300 seconds if there are at least 10 changes to the dataset, will be +reported by `CONFIG GET` as "900 1 300 10". + +@return + +The return type of the command is a @array-reply. diff --git a/iredis/data/commands/config-resetstat.md b/iredis/data/commands/config-resetstat.md new file mode 100644 index 0000000..d25a25b --- /dev/null +++ b/iredis/data/commands/config-resetstat.md @@ -0,0 +1,16 @@ +Resets the statistics reported by Redis using the `INFO` command. + +These are the counters that are reset: + +- Keyspace hits +- Keyspace misses +- Number of commands processed +- Number of connections received +- Number of expired keys +- Number of rejected connections +- Latest fork(2) time +- The `aof_delayed_fsync` counter + +@return + +@simple-string-reply: always `OK`. diff --git a/iredis/data/commands/config-rewrite.md b/iredis/data/commands/config-rewrite.md new file mode 100644 index 0000000..54509e5 --- /dev/null +++ b/iredis/data/commands/config-rewrite.md @@ -0,0 +1,37 @@ +The `CONFIG REWRITE` command rewrites the `redis.conf` file the server was +started with, applying the minimal changes needed to make it reflect the +configuration currently used by the server, which may be different compared to +the original one because of the use of the `CONFIG SET` command. + +The rewrite is performed in a very conservative way: + +- Comments and the overall structure of the original redis.conf are preserved as + much as possible. +- If an option already exists in the old redis.conf file, it will be rewritten + at the same position (line number). +- If an option was not already present, but it is set to its default value, it + is not added by the rewrite process. +- If an option was not already present, but it is set to a non-default value, it + is appended at the end of the file. +- Non used lines are blanked. For instance if you used to have multiple `save` + directives, but the current configuration has fewer or none as you disabled + RDB persistence, all the lines will be blanked. + +CONFIG REWRITE is also able to rewrite the configuration file from scratch if +the original one no longer exists for some reason. However if the server was +started without a configuration file at all, the CONFIG REWRITE will just return +an error. + +## Atomic rewrite process + +In order to make sure the redis.conf file is always consistent, that is, on +errors or crashes you always end with the old file, or the new one, the rewrite +is performed with a single `write(2)` call that has enough content to be at +least as big as the old file. Sometimes additional padding in the form of +comments is added in order to make sure the resulting file is big enough, and +later the file gets truncated to remove the padding at the end. + +@return + +@simple-string-reply: `OK` when the configuration was rewritten properly. +Otherwise an error is returned. diff --git a/iredis/data/commands/config-set.md b/iredis/data/commands/config-set.md new file mode 100644 index 0000000..24c4194 --- /dev/null +++ b/iredis/data/commands/config-set.md @@ -0,0 +1,56 @@ +The `CONFIG SET` command is used in order to reconfigure the server at run time +without the need to restart Redis. You can change both trivial parameters or +switch from one to another persistence option using this command. + +The list of configuration parameters supported by `CONFIG SET` can be obtained +issuing a `CONFIG GET *` command, that is the symmetrical command used to obtain +information about the configuration of a running Redis instance. + +All the configuration parameters set using `CONFIG SET` are immediately loaded +by Redis and will take effect starting with the next command executed. + +All the supported parameters have the same meaning of the equivalent +configuration parameter used in the [redis.conf][hgcarr22rc] file, with the +following important differences: + +[hgcarr22rc]: http://github.com/redis/redis/raw/2.8/redis.conf + +- In options where bytes or other quantities are specified, it is not possible + to use the `redis.conf` abbreviated form (`10k`, `2gb` ... and so forth), + everything should be specified as a well-formed 64-bit integer, in the base + unit of the configuration directive. However since Redis version 3.0 or + greater, it is possible to use `CONFIG SET` with memory units for `maxmemory`, + client output buffers, and replication backlog size. +- The save parameter is a single string of space-separated integers. Every pair + of integers represent a seconds/modifications threshold. + +For instance what in `redis.conf` looks like: + +``` +save 900 1 +save 300 10 +``` + +that means, save after 900 seconds if there is at least 1 change to the dataset, +and after 300 seconds if there are at least 10 changes to the dataset, should be +set using `CONFIG SET SAVE "900 1 300 10"`. + +It is possible to switch persistence from RDB snapshotting to append-only file +(and the other way around) using the `CONFIG SET` command. For more information +about how to do that please check the [persistence page][tp]. + +[tp]: /topics/persistence + +In general what you should know is that setting the `appendonly` parameter to +`yes` will start a background process to save the initial append-only file +(obtained from the in memory data set), and will append all the subsequent +commands on the append-only file, thus obtaining exactly the same effect of a +Redis server that started with AOF turned on since the start. + +You can have both the AOF enabled with RDB snapshotting if you want, the two +options are not mutually exclusive. + +@return + +@simple-string-reply: `OK` when the configuration was set properly. Otherwise an +error is returned. diff --git a/iredis/data/commands/dbsize.md b/iredis/data/commands/dbsize.md new file mode 100644 index 0000000..fe82aa7 --- /dev/null +++ b/iredis/data/commands/dbsize.md @@ -0,0 +1,5 @@ +Return the number of keys in the currently-selected database. + +@return + +@integer-reply diff --git a/iredis/data/commands/debug-object.md b/iredis/data/commands/debug-object.md new file mode 100644 index 0000000..15a4780 --- /dev/null +++ b/iredis/data/commands/debug-object.md @@ -0,0 +1,6 @@ +`DEBUG OBJECT` is a debugging command that should not be used by clients. Check +the `OBJECT` command instead. + +@return + +@simple-string-reply diff --git a/iredis/data/commands/debug-segfault.md b/iredis/data/commands/debug-segfault.md new file mode 100644 index 0000000..4e21b9c --- /dev/null +++ b/iredis/data/commands/debug-segfault.md @@ -0,0 +1,6 @@ +`DEBUG SEGFAULT` performs an invalid memory access that crashes Redis. It is +used to simulate bugs during the development. + +@return + +@simple-string-reply diff --git a/iredis/data/commands/decr.md b/iredis/data/commands/decr.md new file mode 100644 index 0000000..78d2a2c --- /dev/null +++ b/iredis/data/commands/decr.md @@ -0,0 +1,19 @@ +Decrements the number stored at `key` by one. If the key does not exist, it is +set to `0` before performing the operation. An error is returned if the key +contains a value of the wrong type or contains a string that can not be +represented as integer. This operation is limited to **64 bit signed integers**. + +See `INCR` for extra information on increment/decrement operations. + +@return + +@integer-reply: the value of `key` after the decrement + +@examples + +```cli +SET mykey "10" +DECR mykey +SET mykey "234293482390480948029348230948" +DECR mykey +``` diff --git a/iredis/data/commands/decrby.md b/iredis/data/commands/decrby.md new file mode 100644 index 0000000..3d000a0 --- /dev/null +++ b/iredis/data/commands/decrby.md @@ -0,0 +1,17 @@ +Decrements the number stored at `key` by `decrement`. If the key does not exist, +it is set to `0` before performing the operation. An error is returned if the +key contains a value of the wrong type or contains a string that can not be +represented as integer. This operation is limited to 64 bit signed integers. + +See `INCR` for extra information on increment/decrement operations. + +@return + +@integer-reply: the value of `key` after the decrement + +@examples + +```cli +SET mykey "10" +DECRBY mykey 3 +``` diff --git a/iredis/data/commands/del.md b/iredis/data/commands/del.md new file mode 100644 index 0000000..fbb05ec --- /dev/null +++ b/iredis/data/commands/del.md @@ -0,0 +1,13 @@ +Removes the specified keys. A key is ignored if it does not exist. + +@return + +@integer-reply: The number of keys that were removed. + +@examples + +```cli +SET key1 "Hello" +SET key2 "World" +DEL key1 key2 key3 +``` diff --git a/iredis/data/commands/discard.md b/iredis/data/commands/discard.md new file mode 100644 index 0000000..d84b503 --- /dev/null +++ b/iredis/data/commands/discard.md @@ -0,0 +1,10 @@ +Flushes all previously queued commands in a [transaction][tt] and restores the +connection state to normal. + +[tt]: /topics/transactions + +If `WATCH` was used, `DISCARD` unwatches all keys watched by the connection. + +@return + +@simple-string-reply: always `OK`. diff --git a/iredis/data/commands/dump.md b/iredis/data/commands/dump.md new file mode 100644 index 0000000..b81d7af --- /dev/null +++ b/iredis/data/commands/dump.md @@ -0,0 +1,30 @@ +Serialize the value stored at key in a Redis-specific format and return it to +the user. The returned value can be synthesized back into a Redis key using the +`RESTORE` command. + +The serialization format is opaque and non-standard, however it has a few +semantic characteristics: + +- It contains a 64-bit checksum that is used to make sure errors will be + detected. The `RESTORE` command makes sure to check the checksum before + synthesizing a key using the serialized value. +- Values are encoded in the same format used by RDB. +- An RDB version is encoded inside the serialized value, so that different Redis + versions with incompatible RDB formats will refuse to process the serialized + value. + +The serialized value does NOT contain expire information. In order to capture +the time to live of the current value the `PTTL` command should be used. + +If `key` does not exist a nil bulk reply is returned. + +@return + +@bulk-string-reply: the serialized value. + +@examples + +```cli +SET mykey 10 +DUMP mykey +``` diff --git a/iredis/data/commands/echo.md b/iredis/data/commands/echo.md new file mode 100644 index 0000000..642d0f3 --- /dev/null +++ b/iredis/data/commands/echo.md @@ -0,0 +1,11 @@ +Returns `message`. + +@return + +@bulk-string-reply + +@examples + +```cli +ECHO "Hello World!" +``` diff --git a/iredis/data/commands/eval.md b/iredis/data/commands/eval.md new file mode 100644 index 0000000..d1d0346 --- /dev/null +++ b/iredis/data/commands/eval.md @@ -0,0 +1,892 @@ +## Introduction to EVAL + +`EVAL` and `EVALSHA` are used to evaluate scripts using the Lua interpreter +built into Redis starting from version 2.6.0. + +The first argument of `EVAL` is a Lua 5.1 script. The script does not need to +define a Lua function (and should not). It is just a Lua program that will run +in the context of the Redis server. + +The second argument of `EVAL` is the number of arguments that follows the script +(starting from the third argument) that represent Redis key names. The arguments +can be accessed by Lua using the `!KEYS` global variable in the form of a +one-based array (so `KEYS[1]`, `KEYS[2]`, ...). + +All the additional arguments should not represent key names and can be accessed +by Lua using the `ARGV` global variable, very similarly to what happens with +keys (so `ARGV[1]`, `ARGV[2]`, ...). + +The following example should clarify what stated above: + +``` +> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second +1) "key1" +2) "key2" +3) "first" +4) "second" +``` + +Note: as you can see Lua arrays are returned as Redis multi bulk replies, that +is a Redis return type that your client library will likely convert into an +Array type in your programming language. + +It is possible to call Redis commands from a Lua script using two different Lua +functions: + +- `redis.call()` +- `redis.pcall()` + +`redis.call()` is similar to `redis.pcall()`, the only difference is that if a +Redis command call will result in an error, `redis.call()` will raise a Lua +error that in turn will force `EVAL` to return an error to the command caller, +while `redis.pcall` will trap the error and return a Lua table representing the +error. + +The arguments of the `redis.call()` and `redis.pcall()` functions are all the +arguments of a well formed Redis command: + +``` +> eval "return redis.call('set','foo','bar')" 0 +OK +``` + +The above script sets the key `foo` to the string `bar`. However it violates the +`EVAL` command semantics as all the keys that the script uses should be passed +using the `!KEYS` array: + +``` +> eval "return redis.call('set',KEYS[1],'bar')" 1 foo +OK +``` + +All Redis commands must be analyzed before execution to determine which keys the +command will operate on. In order for this to be true for `EVAL`, keys must be +passed explicitly. This is useful in many ways, but especially to make sure +Redis Cluster can forward your request to the appropriate cluster node. + +Note this rule is not enforced in order to provide the user with opportunities +to abuse the Redis single instance configuration, at the cost of writing scripts +not compatible with Redis Cluster. + +Lua scripts can return a value that is converted from the Lua type to the Redis +protocol using a set of conversion rules. + +## Conversion between Lua and Redis data types + +Redis return values are converted into Lua data types when Lua calls a Redis +command using `call()` or `pcall()`. Similarly, Lua data types are converted +into the Redis protocol when calling a Redis command and when a Lua script +returns a value, so that scripts can control what `EVAL` will return to the +client. + +This conversion between data types is designed in a way that if a Redis type is +converted into a Lua type, and then the result is converted back into a Redis +type, the result is the same as the initial value. + +In other words there is a one-to-one conversion between Lua and Redis types. The +following table shows you all the conversions rules: + +**Redis to Lua** conversion table. + +- Redis integer reply -> Lua number +- Redis bulk reply -> Lua string +- Redis multi bulk reply -> Lua table (may have other Redis data types nested) +- Redis status reply -> Lua table with a single `ok` field containing the status +- Redis error reply -> Lua table with a single `err` field containing the error +- Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean type + +**Lua to Redis** conversion table. + +- Lua number -> Redis integer reply (the number is converted into an integer) +- Lua string -> Redis bulk reply +- Lua table (array) -> Redis multi bulk reply (truncated to the first nil inside + the Lua array if any) +- Lua table with a single `ok` field -> Redis status reply +- Lua table with a single `err` field -> Redis error reply +- Lua boolean false -> Redis Nil bulk reply. + +There is an additional Lua-to-Redis conversion rule that has no corresponding +Redis to Lua conversion rule: + +- Lua boolean true -> Redis integer reply with value of 1. + +Lastly, there are three important rules to note: + +- Lua has a single numerical type, Lua numbers. There is no distinction between + integers and floats. So we always convert Lua numbers into integer replies, + removing the decimal part of the number if any. **If you want to return a + float from Lua you should return it as a string**, exactly like Redis itself + does (see for instance the `ZSCORE` command). +- There is + [no simple way to have nils inside Lua arrays](http://www.lua.org/pil/19.1.html), + this is a result of Lua table semantics, so when Redis converts a Lua array + into Redis protocol the conversion is stopped if a nil is encountered. +- When a Lua table contains keys (and their values), the converted Redis reply + will **not** include them. + +**RESP3 mode conversion rules**: note that the Lua engine can work in RESP3 mode +using the new Redis 6 protocol. In this case there are additional conversion +rules, and certain conversions are also modified compared to the RESP2 mode. +Please refer to the RESP3 section of this document for more information. + +Here are a few conversion examples: + +``` +> eval "return 10" 0 +(integer) 10 + +> eval "return {1,2,{3,'Hello World!'}}" 0 +1) (integer) 1 +2) (integer) 2 +3) 1) (integer) 3 + 2) "Hello World!" + +> eval "return redis.call('get','foo')" 0 +"bar" +``` + +The last example shows how it is possible to receive the exact return value of +`redis.call()` or `redis.pcall()` from Lua that would be returned if the command +was called directly. + +In the following example we can see how floats and arrays containing nils and +keys are handled: + +``` +> eval "return {1,2,3.3333,somekey='somevalue','foo',nil,'bar'}" 0 +1) (integer) 1 +2) (integer) 2 +3) (integer) 3 +4) "foo" +``` + +As you can see 3.333 is converted into 3, _somekey_ is excluded, and the _bar_ +string is never returned as there is a nil before. + +## Helper functions to return Redis types + +There are two helper functions to return Redis types from Lua. + +- `redis.error_reply(error_string)` returns an error reply. This function simply + returns a single field table with the `err` field set to the specified string + for you. +- `redis.status_reply(status_string)` returns a status reply. This function + simply returns a single field table with the `ok` field set to the specified + string for you. + +There is no difference between using the helper functions or directly returning +the table with the specified format, so the following two forms are equivalent: + + return {err="My Error"} + return redis.error_reply("My Error") + +## Atomicity of scripts + +Redis uses the same Lua interpreter to run all the commands. Also Redis +guarantees that a script is executed in an atomic way: no other script or Redis +command will be executed while a script is being executed. This semantic is +similar to the one of `MULTI` / `EXEC`. From the point of view of all the other +clients the effects of a script are either still not visible or already +completed. + +However this also means that executing slow scripts is not a good idea. It is +not hard to create fast scripts, as the script overhead is very low, but if you +are going to use slow scripts you should be aware that while the script is +running no other client can execute commands. + +## Error handling + +As already stated, calls to `redis.call()` resulting in a Redis command error +will stop the execution of the script and return an error, in a way that makes +it obvious that the error was generated by a script: + +``` +> del foo +(integer) 1 +> lpush foo a +(integer) 1 +> eval "return redis.call('get','foo')" 0 +(error) ERR Error running script (call to f_6b1bf486c81ceb7edf3c093f4c48582e38c0e791): ERR Operation against a key holding the wrong kind of value +``` + +Using `redis.pcall()` no error is raised, but an error object is returned in the +format specified above (as a Lua table with an `err` field). The script can pass +the exact error to the user by returning the error object returned by +`redis.pcall()`. + +## Bandwidth and EVALSHA + +The `EVAL` command forces you to send the script body again and again. Redis +does not need to recompile the script every time as it uses an internal caching +mechanism, however paying the cost of the additional bandwidth may not be +optimal in many contexts. + +On the other hand, defining commands using a special command or via `redis.conf` +would be a problem for a few reasons: + +- Different instances may have different implementations of a command. + +- Deployment is hard if we have to make sure all instances contain a given + command, especially in a distributed environment. + +- Reading application code, the complete semantics might not be clear since the + application calls commands defined server side. + +In order to avoid these problems while avoiding the bandwidth penalty, Redis +implements the `EVALSHA` command. + +`EVALSHA` works exactly like `EVAL`, but instead of having a script as the first +argument it has the SHA1 digest of a script. The behavior is the following: + +- If the server still remembers a script with a matching SHA1 digest, the script + is executed. + +- If the server does not remember a script with this SHA1 digest, a special + error is returned telling the client to use `EVAL` instead. + +Example: + +``` +> set foo bar +OK +> eval "return redis.call('get','foo')" 0 +"bar" +> evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0 +"bar" +> evalsha ffffffffffffffffffffffffffffffffffffffff 0 +(error) `NOSCRIPT` No matching script. Please use `EVAL`. +``` + +The client library implementation can always optimistically send `EVALSHA` under +the hood even when the client actually calls `EVAL`, in the hope the script was +already seen by the server. If the `NOSCRIPT` error is returned `EVAL` will be +used instead. + +Passing keys and arguments as additional `EVAL` arguments is also very useful in +this context as the script string remains constant and can be efficiently cached +by Redis. + +## Script cache semantics + +Executed scripts are guaranteed to be in the script cache of a given execution +of a Redis instance forever. This means that if an `EVAL` is performed against a +Redis instance all the subsequent `EVALSHA` calls will succeed. + +The reason why scripts can be cached for long time is that it is unlikely for a +well written application to have enough different scripts to cause memory +problems. Every script is conceptually like the implementation of a new command, +and even a large application will likely have just a few hundred of them. Even +if the application is modified many times and scripts will change, the memory +used is negligible. + +The only way to flush the script cache is by explicitly calling the +`SCRIPT FLUSH` command, which will _completely flush_ the scripts cache removing +all the scripts executed so far. + +This is usually needed only when the instance is going to be instantiated for +another customer or application in a cloud environment. + +Also, as already mentioned, restarting a Redis instance flushes the script +cache, which is not persistent. However from the point of view of the client +there are only two ways to make sure a Redis instance was not restarted between +two different commands. + +- The connection we have with the server is persistent and was never closed so + far. +- The client explicitly checks the `runid` field in the `INFO` command in order + to make sure the server was not restarted and is still the same process. + +Practically speaking, for the client it is much better to simply assume that in +the context of a given connection, cached scripts are guaranteed to be there +unless an administrator explicitly called the `SCRIPT FLUSH` command. + +The fact that the user can count on Redis not removing scripts is semantically +useful in the context of pipelining. + +For instance an application with a persistent connection to Redis can be sure +that if a script was sent once it is still in memory, so EVALSHA can be used +against those scripts in a pipeline without the chance of an error being +generated due to an unknown script (we'll see this problem in detail later). + +A common pattern is to call `SCRIPT LOAD` to load all the scripts that will +appear in a pipeline, then use `EVALSHA` directly inside the pipeline without +any need to check for errors resulting from the script hash not being +recognized. + +## The SCRIPT command + +Redis offers a SCRIPT command that can be used in order to control the scripting +subsystem. SCRIPT currently accepts three different commands: + +- `SCRIPT FLUSH` + + This command is the only way to force Redis to flush the scripts cache. It is + most useful in a cloud environment where the same instance can be reassigned + to a different user. It is also useful for testing client libraries' + implementations of the scripting feature. + +- `SCRIPT EXISTS sha1 sha2 ... shaN` + + Given a list of SHA1 digests as arguments this command returns an array of 1 + or 0, where 1 means the specific SHA1 is recognized as a script already + present in the scripting cache, while 0 means that a script with this SHA1 was + never seen before (or at least never seen after the latest SCRIPT FLUSH + command). + +- `SCRIPT LOAD script` + + This command registers the specified script in the Redis script cache. The + command is useful in all the contexts where we want to make sure that + `EVALSHA` will not fail (for instance during a pipeline or MULTI/EXEC + operation), without the need to actually execute the script. + +- `SCRIPT KILL` + + This command is the only way to interrupt a long-running script that reaches + the configured maximum execution time for scripts. The SCRIPT KILL command can + only be used with scripts that did not modify the dataset during their + execution (since stopping a read-only script does not violate the scripting + engine's guaranteed atomicity). See the next sections for more information + about long running scripts. + +## Scripts as pure functions + +_Note: starting with Redis 5, scripts are always replicated as effects and not +sending the script verbatim. So the following section is mostly applicable to +Redis version 4 or older._ + +A very important part of scripting is writing scripts that are pure functions. +Scripts executed in a Redis instance are, by default, propagated to replicas and +to the AOF file by sending the script itself -- not the resulting commands. + +The reason is that sending a script to another Redis instance is often much +faster than sending the multiple commands the script generates, so if the client +is sending many scripts to the master, converting the scripts into individual +commands for the replica / AOF would result in too much bandwidth for the +replication link or the Append Only File (and also too much CPU since +dispatching a command received via network is a lot more work for Redis compared +to dispatching a command invoked by Lua scripts). + +Normally replicating scripts instead of the effects of the scripts makes sense, +however not in all the cases. So starting with Redis 3.2, the scripting engine +is able to, alternatively, replicate the sequence of write commands resulting +from the script execution, instead of replication the script itself. See the +next section for more information. In this section we'll assume that scripts are +replicated by sending the whole script. Let's call this replication mode **whole +scripts replication**. + +The main drawback with the _whole scripts replication_ approach is that scripts +are required to have the following property: + +- The script must always evaluates the same Redis _write_ commands with the same + arguments given the same input data set. Operations performed by the script + cannot depend on any hidden (non-explicit) information or state that may + change as script execution proceeds or between different executions of the + script, nor can it depend on any external input from I/O devices. + +Things like using the system time, calling Redis random commands like +`RANDOMKEY`, or using Lua random number generator, could result into scripts +that will not always evaluate in the same way. + +In order to enforce this behavior in scripts Redis does the following: + +- Lua does not export commands to access the system time or other external + state. +- Redis will block the script with an error if a script calls a Redis command + able to alter the data set **after** a Redis _random_ command like + `RANDOMKEY`, `SRANDMEMBER`, `TIME`. This means that if a script is read-only + and does not modify the data set it is free to call those commands. Note that + a _random command_ does not necessarily mean a command that uses random + numbers: any non-deterministic command is considered a random command (the + best example in this regard is the `TIME` command). +- In Redis version 4, commands that may return elements in random order, like + `SMEMBERS` (because Redis Sets are _unordered_) have a different behavior when + called from Lua, and undergo a silent lexicographical sorting filter before + returning data to Lua scripts. So `redis.call("smembers",KEYS[1])` will always + return the Set elements in the same order, while the same command invoked from + normal clients may return different results even if the key contains exactly + the same elements. However starting with Redis 5 there is no longer such + ordering step, because Redis 5 replicates scripts in a way that no longer + needs non-deterministic commands to be converted into deterministic ones. In + general, even when developing for Redis 4, never assume that certain commands + in Lua will be ordered, but instead rely on the documentation of the original + command you call to see the properties it provides. +- Lua pseudo random number generation functions `math.random` and + `math.randomseed` are modified in order to always have the same seed every + time a new script is executed. This means that calling `math.random` will + always generate the same sequence of numbers every time a script is executed + if `math.randomseed` is not used. + +However the user is still able to write commands with random behavior using the +following simple trick. Imagine I want to write a Redis script that will +populate a list with N random integers. + +I can start with this small Ruby program: + +``` +require 'rubygems' +require 'redis' + +r = Redis.new + +RandomPushScript = <<EOF + local i = tonumber(ARGV[1]) + local res + while (i > 0) do + res = redis.call('lpush',KEYS[1],math.random()) + i = i-1 + end + return res +EOF + +r.del(:mylist) +puts r.eval(RandomPushScript,[:mylist],[10,rand(2**32)]) +``` + +Every time this script executed the resulting list will have exactly the +following elements: + +``` +> lrange mylist 0 -1 + 1) "0.74509509873814" + 2) "0.87390407681181" + 3) "0.36876626981831" + 4) "0.6921941534114" + 5) "0.7857992587545" + 6) "0.57730350670279" + 7) "0.87046522734243" + 8) "0.09637165539729" + 9) "0.74990198051087" +10) "0.17082803611217" +``` + +In order to make it a pure function, but still be sure that every invocation of +the script will result in different random elements, we can simply add an +additional argument to the script that will be used in order to seed the Lua +pseudo-random number generator. The new script is as follows: + +``` +RandomPushScript = <<EOF + local i = tonumber(ARGV[1]) + local res + math.randomseed(tonumber(ARGV[2])) + while (i > 0) do + res = redis.call('lpush',KEYS[1],math.random()) + i = i-1 + end + return res +EOF + +r.del(:mylist) +puts r.eval(RandomPushScript,1,:mylist,10,rand(2**32)) +``` + +What we are doing here is sending the seed of the PRNG as one of the arguments. +This way the script output will be the same given the same arguments, but we are +changing one of the arguments in every invocation, generating the random seed +client-side. The seed will be propagated as one of the arguments both in the +replication link and in the Append Only File, guaranteeing that the same changes +will be generated when the AOF is reloaded or when the replica processes the +script. + +Note: an important part of this behavior is that the PRNG that Redis implements +as `math.random` and `math.randomseed` is guaranteed to have the same output +regardless of the architecture of the system running Redis. 32-bit, 64-bit, +big-endian and little-endian systems will all produce the same output. + +## Replicating commands instead of scripts + +_Note: starting with Redis 5, the replication method described in this section +(scripts effects replication) is the default and does not need to be explicitly +enabled._ + +Starting with Redis 3.2, it is possible to select an alternative replication +method. Instead of replication whole scripts, we can just replicate single write +commands generated by the script. We call this **script effects replication**. + +In this replication mode, while Lua scripts are executed, Redis collects all the +commands executed by the Lua scripting engine that actually modify the dataset. +When the script execution finishes, the sequence of commands that the script +generated are wrapped into a MULTI / EXEC transaction and are sent to replicas +and AOF. + +This is useful in several ways depending on the use case: + +- When the script is slow to compute, but the effects can be summarized by a few + write commands, it is a shame to re-compute the script on the replicas or when + reloading the AOF. In this case to replicate just the effect of the script is + much better. +- When script effects replication is enabled, the controls about non + deterministic functions are disabled. You can, for example, use the `TIME` or + `SRANDMEMBER` commands inside your scripts freely at any place. +- The Lua PRNG in this mode is seeded randomly at every call. + +In order to enable script effects replication, you need to issue the following +Lua command before any write operated by the script: + + redis.replicate_commands() + +The function returns true if the script effects replication was enabled, +otherwise if the function was called after the script already called some write +command, it returns false, and normal whole script replication is used. + +## Selective replication of commands + +When script effects replication is selected (see the previous section), it is +possible to have more control in the way commands are replicated to replicas and +AOF. This is a very advanced feature since **a misuse can do damage** by +breaking the contract that the master, replicas, and AOF, all must contain the +same logical content. + +However this is a useful feature since, sometimes, we need to execute certain +commands only in the master in order to create, for example, intermediate +values. + +Think at a Lua script where we perform an intersection between two sets. Pick +five random elements, and create a new set with this five random elements. +Finally we delete the temporary key representing the intersection between the +two original sets. What we want to replicate is only the creation of the new set +with the five elements. It's not useful to also replicate the commands creating +the temporary key. + +For this reason, Redis 3.2 introduces a new command that only works when script +effects replication is enabled, and is able to control the scripting replication +engine. The command is called `redis.set_repl()` and fails raising an error if +called when script effects replication is disabled. + +The command can be called with four different arguments: + + redis.set_repl(redis.REPL_ALL) -- Replicate to AOF and replicas. + redis.set_repl(redis.REPL_AOF) -- Replicate only to AOF. + redis.set_repl(redis.REPL_REPLICA) -- Replicate only to replicas (Redis >= 5) + redis.set_repl(redis.REPL_SLAVE) -- Used for backward compatibility, the same as REPL_REPLICA. + redis.set_repl(redis.REPL_NONE) -- Don't replicate at all. + +By default the scripting engine is always set to `REPL_ALL`. By calling this +function the user can switch on/off AOF and or replicas propagation, and turn +them back later at her/his wish. + +A simple example follows: + + redis.replicate_commands() -- Enable effects replication. + redis.call('set','A','1') + redis.set_repl(redis.REPL_NONE) + redis.call('set','B','2') + redis.set_repl(redis.REPL_ALL) + redis.call('set','C','3') + +After running the above script, the result is that only keys A and C will be +created on replicas and AOF. + +## Global variables protection + +Redis scripts are not allowed to create global variables, in order to avoid +leaking data into the Lua state. If a script needs to maintain state between +calls (a pretty uncommon need) it should use Redis keys instead. + +When global variable access is attempted the script is terminated and EVAL +returns with an error: + +``` +redis 127.0.0.1:6379> eval 'a=10' 0 +(error) ERR Error running script (call to f_933044db579a2f8fd45d8065f04a8d0249383e57): user_script:1: Script attempted to create global variable 'a' +``` + +Accessing a _non existing_ global variable generates a similar error. + +Using Lua debugging functionality or other approaches like altering the meta +table used to implement global protections in order to circumvent globals +protection is not hard. However it is difficult to do it accidentally. If the +user messes with the Lua global state, the consistency of AOF and replication is +not guaranteed: don't do it. + +Note for Lua newbies: in order to avoid using global variables in your scripts +simply declare every variable you are going to use using the _local_ keyword. + +## Using SELECT inside scripts + +It is possible to call `SELECT` inside Lua scripts like with normal clients, +However one subtle aspect of the behavior changes between Redis 2.8.11 and Redis +2.8.12. Before the 2.8.12 release the database selected by the Lua script was +_transferred_ to the calling script as current database. Starting from Redis +2.8.12 the database selected by the Lua script only affects the execution of the +script itself, but does not modify the database selected by the client calling +the script. + +The semantic change between patch level releases was needed since the old +behavior was inherently incompatible with the Redis replication layer and was +the cause of bugs. + +## Using Lua scripting in RESP3 mode + +Starting with Redis version 6, the server supports two differnent protocols. One +is called RESP2, and is the old protocol: all the new connections to the server +start in this mode. However clients are able to negotiate the new protocol using +the `HELLO` command: this way the connection is put in RESP3 mode. In this mode +certain commands, like for instance `HGETALL`, reply with a new data type (the +Map data type in this specific case). The RESP3 protocol is semantically more +powerful, however most scripts are ok with using just RESP2. + +The Lua engine always assumes to run in RESP2 mode when talking with Redis, so +whatever the connection that is invoking the `EVAL` or `EVALSHA` command is in +RESP2 or RESP3 mode, Lua scripts will, by default, still see the same kind of +replies they used to see in the past from Redis, when calling commands using the +`redis.call()` built-in function. + +However Lua scripts running in Redis 6 or greater, are able to switch to RESP3 +mode, and get the replies using the new available types. Similarly Lua scripts +are able to reply to clients using the new types. Please make sure to understand +[the capabilities for RESP3](https://github.com/antirez/resp3) before continuing +reading this section. + +In order to switch to RESP3 a script should call this function: + + redis.setresp(3) + +Note that a script can switch back and forth from RESP3 and RESP2 by calling the +function with the argument '3' or '2'. + +At this point the new conversions are available, specifically: + +**Redis to Lua** conversion table specific to RESP3: + +- Redis map reply -> Lua table with a single `map` field containing a Lua table + representing the fields and values of the map. +- Redis set reply -> Lua table with a single `set` field containing a Lua table + representing the elements of the set as fields, having as value just `true`. +- Redis new RESP3 single null value -> Lua nil. +- Redis true reply -> Lua true boolean value. +- Redis false reply -> Lua false boolean value. +- Redis double reply -> Lua table with a single `score` field containing a Lua + number representing the double value. +- All the RESP2 old conversions still apply. + +**Lua to Redis** conversion table specific for RESP3. + +- Lua boolean -> Redis boolean true or false. **Note that this is a change + compared to the RESP2 mode**, where returning true from Lua returned the + number 1 to the Redis client, and returning false used to return NULL. +- Lua table with a single `map` field set to a field-value Lua table -> Redis + map reply. +- Lua table with a single `set` field set to a field-value Lua table -> Redis + set reply, the values are discared and can be anything. +- Lua table with a single `double` field set to a field-value Lua table -> Redis + double reply. +- Lua null -> Redis RESP3 new null reply (protocol `"_\r\n"`). +- All the RESP2 old conversions still apply unless specified above. + +There is one key thing to understand: in case Lua replies with RESP3 types, but +the connection calling Lua is in RESP2 mode, Redis will automatically convert +the RESP3 protocol to RESP2 compatible protocol, as it happens for normal +commands. For instance returning a map type to a connection in RESP2 mode will +have the effect of returning a flat array of fields and values. + +## Available libraries + +The Redis Lua interpreter loads the following Lua libraries: + +- `base` lib. +- `table` lib. +- `string` lib. +- `math` lib. +- `struct` lib. +- `cjson` lib. +- `cmsgpack` lib. +- `bitop` lib. +- `redis.sha1hex` function. +- `redis.breakpoint and redis.debug` function in the context of the + [Redis Lua debugger](/topics/ldb). + +Every Redis instance is _guaranteed_ to have all the above libraries so you can +be sure that the environment for your Redis scripts is always the same. + +struct, CJSON and cmsgpack are external libraries, all the other libraries are +standard Lua libraries. + +### struct + +struct is a library for packing/unpacking structures within Lua. + +``` +Valid formats: +> - big endian +< - little endian +![num] - alignment +x - pading +b/B - signed/unsigned byte +h/H - signed/unsigned short +l/L - signed/unsigned long +T - size_t +i/In - signed/unsigned integer with size `n' (default is size of int) +cn - sequence of `n' chars (from/to a string); when packing, n==0 means + the whole string; when unpacking, n==0 means use the previous + read number as the string length +s - zero-terminated string +f - float +d - double +' ' - ignored +``` + +Example: + +``` +127.0.0.1:6379> eval 'return struct.pack("HH", 1, 2)' 0 +"\x01\x00\x02\x00" +127.0.0.1:6379> eval 'return {struct.unpack("HH", ARGV[1])}' 0 "\x01\x00\x02\x00" +1) (integer) 1 +2) (integer) 2 +3) (integer) 5 +127.0.0.1:6379> eval 'return struct.size("HH")' 0 +(integer) 4 +``` + +### CJSON + +The CJSON library provides extremely fast JSON manipulation within Lua. + +Example: + +``` +redis 127.0.0.1:6379> eval 'return cjson.encode({["foo"]= "bar"})' 0 +"{\"foo\":\"bar\"}" +redis 127.0.0.1:6379> eval 'return cjson.decode(ARGV[1])["foo"]' 0 "{\"foo\":\"bar\"}" +"bar" +``` + +### cmsgpack + +The cmsgpack library provides simple and fast MessagePack manipulation within +Lua. + +Example: + +``` +127.0.0.1:6379> eval 'return cmsgpack.pack({"foo", "bar", "baz"})' 0 +"\x93\xa3foo\xa3bar\xa3baz" +127.0.0.1:6379> eval 'return cmsgpack.unpack(ARGV[1])' 0 "\x93\xa3foo\xa3bar\xa3baz" +1) "foo" +2) "bar" +3) "baz" +``` + +### bitop + +The Lua Bit Operations Module adds bitwise operations on numbers. It is +available for scripting in Redis since version 2.8.18. + +Example: + +``` +127.0.0.1:6379> eval 'return bit.tobit(1)' 0 +(integer) 1 +127.0.0.1:6379> eval 'return bit.bor(1,2,4,8,16,32,64,128)' 0 +(integer) 255 +127.0.0.1:6379> eval 'return bit.tohex(422342)' 0 +"000671c6" +``` + +It supports several other functions: `bit.tobit`, `bit.tohex`, `bit.bnot`, +`bit.band`, `bit.bor`, `bit.bxor`, `bit.lshift`, `bit.rshift`, `bit.arshift`, +`bit.rol`, `bit.ror`, `bit.bswap`. All available functions are documented in the +[Lua BitOp documentation](http://bitop.luajit.org/api.html) + +### `redis.sha1hex` + +Perform the SHA1 of the input string. + +Example: + +``` +127.0.0.1:6379> eval 'return redis.sha1hex(ARGV[1])' 0 "foo" +"0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33" +``` + +## Emitting Redis logs from scripts + +It is possible to write to the Redis log file from Lua scripts using the +`redis.log` function. + +``` +redis.log(loglevel,message) +``` + +`loglevel` is one of: + +- `redis.LOG_DEBUG` +- `redis.LOG_VERBOSE` +- `redis.LOG_NOTICE` +- `redis.LOG_WARNING` + +They correspond directly to the normal Redis log levels. Only logs emitted by +scripting using a log level that is equal or greater than the currently +configured Redis instance log level will be emitted. + +The `message` argument is simply a string. Example: + +``` +redis.log(redis.LOG_WARNING,"Something is wrong with this script.") +``` + +Will generate the following: + +``` +[32343] 22 Mar 15:21:39 # Something is wrong with this script. +``` + +## Sandbox and maximum execution time + +Scripts should never try to access the external system, like the file system or +any other system call. A script should only operate on Redis data and passed +arguments. + +Scripts are also subject to a maximum execution time (five seconds by default). +This default timeout is huge since a script should usually run in under a +millisecond. The limit is mostly to handle accidental infinite loops created +during development. + +It is possible to modify the maximum time a script can be executed with +millisecond precision, either via `redis.conf` or using the CONFIG GET / CONFIG +SET command. The configuration parameter affecting max execution time is called +`lua-time-limit`. + +When a script reaches the timeout it is not automatically terminated by Redis +since this violates the contract Redis has with the scripting engine to ensure +that scripts are atomic. Interrupting a script means potentially leaving the +dataset with half-written data. For this reasons when a script executes for more +than the specified time the following happens: + +- Redis logs that a script is running too long. +- It starts accepting commands again from other clients, but will reply with a + BUSY error to all the clients sending normal commands. The only allowed + commands in this status are `SCRIPT KILL` and `SHUTDOWN NOSAVE`. +- It is possible to terminate a script that executes only read-only commands + using the `SCRIPT KILL` command. This does not violate the scripting semantic + as no data was yet written to the dataset by the script. +- If the script already called write commands the only allowed command becomes + `SHUTDOWN NOSAVE` that stops the server without saving the current data set on + disk (basically the server is aborted). + +## EVALSHA in the context of pipelining + +Care should be taken when executing `EVALSHA` in the context of a pipelined +request, since even in a pipeline the order of execution of commands must be +guaranteed. If `EVALSHA` will return a `NOSCRIPT` error the command can not be +reissued later otherwise the order of execution is violated. + +The client library implementation should take one of the following approaches: + +- Always use plain `EVAL` when in the context of a pipeline. + +- Accumulate all the commands to send into the pipeline, then check for `EVAL` + commands and use the `SCRIPT EXISTS` command to check if all the scripts are + already defined. If not, add `SCRIPT LOAD` commands on top of the pipeline as + required, and use `EVALSHA` for all the `EVAL` calls. + +## Debugging Lua scripts + +Starting with Redis 3.2, Redis has support for native Lua debugging. The Redis +Lua debugger is a remote debugger consisting of a server, which is Redis itself, +and a client, which is by default `redis-cli`. + +The Lua debugger is described in the [Lua scripts debugging](/topics/ldb) +section of the Redis documentation. diff --git a/iredis/data/commands/evalsha.md b/iredis/data/commands/evalsha.md new file mode 100644 index 0000000..b3bb515 --- /dev/null +++ b/iredis/data/commands/evalsha.md @@ -0,0 +1,3 @@ +Evaluates a script cached on the server side by its SHA1 digest. Scripts are +cached on the server side using the `SCRIPT LOAD` command. The command is +otherwise identical to `EVAL`. diff --git a/iredis/data/commands/exec.md b/iredis/data/commands/exec.md new file mode 100644 index 0000000..b35d04a --- /dev/null +++ b/iredis/data/commands/exec.md @@ -0,0 +1,16 @@ +Executes all previously queued commands in a [transaction][tt] and restores the +connection state to normal. + +[tt]: /topics/transactions + +When using `WATCH`, `EXEC` will execute commands only if the watched keys were +not modified, allowing for a [check-and-set mechanism][ttc]. + +[ttc]: /topics/transactions#cas + +@return + +@array-reply: each element being the reply to each of the commands in the atomic +transaction. + +When using `WATCH`, `EXEC` can return a @nil-reply if the execution was aborted. diff --git a/iredis/data/commands/exists.md b/iredis/data/commands/exists.md new file mode 100644 index 0000000..83c2043 --- /dev/null +++ b/iredis/data/commands/exists.md @@ -0,0 +1,33 @@ +Returns if `key` exists. + +Since Redis 3.0.3 it is possible to specify multiple keys instead of a single +one. In such a case, it returns the total number of keys existing. Note that +returning 1 or 0 for a single key is just a special case of the variadic usage, +so the command is completely backward compatible. + +The user should be aware that if the same existing key is mentioned in the +arguments multiple times, it will be counted multiple times. So if `somekey` +exists, `EXISTS somekey somekey` will return 2. + +@return + +@integer-reply, specifically: + +- `1` if the key exists. +- `0` if the key does not exist. + +Since Redis 3.0.3 the command accepts a variable number of keys and the return +value is generalized: + +- The number of keys existing among the ones specified as arguments. Keys + mentioned multiple times and existing are counted multiple times. + +@examples + +```cli +SET key1 "Hello" +EXISTS key1 +EXISTS nosuchkey +SET key2 "World" +EXISTS key1 key2 nosuchkey +``` diff --git a/iredis/data/commands/expire.md b/iredis/data/commands/expire.md new file mode 100644 index 0000000..e5ca954 --- /dev/null +++ b/iredis/data/commands/expire.md @@ -0,0 +1,166 @@ +Set a timeout on `key`. After the timeout has expired, the key will +automatically be deleted. A key with an associated timeout is often said to be +_volatile_ in Redis terminology. + +The timeout will only be cleared by commands that delete or overwrite the +contents of the key, including `DEL`, `SET`, `GETSET` and all the `*STORE` +commands. This means that all the operations that conceptually _alter_ the value +stored at the key without replacing it with a new one will leave the timeout +untouched. For instance, incrementing the value of a key with `INCR`, pushing a +new value into a list with `LPUSH`, or altering the field value of a hash with +`HSET` are all operations that will leave the timeout untouched. + +The timeout can also be cleared, turning the key back into a persistent key, +using the `PERSIST` command. + +If a key is renamed with `RENAME`, the associated time to live is transferred to +the new key name. + +If a key is overwritten by `RENAME`, like in the case of an existing key `Key_A` +that is overwritten by a call like `RENAME Key_B Key_A`, it does not matter if +the original `Key_A` had a timeout associated or not, the new key `Key_A` will +inherit all the characteristics of `Key_B`. + +Note that calling `EXPIRE`/`PEXPIRE` with a non-positive timeout or +`EXPIREAT`/`PEXPIREAT` with a time in the past will result in the key being +[deleted][del] rather than expired (accordingly, the emitted [key event][ntf] +will be `del`, not `expired`). + +[del]: /commands/del +[ntf]: /topics/notifications + +## Refreshing expires + +It is possible to call `EXPIRE` using as argument a key that already has an +existing expire set. In this case the time to live of a key is _updated_ to the +new value. There are many useful applications for this, an example is documented +in the _Navigation session_ pattern section below. + +## Differences in Redis prior 2.1.3 + +In Redis versions prior **2.1.3** altering a key with an expire set using a +command altering its value had the effect of removing the key entirely. This +semantics was needed because of limitations in the replication layer that are +now fixed. + +`EXPIRE` would return 0 and not alter the timeout for a key with a timeout set. + +@return + +@integer-reply, specifically: + +- `1` if the timeout was set. +- `0` if `key` does not exist. + +@examples + +```cli +SET mykey "Hello" +EXPIRE mykey 10 +TTL mykey +SET mykey "Hello World" +TTL mykey +``` + +## Pattern: Navigation session + +Imagine you have a web service and you are interested in the latest N pages +_recently_ visited by your users, such that each adjacent page view was not +performed more than 60 seconds after the previous. Conceptually you may consider +this set of page views as a _Navigation session_ of your user, that may contain +interesting information about what kind of products he or she is looking for +currently, so that you can recommend related products. + +You can easily model this pattern in Redis using the following strategy: every +time the user does a page view you call the following commands: + +``` +MULTI +RPUSH pagewviews.user:<userid> http://..... +EXPIRE pagewviews.user:<userid> 60 +EXEC +``` + +If the user will be idle more than 60 seconds, the key will be deleted and only +subsequent page views that have less than 60 seconds of difference will be +recorded. + +This pattern is easily modified to use counters using `INCR` instead of lists +using `RPUSH`. + +# Appendix: Redis expires + +## Keys with an expire + +Normally Redis keys are created without an associated time to live. The key will +simply live forever, unless it is removed by the user in an explicit way, for +instance using the `DEL` command. + +The `EXPIRE` family of commands is able to associate an expire to a given key, +at the cost of some additional memory used by the key. When a key has an expire +set, Redis will make sure to remove the key when the specified amount of time +elapsed. + +The key time to live can be updated or entirely removed using the `EXPIRE` and +`PERSIST` command (or other strictly related commands). + +## Expire accuracy + +In Redis 2.4 the expire might not be pin-point accurate, and it could be between +zero to one seconds out. + +Since Redis 2.6 the expire error is from 0 to 1 milliseconds. + +## Expires and persistence + +Keys expiring information is stored as absolute Unix timestamps (in milliseconds +in case of Redis version 2.6 or greater). This means that the time is flowing +even when the Redis instance is not active. + +For expires to work well, the computer time must be taken stable. If you move an +RDB file from two computers with a big desync in their clocks, funny things may +happen (like all the keys loaded to be expired at loading time). + +Even running instances will always check the computer clock, so for instance if +you set a key with a time to live of 1000 seconds, and then set your computer +time 2000 seconds in the future, the key will be expired immediately, instead of +lasting for 1000 seconds. + +## How Redis expires keys + +Redis keys are expired in two ways: a passive way, and an active way. + +A key is passively expired simply when some client tries to access it, and the +key is found to be timed out. + +Of course this is not enough as there are expired keys that will never be +accessed again. These keys should be expired anyway, so periodically Redis tests +a few keys at random among keys with an expire set. All the keys that are +already expired are deleted from the keyspace. + +Specifically this is what Redis does 10 times per second: + +1. Test 20 random keys from the set of keys with an associated expire. +2. Delete all the keys found expired. +3. If more than 25% of keys were expired, start again from step 1. + +This is a trivial probabilistic algorithm, basically the assumption is that our +sample is representative of the whole key space, and we continue to expire until +the percentage of keys that are likely to be expired is under 25% + +This means that at any given moment the maximum amount of keys already expired +that are using memory is at max equal to max amount of write operations per +second divided by 4. + +## How expires are handled in the replication link and AOF file + +In order to obtain a correct behavior without sacrificing consistency, when a +key expires, a `DEL` operation is synthesized in both the AOF file and gains all +the attached replicas nodes. This way the expiration process is centralized in +the master instance, and there is no chance of consistency errors. + +However while the replicas connected to a master will not expire keys +independently (but will wait for the `DEL` coming from the master), they'll +still take the full state of the expires existing in the dataset, so when a +replica is elected to master it will be able to expire the keys independently, +fully acting as a master. diff --git a/iredis/data/commands/expireat.md b/iredis/data/commands/expireat.md new file mode 100644 index 0000000..92c4e9a --- /dev/null +++ b/iredis/data/commands/expireat.md @@ -0,0 +1,31 @@ +`EXPIREAT` has the same effect and semantic as `EXPIRE`, but instead of +specifying the number of seconds representing the TTL (time to live), it takes +an absolute [Unix timestamp][hewowu] (seconds since January 1, 1970). A +timestamp in the past will delete the key immediately. + +[hewowu]: http://en.wikipedia.org/wiki/Unix_time + +Please for the specific semantics of the command refer to the documentation of +`EXPIRE`. + +## Background + +`EXPIREAT` was introduced in order to convert relative timeouts to absolute +timeouts for the AOF persistence mode. Of course, it can be used directly to +specify that a given key should expire at a given time in the future. + +@return + +@integer-reply, specifically: + +- `1` if the timeout was set. +- `0` if `key` does not exist. + +@examples + +```cli +SET mykey "Hello" +EXISTS mykey +EXPIREAT mykey 1293840000 +EXISTS mykey +``` diff --git a/iredis/data/commands/flushall.md b/iredis/data/commands/flushall.md new file mode 100644 index 0000000..8cc7560 --- /dev/null +++ b/iredis/data/commands/flushall.md @@ -0,0 +1,19 @@ +Delete all the keys of all the existing databases, not just the currently +selected one. This command never fails. + +The time-complexity for this operation is O(N), N being the number of keys in +all existing databases. + +## `FLUSHALL ASYNC` (Redis 4.0.0 or greater) + +Redis is now able to delete keys in the background in a different thread without +blocking the server. An `ASYNC` option was added to `FLUSHALL` and `FLUSHDB` in +order to let the entire dataset or a single database to be freed asynchronously. + +Asynchronous `FLUSHALL` and `FLUSHDB` commands only delete keys that were +present at the time the command was invoked. Keys created during an asynchronous +flush will be unaffected. + +@return + +@simple-string-reply diff --git a/iredis/data/commands/flushdb.md b/iredis/data/commands/flushdb.md new file mode 100644 index 0000000..8c76001 --- /dev/null +++ b/iredis/data/commands/flushdb.md @@ -0,0 +1,12 @@ +Delete all the keys of the currently selected DB. This command never fails. + +The time-complexity for this operation is O(N), N being the number of keys in +the database. + +## `FLUSHDB ASYNC` (Redis 4.0.0 or greater) + +See `FLUSHALL` for documentation. + +@return + +@simple-string-reply diff --git a/iredis/data/commands/geoadd.md b/iredis/data/commands/geoadd.md new file mode 100644 index 0000000..32751db --- /dev/null +++ b/iredis/data/commands/geoadd.md @@ -0,0 +1,56 @@ +Adds the specified geospatial items (latitude, longitude, name) to the specified +key. Data is stored into the key as a sorted set, in a way that makes it +possible to later retrieve items using a query by radius with the `GEORADIUS` or +`GEORADIUSBYMEMBER` commands. + +The command takes arguments in the standard format x,y so the longitude must be +specified before the latitude. There are limits to the coordinates that can be +indexed: areas very near to the poles are not indexable. The exact limits, as +specified by EPSG:900913 / EPSG:3785 / OSGEO:41001 are the following: + +- Valid longitudes are from -180 to 180 degrees. +- Valid latitudes are from -85.05112878 to 85.05112878 degrees. + +The command will report an error when the user attempts to index coordinates +outside the specified ranges. + +**Note:** there is no **GEODEL** command because you can use `ZREM` in order to +remove elements. The Geo index structure is just a sorted set. + +## How does it work? + +The way the sorted set is populated is using a technique called +[Geohash](https://en.wikipedia.org/wiki/Geohash). Latitude and Longitude bits +are interleaved in order to form an unique 52 bit integer. We know that a sorted +set double score can represent a 52 bit integer without losing precision. + +This format allows for radius querying by checking the 1+8 areas needed to cover +the whole radius, and discarding elements outside the radius. The areas are +checked by calculating the range of the box covered removing enough bits from +the less significant part of the sorted set score, and computing the score range +to query in the sorted set for each area. + +## What Earth model does it use? + +It just assumes that the Earth is a sphere, since the used distance formula is +the Haversine formula. This formula is only an approximation when applied to the +Earth, which is not a perfect sphere. The introduced errors are not an issue +when used in the context of social network sites that need to query by radius +and most other applications. However in the worst case the error may be up to +0.5%, so you may want to consider other systems for error-critical applications. + +@return + +@integer-reply, specifically: + +- The number of elements added to the sorted set, not including elements already + existing for which the score was updated. + +@examples + +```cli +GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +GEODIST Sicily Palermo Catania +GEORADIUS Sicily 15 37 100 km +GEORADIUS Sicily 15 37 200 km +``` diff --git a/iredis/data/commands/geodecode.md b/iredis/data/commands/geodecode.md new file mode 100644 index 0000000..f083fd9 --- /dev/null +++ b/iredis/data/commands/geodecode.md @@ -0,0 +1,34 @@ +Geospatial Redis commands encode positions of objects in a single 52 bit +integer, using a technique called geohash. Those 52 bit integers are: + +1. Returned by `GEOAENCODE` as return value. +2. Used by `GEOADD` as sorted set scores of members. + +The `GEODECODE` command is able to translate the 52 bit integers back into a +position expressed as longitude and latitude. The command also returns the +corners of the box that the 52 bit integer identifies on the earth surface, +since each 52 integer actually represent not a single point, but a small area. + +This command usefulness is limited to the rare situations where you want to +fetch raw data from the sorted set, for example with `ZRANGE`, and later need to +decode the scores into positions. The other obvious use is debugging. + +@return + +@array-reply, specifically: + +The command returns an array of three elements. Each element of the main array +is an array of two elements, specifying a longitude and a latitude. So the +returned value is in the following form: + +- center-longitude, center-latitude +- min-longitude, min-latitude +- max-longitude, max-latitude + +@examples + +```cli +GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +ZSCORE Sicily "Palermo" +GEODECODE 3479099956230698 +``` diff --git a/iredis/data/commands/geodist.md b/iredis/data/commands/geodist.md new file mode 100644 index 0000000..3f32a89 --- /dev/null +++ b/iredis/data/commands/geodist.md @@ -0,0 +1,35 @@ +Return the distance between two members in the geospatial index represented by +the sorted set. + +Given a sorted set representing a geospatial index, populated using the `GEOADD` +command, the command returns the distance between the two specified members in +the specified unit. + +If one or both the members are missing, the command returns NULL. + +The unit must be one of the following, and defaults to meters: + +- **m** for meters. +- **km** for kilometers. +- **mi** for miles. +- **ft** for feet. + +The distance is computed assuming that the Earth is a perfect sphere, so errors +up to 0.5% are possible in edge cases. + +@return + +@bulk-string-reply, specifically: + +The command returns the distance as a double (represented as a string) in the +specified unit, or NULL if one or both the elements are missing. + +@examples + +```cli +GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +GEODIST Sicily Palermo Catania +GEODIST Sicily Palermo Catania km +GEODIST Sicily Palermo Catania mi +GEODIST Sicily Foo Bar +``` diff --git a/iredis/data/commands/geoencode.md b/iredis/data/commands/geoencode.md new file mode 100644 index 0000000..5e10af6 --- /dev/null +++ b/iredis/data/commands/geoencode.md @@ -0,0 +1,57 @@ +Geospatial Redis commands encode positions of objects in a single 52 bit +integer, using a technique called geohash. The encoding is further explained in +the `GEODECODE` and `GEOADD` documentation. The `GEOENCODE` command, documented +in this page, is able to convert a longitude and latitude pair into such 52 bit +integer, which is used as the _score_ for the sorted set members representing +geopositional information. + +Normally you don't need to use this command, unless you plan to implement low +level code in the client side interacting with the Redis geo commands. This +command may also be useful for debugging purposes. + +`GEOENCODE` takes as input: + +1. The longitude and latitude of a point on the Earth surface. +2. Optionally a radius represented by an integer and an unit. + +And returns a set of information, including the representation of the position +as a 52 bit integer, the min and max corners of the bounding box represented by +the geo hash, the center point in the area covered by the geohash integer, and +finally the two sorted set scores to query in order to retrieve all the elements +included in the geohash area. + +The radius optionally provided to the command is used in order to compute the +two scores returned by the command for range query purposes. Moreover the +returned geohash integer will only have the most significant bits set, according +to the number of bits needed to approximate the specified radius. + +## Use case + +As already specified this command is mostly not needed if not for debugging. +However there are actual use cases, which is, when there is to query for the +same areas multiple times, or with a different granularity or area shape +compared to what Redis `GEORADIUS` is able to provide, the client may implement +using this command part of the logic on the client side. Score ranges +representing given areas can be cached client side and used to retrieve elements +directly using `ZRANGEBYSCORE`. + +@return + +@array-reply, specifically: + +The command returns an array of give elements in the following order: + +- The 52 bit geohash +- min-longitude, min-latitude of the area identified +- max-longitude, max-latitude of the area identified +- center-longitude, center-latitude +- min-score and max-score of the sorted set to retrieve the members inside the + area + +@examples + +```cli +GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +ZSCORE Sicily "Palermo" +GEOENCODE 13.361389 38.115556 100 km +``` diff --git a/iredis/data/commands/geohash.md b/iredis/data/commands/geohash.md new file mode 100644 index 0000000..f091912 --- /dev/null +++ b/iredis/data/commands/geohash.md @@ -0,0 +1,39 @@ +Return valid [Geohash](https://en.wikipedia.org/wiki/Geohash) strings +representing the position of one or more elements in a sorted set value +representing a geospatial index (where elements were added using `GEOADD`). + +Normally Redis represents positions of elements using a variation of the Geohash +technique where positions are encoded using 52 bit integers. The encoding is +also different compared to the standard because the initial min and max +coordinates used during the encoding and decoding process are different. This +command however **returns a standard Geohash** in the form of a string as +described in the [Wikipedia article](https://en.wikipedia.org/wiki/Geohash) and +compatible with the [geohash.org](http://geohash.org) web site. + +## Geohash string properties + +The command returns 11 characters Geohash strings, so no precision is loss +compared to the Redis internal 52 bit representation. The returned Geohashes +have the following properties: + +1. They can be shortened removing characters from the right. It will lose + precision but will still point to the same area. +2. It is possible to use them in `geohash.org` URLs such as + `http://geohash.org/<geohash-string>`. This is an + [example of such URL](http://geohash.org/sqdtr74hyu0). +3. Strings with a similar prefix are nearby, but the contrary is not true, it is + possible that strings with different prefixes are nearby too. + +@return + +@array-reply, specifically: + +The command returns an array where each element is the Geohash corresponding to +each member name passed as argument to the command. + +@examples + +```cli +GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +GEOHASH Sicily Palermo Catania +``` diff --git a/iredis/data/commands/geopos.md b/iredis/data/commands/geopos.md new file mode 100644 index 0000000..14b941c --- /dev/null +++ b/iredis/data/commands/geopos.md @@ -0,0 +1,28 @@ +Return the positions (longitude,latitude) of all the specified members of the +geospatial index represented by the sorted set at _key_. + +Given a sorted set representing a geospatial index, populated using the `GEOADD` +command, it is often useful to obtain back the coordinates of specified members. +When the geospatial index is populated via `GEOADD` the coordinates are +converted into a 52 bit geohash, so the coordinates returned may not be exactly +the ones used in order to add the elements, but small errors may be introduced. + +The command can accept a variable number of arguments so it always returns an +array of positions even when a single element is specified. + +@return + +@array-reply, specifically: + +The command returns an array where each element is a two elements array +representing longitude and latitude (x,y) of each member name passed as argument +to the command. + +Non existing elements are reported as NULL elements of the array. + +@examples + +```cli +GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +GEOPOS Sicily Palermo Catania NonExisting +``` diff --git a/iredis/data/commands/georadius.md b/iredis/data/commands/georadius.md new file mode 100644 index 0000000..a1dd20d --- /dev/null +++ b/iredis/data/commands/georadius.md @@ -0,0 +1,103 @@ +Return the members of a sorted set populated with geospatial information using +`GEOADD`, which are within the borders of the area specified with the center +location and the maximum distance from the center (the radius). + +This manual page also covers the `GEORADIUS_RO` and `GEORADIUSBYMEMBER_RO` +variants (see the section below for more information). + +The common use case for this command is to retrieve geospatial items near a +specified point not farther than a given amount of meters (or other units). This +allows, for example, to suggest mobile users of an application nearby places. + +The radius is specified in one of the following units: + +- **m** for meters. +- **km** for kilometers. +- **mi** for miles. +- **ft** for feet. + +The command optionally returns additional information using the following +options: + +- `WITHDIST`: Also return the distance of the returned items from the specified + center. The distance is returned in the same unit as the unit specified as the + radius argument of the command. +- `WITHCOORD`: Also return the longitude,latitude coordinates of the matching + items. +- `WITHHASH`: Also return the raw geohash-encoded sorted set score of the item, + in the form of a 52 bit unsigned integer. This is only useful for low level + hacks or debugging and is otherwise of little interest for the general user. + +The command default is to return unsorted items. Two different sorting methods +can be invoked using the following two options: + +- `ASC`: Sort returned items from the nearest to the farthest, relative to the + center. +- `DESC`: Sort returned items from the farthest to the nearest, relative to the + center. + +By default all the matching items are returned. It is possible to limit the +results to the first N matching items by using the **COUNT `<count>`** option. +However note that internally the command needs to perform an effort proportional +to the number of items matching the specified area, so to query very large areas +with a very small `COUNT` option may be slow even if just a few results are +returned. On the other hand `COUNT` can be a very effective way to reduce +bandwidth usage if normally just the first results are used. + +By default the command returns the items to the client. It is possible to store +the results with one of these options: + +- `!STORE`: Store the items in a sorted set populated with their geospatial + information. +- `!STOREDIST`: Store the items in a sorted set populated with their distance + from the center as a floating point number, in the same unit specified in the + radius. + +@return + +@array-reply, specifically: + +- Without any `WITH` option specified, the command just returns a linear array + like ["New York","Milan","Paris"]. +- If `WITHCOORD`, `WITHDIST` or `WITHHASH` options are specified, the command + returns an array of arrays, where each sub-array represents a single item. + +When additional information is returned as an array of arrays for each item, the +first item in the sub-array is always the name of the returned item. The other +information is returned in the following order as successive elements of the +sub-array. + +1. The distance from the center as a floating point number, in the same unit + specified in the radius. +2. The geohash integer. +3. The coordinates as a two items x,y array (longitude,latitude). + +So for example the command `GEORADIUS Sicily 15 37 200 km WITHCOORD WITHDIST` +will return each item in the following way: + + ["Palermo","190.4424",["13.361389338970184","38.115556395496299"]] + +## Read only variants + +Since `GEORADIUS` and `GEORADIUSBYMEMBER` have a `STORE` and `STOREDIST` option +they are technically flagged as writing commands in the Redis command table. For +this reason read-only replicas will flag them, and Redis Cluster replicas will +redirect them to the master instance even if the connection is in read only mode +(See the `READONLY` command of Redis Cluster). + +Breaking the compatibility with the past was considered but rejected, at least +for Redis 4.0, so instead two read only variants of the commands were added. +They are exactly like the original commands but refuse the `STORE` and +`STOREDIST` options. The two variants are called `GEORADIUS_RO` and +`GEORADIUSBYMEMBER_RO`, and can safely be used in replicas. + +Both commands were introduced in Redis 3.2.10 and Redis 4.0.0 respectively. + +@examples + +```cli +GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +GEORADIUS Sicily 15 37 200 km WITHDIST +GEORADIUS Sicily 15 37 200 km WITHCOORD +GEORADIUS Sicily 15 37 200 km WITHDIST WITHCOORD +``` diff --git a/iredis/data/commands/georadiusbymember.md b/iredis/data/commands/georadiusbymember.md new file mode 100644 index 0000000..88c1be4 --- /dev/null +++ b/iredis/data/commands/georadiusbymember.md @@ -0,0 +1,21 @@ +This command is exactly like `GEORADIUS` with the sole difference that instead +of taking, as the center of the area to query, a longitude and latitude value, +it takes the name of a member already existing inside the geospatial index +represented by the sorted set. + +The position of the specified member is used as the center of the query. + +Please check the example below and the `GEORADIUS` documentation for more +information about the command and its options. + +Note that `GEORADIUSBYMEMBER_RO` is also available since Redis 3.2.10 and Redis +4.0.0 in order to provide a read-only command that can be used in replicas. See +the `GEORADIUS` page for more information. + +@examples + +```cli +GEOADD Sicily 13.583333 37.316667 "Agrigento" +GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +GEORADIUSBYMEMBER Sicily Agrigento 100 km +``` diff --git a/iredis/data/commands/get.md b/iredis/data/commands/get.md new file mode 100644 index 0000000..423aab3 --- /dev/null +++ b/iredis/data/commands/get.md @@ -0,0 +1,15 @@ +Get the value of `key`. If the key does not exist the special value `nil` is +returned. An error is returned if the value stored at `key` is not a string, +because `GET` only handles string values. + +@return + +@bulk-string-reply: the value of `key`, or `nil` when `key` does not exist. + +@examples + +```cli +GET nonexisting +SET mykey "Hello" +GET mykey +``` diff --git a/iredis/data/commands/getbit.md b/iredis/data/commands/getbit.md new file mode 100644 index 0000000..2d05d12 --- /dev/null +++ b/iredis/data/commands/getbit.md @@ -0,0 +1,19 @@ +Returns the bit value at _offset_ in the string value stored at _key_. + +When _offset_ is beyond the string length, the string is assumed to be a +contiguous space with 0 bits. When _key_ does not exist it is assumed to be an +empty string, so _offset_ is always out of range and the value is also assumed +to be a contiguous space with 0 bits. + +@return + +@integer-reply: the bit value stored at _offset_. + +@examples + +```cli +SETBIT mykey 7 1 +GETBIT mykey 0 +GETBIT mykey 7 +GETBIT mykey 100 +``` diff --git a/iredis/data/commands/getrange.md b/iredis/data/commands/getrange.md new file mode 100644 index 0000000..d25ac7a --- /dev/null +++ b/iredis/data/commands/getrange.md @@ -0,0 +1,24 @@ +**Warning**: this command was renamed to `GETRANGE`, it is called `SUBSTR` in +Redis versions `<= 2.0`. + +Returns the substring of the string value stored at `key`, determined by the +offsets `start` and `end` (both are inclusive). Negative offsets can be used in +order to provide an offset starting from the end of the string. So -1 means the +last character, -2 the penultimate and so forth. + +The function handles out of range requests by limiting the resulting range to +the actual length of the string. + +@return + +@bulk-string-reply + +@examples + +```cli +SET mykey "This is a string" +GETRANGE mykey 0 3 +GETRANGE mykey -3 -1 +GETRANGE mykey 0 -1 +GETRANGE mykey 10 100 +``` diff --git a/iredis/data/commands/getset.md b/iredis/data/commands/getset.md new file mode 100644 index 0000000..20b86a2 --- /dev/null +++ b/iredis/data/commands/getset.md @@ -0,0 +1,28 @@ +Atomically sets `key` to `value` and returns the old value stored at `key`. +Returns an error when `key` exists but does not hold a string value. + +## Design pattern + +`GETSET` can be used together with `INCR` for counting with atomic reset. For +example: a process may call `INCR` against the key `mycounter` every time some +event occurs, but from time to time we need to get the value of the counter and +reset it to zero atomically. This can be done using `GETSET mycounter "0"`: + +```cli +INCR mycounter +GETSET mycounter "0" +GET mycounter +``` + +@return + +@bulk-string-reply: the old value stored at `key`, or `nil` when `key` did not +exist. + +@examples + +```cli +SET mykey "Hello" +GETSET mykey "World" +GET mykey +``` diff --git a/iredis/data/commands/hdel.md b/iredis/data/commands/hdel.md new file mode 100644 index 0000000..ce1d184 --- /dev/null +++ b/iredis/data/commands/hdel.md @@ -0,0 +1,24 @@ +Removes the specified fields from the hash stored at `key`. Specified fields +that do not exist within this hash are ignored. If `key` does not exist, it is +treated as an empty hash and this command returns `0`. + +@return + +@integer-reply: the number of fields that were removed from the hash, not +including specified but non existing fields. + +@history + +- `>= 2.4`: Accepts multiple `field` arguments. Redis versions older than 2.4 + can only remove a field per call. + + To remove multiple fields from a hash in an atomic fashion in earlier + versions, use a `MULTI` / `EXEC` block. + +@examples + +```cli +HSET myhash field1 "foo" +HDEL myhash field1 +HDEL myhash field2 +``` diff --git a/iredis/data/commands/hello.md b/iredis/data/commands/hello.md new file mode 100644 index 0000000..b7a9dc4 --- /dev/null +++ b/iredis/data/commands/hello.md @@ -0,0 +1,46 @@ +Switch the connection to a different protocol. Redis version 6 or greater are +able to support two protocols, the old protocol, RESP2, and a new one introduced +with Redis 6, RESP3. RESP3 has certain advantages since when the connection is +in this mode, Redis is able to reply with more semantical replies: for instance +`HGETALL` will return a _map type_, so a client library implementation no longer +requires to know in advance to translate the array into a hash before returning +it to the caller. For a full coverage of RESP3 please +[check this repository](https://github.com/antirez/resp3). + +Redis 6 connections starts in RESP2 mode, so clients implementing RESP2 do not +need to change (nor there are short term plans to drop support for RESP2). +Clients that want to handshake the RESP3 mode need to call the `HELLO` command, +using "3" as first argument. + + > HELLO 3 + 1# "server" => "redis" + 2# "version" => "6.0.0" + 3# "proto" => (integer) 3 + 4# "id" => (integer) 10 + 5# "mode" => "standalone" + 6# "role" => "master" + 7# "modules" => (empty array) + +The `HELLO` command has a useful reply that will state a number of facts about +the server: the exact version, the set of modules loaded, the client ID, the +replication role and so forth. Because of that, and given that the `HELLO` +command also works with "2" as argument, both in order to downgrade the protocol +back to version 2, or just to get the reply from the server without switching +the protocol, client library authors may consider using this command instead of +the canonical `PING` when setting up the connection. + +This command accepts two non mandatory options: + +- `AUTH <username> <password>`: directly authenticate the connection other than + switching to the specified protocol. In this way there is no need to call + `AUTH` before `HELLO` when setting up new connections. Note that the username + can be set to "default" in order to authenticate against a server that does + not use ACLs, but the simpler `requirepass` mechanism of Redis before + version 6. +- `SETNAME <clientname>`: this is equivalent to also call `CLIENT SETNAME`. + +@return + +@array-reply: a list of server properties. The reply is a map instead of an +array when RESP3 is selected. The command returns an error if the protocol +requested does not exist. diff --git a/iredis/data/commands/hexists.md b/iredis/data/commands/hexists.md new file mode 100644 index 0000000..4581b63 --- /dev/null +++ b/iredis/data/commands/hexists.md @@ -0,0 +1,16 @@ +Returns if `field` is an existing field in the hash stored at `key`. + +@return + +@integer-reply, specifically: + +- `1` if the hash contains `field`. +- `0` if the hash does not contain `field`, or `key` does not exist. + +@examples + +```cli +HSET myhash field1 "foo" +HEXISTS myhash field1 +HEXISTS myhash field2 +``` diff --git a/iredis/data/commands/hget.md b/iredis/data/commands/hget.md new file mode 100644 index 0000000..12556a1 --- /dev/null +++ b/iredis/data/commands/hget.md @@ -0,0 +1,14 @@ +Returns the value associated with `field` in the hash stored at `key`. + +@return + +@bulk-string-reply: the value associated with `field`, or `nil` when `field` is +not present in the hash or `key` does not exist. + +@examples + +```cli +HSET myhash field1 "foo" +HGET myhash field1 +HGET myhash field2 +``` diff --git a/iredis/data/commands/hgetall.md b/iredis/data/commands/hgetall.md new file mode 100644 index 0000000..42e95de --- /dev/null +++ b/iredis/data/commands/hgetall.md @@ -0,0 +1,16 @@ +Returns all fields and values of the hash stored at `key`. In the returned +value, every field name is followed by its value, so the length of the reply is +twice the size of the hash. + +@return + +@array-reply: list of fields and their values stored in the hash, or an empty +list when `key` does not exist. + +@examples + +```cli +HSET myhash field1 "Hello" +HSET myhash field2 "World" +HGETALL myhash +``` diff --git a/iredis/data/commands/hincrby.md b/iredis/data/commands/hincrby.md new file mode 100644 index 0000000..71f53cb --- /dev/null +++ b/iredis/data/commands/hincrby.md @@ -0,0 +1,22 @@ +Increments the number stored at `field` in the hash stored at `key` by +`increment`. If `key` does not exist, a new key holding a hash is created. If +`field` does not exist the value is set to `0` before the operation is +performed. + +The range of values supported by `HINCRBY` is limited to 64 bit signed integers. + +@return + +@integer-reply: the value at `field` after the increment operation. + +@examples + +Since the `increment` argument is signed, both increment and decrement +operations can be performed: + +```cli +HSET myhash field 5 +HINCRBY myhash field 1 +HINCRBY myhash field -1 +HINCRBY myhash field -10 +``` diff --git a/iredis/data/commands/hincrbyfloat.md b/iredis/data/commands/hincrbyfloat.md new file mode 100644 index 0000000..fe58beb --- /dev/null +++ b/iredis/data/commands/hincrbyfloat.md @@ -0,0 +1,33 @@ +Increment the specified `field` of a hash stored at `key`, and representing a +floating point 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. An error is returned if one of the following conditions occur: + +- The field contains a value of the wrong type (not a string). +- The current field content or the specified increment are not parsable as a + double precision floating point number. + +The exact behavior of this command is identical to the one of the `INCRBYFLOAT` +command, please refer to the documentation of `INCRBYFLOAT` for further +information. + +@return + +@bulk-string-reply: the value of `field` after the increment. + +@examples + +```cli +HSET mykey field 10.50 +HINCRBYFLOAT mykey field 0.1 +HINCRBYFLOAT mykey field -5 +HSET mykey field 5.0e3 +HINCRBYFLOAT mykey field 2.0e2 +``` + +## Implementation details + +The command is always propagated in the replication link and the Append Only +File as a `HSET` operation, so that differences in the underlying floating point +math implementation will not be sources of inconsistency. diff --git a/iredis/data/commands/hkeys.md b/iredis/data/commands/hkeys.md new file mode 100644 index 0000000..42fd82c --- /dev/null +++ b/iredis/data/commands/hkeys.md @@ -0,0 +1,14 @@ +Returns all field names in the hash stored at `key`. + +@return + +@array-reply: list of fields in the hash, or an empty list when `key` does not +exist. + +@examples + +```cli +HSET myhash field1 "Hello" +HSET myhash field2 "World" +HKEYS myhash +``` diff --git a/iredis/data/commands/hlen.md b/iredis/data/commands/hlen.md new file mode 100644 index 0000000..2c18193 --- /dev/null +++ b/iredis/data/commands/hlen.md @@ -0,0 +1,13 @@ +Returns the number of fields contained in the hash stored at `key`. + +@return + +@integer-reply: number of fields in the hash, or `0` when `key` does not exist. + +@examples + +```cli +HSET myhash field1 "Hello" +HSET myhash field2 "World" +HLEN myhash +``` diff --git a/iredis/data/commands/hmget.md b/iredis/data/commands/hmget.md new file mode 100644 index 0000000..14b8733 --- /dev/null +++ b/iredis/data/commands/hmget.md @@ -0,0 +1,17 @@ +Returns the values associated with the specified `fields` in the hash stored at +`key`. + +For every `field` that does not exist in the hash, a `nil` value is returned. +Because non-existing keys are treated as empty hashes, running `HMGET` against a +non-existing `key` will return a list of `nil` values. + +@return + +@array-reply: list of values associated with the given fields, in the same order +as they are requested. + +```cli +HSET myhash field1 "Hello" +HSET myhash field2 "World" +HMGET myhash field1 field2 nofield +``` diff --git a/iredis/data/commands/hmset.md b/iredis/data/commands/hmset.md new file mode 100644 index 0000000..f66a364 --- /dev/null +++ b/iredis/data/commands/hmset.md @@ -0,0 +1,18 @@ +Sets the specified fields to their respective values in the hash stored at +`key`. This command overwrites any specified fields already existing in the +hash. If `key` does not exist, a new key holding a hash is created. + +As per Redis 4.0.0, HMSET is considered deprecated. Please use `HSET` in new +code. + +@return + +@simple-string-reply + +@examples + +```cli +HMSET myhash field1 "Hello" field2 "World" +HGET myhash field1 +HGET myhash field2 +``` diff --git a/iredis/data/commands/hscan.md b/iredis/data/commands/hscan.md new file mode 100644 index 0000000..9ab2616 --- /dev/null +++ b/iredis/data/commands/hscan.md @@ -0,0 +1 @@ +See `SCAN` for `HSCAN` documentation. diff --git a/iredis/data/commands/hset.md b/iredis/data/commands/hset.md new file mode 100644 index 0000000..a975947 --- /dev/null +++ b/iredis/data/commands/hset.md @@ -0,0 +1,17 @@ +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. + +As of Redis 4.0.0, HSET is variadic and allows for multiple `field`/`value` +pairs. + +@return + +@integer-reply: The number of fields that were added. + +@examples + +```cli +HSET myhash field1 "Hello" +HGET myhash field1 +``` diff --git a/iredis/data/commands/hsetnx.md b/iredis/data/commands/hsetnx.md new file mode 100644 index 0000000..1926178 --- /dev/null +++ b/iredis/data/commands/hsetnx.md @@ -0,0 +1,18 @@ +Sets `field` in the hash stored at `key` to `value`, only if `field` does not +yet exist. If `key` does not exist, a new key holding a hash is created. If +`field` already exists, this operation has no effect. + +@return + +@integer-reply, specifically: + +- `1` if `field` is a new field in the hash and `value` was set. +- `0` if `field` already exists in the hash and no operation was performed. + +@examples + +```cli +HSETNX myhash field "Hello" +HSETNX myhash field "World" +HGET myhash field +``` diff --git a/iredis/data/commands/hstrlen.md b/iredis/data/commands/hstrlen.md new file mode 100644 index 0000000..9a96337 --- /dev/null +++ b/iredis/data/commands/hstrlen.md @@ -0,0 +1,16 @@ +Returns the string length of the value associated with `field` in the hash +stored at `key`. If the `key` or the `field` do not exist, 0 is returned. + +@return + +@integer-reply: the string length of the value associated with `field`, or zero +when `field` is not present in the hash or `key` does not exist at all. + +@examples + +```cli +HMSET myhash f1 HelloWorld f2 99 f3 -256 +HSTRLEN myhash f1 +HSTRLEN myhash f2 +HSTRLEN myhash f3 +``` diff --git a/iredis/data/commands/hvals.md b/iredis/data/commands/hvals.md new file mode 100644 index 0000000..1dbe950 --- /dev/null +++ b/iredis/data/commands/hvals.md @@ -0,0 +1,14 @@ +Returns all values in the hash stored at `key`. + +@return + +@array-reply: list of values in the hash, or an empty list when `key` does not +exist. + +@examples + +```cli +HSET myhash field1 "Hello" +HSET myhash field2 "World" +HVALS myhash +``` diff --git a/iredis/data/commands/incr.md b/iredis/data/commands/incr.md new file mode 100644 index 0000000..8d8b012 --- /dev/null +++ b/iredis/data/commands/incr.md @@ -0,0 +1,156 @@ +Increments the number stored at `key` by one. If the key does not exist, it is +set to `0` before performing the operation. An error is returned if the key +contains a value of the wrong type or contains a string that can not be +represented as integer. This operation is limited to 64 bit signed integers. + +**Note**: this is a string operation because Redis does not have a dedicated +integer type. The string stored at the key is interpreted as a base-10 **64 bit +signed integer** to execute the operation. + +Redis stores integers in their integer representation, so for string values that +actually hold an integer, there is no overhead for storing the string +representation of the integer. + +@return + +@integer-reply: the value of `key` after the increment + +@examples + +```cli +SET mykey "10" +INCR mykey +GET mykey +``` + +## Pattern: Counter + +The counter pattern is the most obvious thing you can do with Redis atomic +increment operations. The idea is simply send an `INCR` command to Redis every +time an operation occurs. For instance in a web application we may want to know +how many page views this user did every day of the year. + +To do so the web application may simply increment a key every time the user +performs a page view, creating the key name concatenating the User ID and a +string representing the current date. + +This simple pattern can be extended in many ways: + +- It is possible to use `INCR` and `EXPIRE` together at every page view to have + a counter counting only the latest N page views separated by less than the + specified amount of seconds. +- A client may use GETSET in order to atomically get the current counter value + and reset it to zero. +- Using other atomic increment/decrement commands like `DECR` or `INCRBY` it is + possible to handle values that may get bigger or smaller depending on the + operations performed by the user. Imagine for instance the score of different + users in an online game. + +## Pattern: Rate limiter + +The rate limiter pattern is a special counter that is used to limit the rate at +which an operation can be performed. The classical materialization of this +pattern involves limiting the number of requests that can be performed against a +public API. + +We provide two implementations of this pattern using `INCR`, where we assume +that the problem to solve is limiting the number of API calls to a maximum of +_ten requests per second per IP address_. + +## Pattern: Rate limiter 1 + +The more simple and direct implementation of this pattern is the following: + +``` +FUNCTION LIMIT_API_CALL(ip) +ts = CURRENT_UNIX_TIME() +keyname = ip+":"+ts +current = GET(keyname) +IF current != NULL AND current > 10 THEN + ERROR "too many requests per second" +ELSE + MULTI + INCR(keyname,1) + EXPIRE(keyname,10) + EXEC + PERFORM_API_CALL() +END +``` + +Basically we have a counter for every IP, for every different second. But this +counters are always incremented setting an expire of 10 seconds so that they'll +be removed by Redis automatically when the current second is a different one. + +Note the used of `MULTI` and `EXEC` in order to make sure that we'll both +increment and set the expire at every API call. + +## Pattern: Rate limiter 2 + +An alternative implementation uses a single counter, but is a bit more complex +to get it right without race conditions. We'll examine different variants. + +``` +FUNCTION LIMIT_API_CALL(ip): +current = GET(ip) +IF current != NULL AND current > 10 THEN + ERROR "too many requests per second" +ELSE + value = INCR(ip) + IF value == 1 THEN + EXPIRE(ip,1) + END + PERFORM_API_CALL() +END +``` + +The counter is created in a way that it only will survive one second, starting +from the first request performed in the current second. If there are more than +10 requests in the same second the counter will reach a value greater than 10, +otherwise it will expire and start again from 0. + +**In the above code there is a race condition**. If for some reason the client +performs the `INCR` command but does not perform the `EXPIRE` the key will be +leaked until we'll see the same IP address again. + +This can be fixed easily turning the `INCR` with optional `EXPIRE` into a Lua +script that is send using the `EVAL` command (only available since Redis version +2.6). + +``` +local current +current = redis.call("incr",KEYS[1]) +if tonumber(current) == 1 then + redis.call("expire",KEYS[1],1) +end +``` + +There is a different way to fix this issue without using scripting, but using +Redis lists instead of counters. The implementation is more complex and uses +more advanced features but has the advantage of remembering the IP addresses of +the clients currently performing an API call, that may be useful or not +depending on the application. + +``` +FUNCTION LIMIT_API_CALL(ip) +current = LLEN(ip) +IF current > 10 THEN + ERROR "too many requests per second" +ELSE + IF EXISTS(ip) == FALSE + MULTI + RPUSH(ip,ip) + EXPIRE(ip,1) + EXEC + ELSE + RPUSHX(ip,ip) + END + PERFORM_API_CALL() +END +``` + +The `RPUSHX` command only pushes the element if the key already exists. + +Note that we have a race here, but it is not a problem: `EXISTS` may return +false but the key may be created by another client before we create it inside +the `MULTI` / `EXEC` block. However this race will just miss an API call under +rare conditions, so the rate limiting will still work correctly. diff --git a/iredis/data/commands/incrby.md b/iredis/data/commands/incrby.md new file mode 100644 index 0000000..103dbbf --- /dev/null +++ b/iredis/data/commands/incrby.md @@ -0,0 +1,17 @@ +Increments the number stored at `key` by `increment`. If the key does not exist, +it is set to `0` before performing the operation. An error is returned if the +key contains a value of the wrong type or contains a string that can not be +represented as integer. This operation is limited to 64 bit signed integers. + +See `INCR` for extra information on increment/decrement operations. + +@return + +@integer-reply: the value of `key` after the increment + +@examples + +```cli +SET mykey "10" +INCRBY mykey 5 +``` diff --git a/iredis/data/commands/incrbyfloat.md b/iredis/data/commands/incrbyfloat.md new file mode 100644 index 0000000..4998f5f --- /dev/null +++ b/iredis/data/commands/incrbyfloat.md @@ -0,0 +1,42 @@ +Increment the string representing a floating point number stored at `key` by the +specified `increment`. By using a negative `increment` value, the result is that +the value stored at the key is decremented (by the obvious properties of +addition). If the key does not exist, it is set to `0` before performing the +operation. An error is returned if one of the following conditions occur: + +- The key contains a value of the wrong type (not a string). +- The current key content or the specified increment are not parsable as a + double precision floating point number. + +If the command is successful the new incremented value is stored as the new +value of the key (replacing the old one), and returned to the caller as a +string. + +Both the value already contained in the string key and the increment argument +can be optionally provided in exponential notation, however the value computed +after the increment is stored consistently in the same format, that is, an +integer number followed (if needed) by a dot, and a variable number of digits +representing the decimal part of the number. Trailing zeroes are always removed. + +The precision of the output is fixed at 17 digits after the decimal point +regardless of the actual internal precision of the computation. + +@return + +@bulk-string-reply: the value of `key` after the increment. + +@examples + +```cli +SET mykey 10.50 +INCRBYFLOAT mykey 0.1 +INCRBYFLOAT mykey -5 +SET mykey 5.0e3 +INCRBYFLOAT mykey 2.0e2 +``` + +## Implementation details + +The command is always propagated in the replication link and the Append Only +File as a `SET` operation, so that differences in the underlying floating point +math implementation will not be sources of inconsistency. diff --git a/iredis/data/commands/info.md b/iredis/data/commands/info.md new file mode 100644 index 0000000..8dcebc1 --- /dev/null +++ b/iredis/data/commands/info.md @@ -0,0 +1,335 @@ +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. + +The optional parameter can be used to select a specific section of information: + +- `server`: General information about the Redis server +- `clients`: Client connections section +- `memory`: Memory consumption related information +- `persistence`: RDB and AOF related information +- `stats`: General statistics +- `replication`: Master/replica replication information +- `cpu`: CPU consumption statistics +- `commandstats`: Redis command statistics +- `cluster`: Redis Cluster section +- `modules`: Modules section +- `keyspace`: Database related statistics +- `modules`: Module related sections + +It can also take the following values: + +- `all`: Return all sections (excluding module generated ones) +- `default`: Return only the default set of sections +- `everything`: Includes `all` and `modules` + +When no parameter is provided, the `default` option is assumed. + +@return + +@bulk-string-reply: as a collection of text lines. + +Lines can contain a section name (starting with a # character) or a property. +All the properties are in the form of `field:value` terminated by `\r\n`. + +```cli +INFO +``` + +## Notes + +Please note depending on the version of Redis some of the fields have been added +or removed. A robust client application should therefore parse the result of +this command by skipping unknown properties, and gracefully handle missing +fields. + +Here is the description of fields for Redis >= 2.4. + +Here is the meaning of all fields in the **server** section: + +- `redis_version`: Version of the Redis server +- `redis_git_sha1`: Git SHA1 +- `redis_git_dirty`: Git dirty flag +- `redis_build_id`: The build id +- `redis_mode`: The server's mode ("standalone", "sentinel" or "cluster") +- `os`: Operating system hosting the Redis server +- `arch_bits`: Architecture (32 or 64 bits) +- `multiplexing_api`: Event loop mechanism used by Redis +- `atomicvar_api`: Atomicvar API used by Redis +- `gcc_version`: Version of the GCC compiler used to compile the Redis server +- `process_id`: PID of the server process +- `run_id`: Random value identifying the Redis server (to be used by Sentinel + and Cluster) +- `tcp_port`: TCP/IP listen port +- `uptime_in_seconds`: Number of seconds since Redis server start +- `uptime_in_days`: Same value expressed in days +- `hz`: The server's current frequency setting +- `configured_hz`: The server's configured frequency setting +- `lru_clock`: Clock incrementing every minute, for LRU management +- `executable`: The path to the server's executable +- `config_file`: The path to the config file + +Here is the meaning of all fields in the **clients** section: + +- `connected_clients`: Number of client connections (excluding connections from + replicas) +- `client_longest_output_list`: Longest output list among current client + connections +- `client_biggest_input_buf`: Biggest input buffer among current client + connections +- `blocked_clients`: Number of clients pending on a blocking call (`BLPOP`, + `BRPOP`, `BRPOPLPUSH`, `BZPOPMIN`, `BZPOPMAX`) +- `tracking_clients`: Number of clients being tracked (`CLIENT TRACKING`) +- `clients_in_timeout_table`: Number of clients in the clients timeout table + +Here is the meaning of all fields in the **memory** section: + +- `used_memory`: Total number of bytes allocated by Redis using its allocator + (either standard **libc**, **jemalloc**, or an alternative allocator such as + [**tcmalloc**][hcgcpgp]) +- `used_memory_human`: Human readable representation of previous value +- `used_memory_rss`: Number of bytes that Redis allocated as seen by the + operating system (a.k.a resident set size). This is the number reported by + tools such as `top(1)` and `ps(1)` +- `used_memory_rss_human`: Human readable representation of previous value +- `used_memory_peak`: Peak memory consumed by Redis (in bytes) +- `used_memory_peak_human`: Human readable representation of previous value +- `used_memory_peak_perc`: The percentage of `used_memory_peak` out of + `used_memory` +- `used_memory_overhead`: The sum in bytes of all overheads that the server + allocated for managing its internal data structures +- `used_memory_startup`: Initial amount of memory consumed by Redis at startup + in bytes +- `used_memory_dataset`: The size in bytes of the dataset + (`used_memory_overhead` subtracted from `used_memory`) +- `used_memory_dataset_perc`: The percentage of `used_memory_dataset` out of the + net memory usage (`used_memory` minus `used_memory_startup`) +- `total_system_memory`: The total amount of memory that the Redis host has +- `total_system_memory_human`: Human readable representation of previous value +- `used_memory_lua`: Number of bytes used by the Lua engine +- `used_memory_lua_human`: Human readable representation of previous value +- `used_memory_scripts`: Number of bytes used by cached Lua scripts +- `used_memory_scripts_human`: Human readable representation of previous value +- `maxmemory`: The value of the `maxmemory` configuration directive +- `maxmemory_human`: Human readable representation of previous value +- `maxmemory_policy`: The value of the `maxmemory-policy` configuration + directive +- `mem_fragmentation_ratio`: Ratio between `used_memory_rss` and `used_memory` +- `mem_allocator`: Memory allocator, chosen at compile time +- `active_defrag_running`: Flag indicating if active defragmentation is active +- `lazyfree_pending_objects`: The number of objects waiting to be freed (as a + result of calling `UNLINK`, or `FLUSHDB` and `FLUSHALL` with the **ASYNC** + option) + +Ideally, the `used_memory_rss` value should be only slightly higher than +`used_memory`. When rss >> used, a large difference means there is memory +fragmentation (internal or external), which can be evaluated by checking +`mem_fragmentation_ratio`. When used >> rss, it means part of Redis memory has +been swapped off by the operating system: expect some significant latencies. + +Because Redis does not have control over how its allocations are mapped to +memory pages, high `used_memory_rss` is often the result of a spike in memory +usage. + +When Redis frees memory, the memory is given back to the allocator, and the +allocator may or may not give the memory back to the system. There may be a +discrepancy between the `used_memory` value and memory consumption as reported +by the operating system. It may be due to the fact memory has been used and +released by Redis, but not given back to the system. The `used_memory_peak` +value is generally useful to check this point. + +Additional introspective information about the server's memory can be obtained +by referring to the `MEMORY STATS` command and the `MEMORY DOCTOR`. + +Here is the meaning of all fields in the **persistence** section: + +- `loading`: Flag indicating if the load of a dump file is on-going +- `rdb_changes_since_last_save`: Number of changes since the last dump +- `rdb_bgsave_in_progress`: Flag indicating a RDB save is on-going +- `rdb_last_save_time`: Epoch-based timestamp of last successful RDB save +- `rdb_last_bgsave_status`: Status of the last RDB save operation +- `rdb_last_bgsave_time_sec`: Duration of the last RDB save operation in seconds +- `rdb_current_bgsave_time_sec`: Duration of the on-going RDB save operation if + any +- `rdb_last_cow_size`: The size in bytes of copy-on-write allocations during the + last RDB save operation +- `aof_enabled`: Flag indicating AOF logging is activated +- `aof_rewrite_in_progress`: Flag indicating a AOF rewrite operation is on-going +- `aof_rewrite_scheduled`: Flag indicating an AOF rewrite operation will be + scheduled once the on-going RDB save is complete. +- `aof_last_rewrite_time_sec`: Duration of the last AOF rewrite operation in + seconds +- `aof_current_rewrite_time_sec`: Duration of the on-going AOF rewrite operation + if any +- `aof_last_bgrewrite_status`: Status of the last AOF rewrite operation +- `aof_last_write_status`: Status of the last write operation to the AOF +- `aof_last_cow_size`: The size in bytes of copy-on-write allocations during the + last AOF rewrite operation +- `module_fork_in_progress`: Flag indicating a module fork is on-going +- `module_fork_last_cow_size`: The size in bytes of copy-on-write allocations + during the last module fork operation + +`rdb_changes_since_last_save` refers to the number of operations that produced +some kind of changes in the dataset since the last time either `SAVE` or +`BGSAVE` was called. + +If AOF is activated, these additional fields will be added: + +- `aof_current_size`: AOF current file size +- `aof_base_size`: AOF file size on latest startup or rewrite +- `aof_pending_rewrite`: Flag indicating an AOF rewrite operation will be + scheduled once the on-going RDB save is complete. +- `aof_buffer_length`: Size of the AOF buffer +- `aof_rewrite_buffer_length`: Size of the AOF rewrite buffer +- `aof_pending_bio_fsync`: Number of fsync pending jobs in background I/O queue +- `aof_delayed_fsync`: Delayed fsync counter + +If a load operation is on-going, these additional fields will be added: + +- `loading_start_time`: Epoch-based timestamp of the start of the load operation +- `loading_total_bytes`: Total file size +- `loading_loaded_bytes`: Number of bytes already loaded +- `loading_loaded_perc`: Same value expressed as a percentage +- `loading_eta_seconds`: ETA in seconds for the load to be complete + +Here is the meaning of all fields in the **stats** section: + +- `total_connections_received`: Total number of connections accepted by the + server +- `total_commands_processed`: Total number of commands processed by the server +- `instantaneous_ops_per_sec`: Number of commands processed per second +- `total_net_input_bytes`: The total number of bytes read from the network +- `total_net_output_bytes`: The total number of bytes written to the network +- `instantaneous_input_kbps`: The network's read rate per second in KB/sec +- `instantaneous_output_kbps`: The network's write rate per second in KB/sec +- `rejected_connections`: Number of connections rejected because of `maxclients` + limit +- `sync_full`: The number of full resyncs with replicas +- `sync_partial_ok`: The number of accepted partial resync requests +- `sync_partial_err`: The number of denied partial resync requests +- `expired_keys`: Total number of key expiration events +- `expired_stale_perc`: The percentage of keys probably expired +- `expired_time_cap_reached_count`: The count of times that active expiry cycles + have stopped early +- `expire_cycle_cpu_milliseconds`: The cumulative amount of time spend on active + expiry cycles +- `evicted_keys`: Number of evicted keys due to `maxmemory` limit +- `keyspace_hits`: Number of successful lookup of keys in the main dictionary +- `keyspace_misses`: Number of failed lookup of keys in the main dictionary +- `pubsub_channels`: Global number of pub/sub channels with client subscriptions +- `pubsub_patterns`: Global number of pub/sub pattern with client subscriptions +- `latest_fork_usec`: Duration of the latest fork operation in microseconds +- `migrate_cached_sockets`: The number of sockets open for `MIGRATE` purposes +- `slave_expires_tracked_keys`: The number of keys tracked for expiry purposes + (applicable only to writable replicas) +- `active_defrag_hits`: Number of value reallocations performed by active the + defragmentation process +- `active_defrag_misses`: Number of aborted value reallocations started by the + active defragmentation process +- `active_defrag_key_hits`: Number of keys that were actively defragmented +- `active_defrag_key_misses`: Number of keys that were skipped by the active + defragmentation process +- `tracking_total_keys`: Number of keys being tracked by the server +- `tracking_total_items`: Number of items, that is the sum of clients number for + each key, that are being tracked +- `tracking_total_prefixes`: Number of tracked prefixes in server's prefix table + (only applicable for broadcast mode) +- `unexpected_error_replies`: Number of unexpected error replies, that are types + of errors from an AOF load or replication + +Here is the meaning of all fields in the **replication** section: + +- `role`: Value is "master" if the instance is replica of no one, or "slave" if + the instance is a replica of some master instance. Note that a replica can be + master of another replica (chained replication). +- `master_replid`: The replication ID of the Redis server. +- `master_replid2`: The secondary replication ID, used for PSYNC after a + failover. +- `master_repl_offset`: The server's current replication offset +- `second_repl_offset`: The offset up to which replication IDs are accepted +- `repl_backlog_active`: Flag indicating replication backlog is active +- `repl_backlog_size`: Total size in bytes of the replication backlog buffer +- `repl_backlog_first_byte_offset`: The master offset of the replication backlog + buffer +- `repl_backlog_histlen`: Size in bytes of the data in the replication backlog + buffer + +If the instance is a replica, these additional fields are provided: + +- `master_host`: Host or IP address of the master +- `master_port`: Master listening TCP port +- `master_link_status`: Status of the link (up/down) +- `master_last_io_seconds_ago`: Number of seconds since the last interaction + with master +- `master_sync_in_progress`: Indicate the master is syncing to the replica +- `slave_repl_offset`: The replication offset of the replica instance +- `slave_priority`: The priority of the instance as a candidate for failover +- `slave_read_only`: Flag indicating if the replica is read-only + +If a SYNC operation is on-going, these additional fields are provided: + +- `master_sync_left_bytes`: Number of bytes left before syncing is complete +- `master_sync_last_io_seconds_ago`: Number of seconds since last transfer I/O + during a SYNC operation + +If the link between master and replica is down, an additional field is provided: + +- `master_link_down_since_seconds`: Number of seconds since the link is down + +The following field is always provided: + +- `connected_slaves`: Number of connected replicas + +If the server is configured with the `min-slaves-to-write` (or starting with +Redis 5 with the `min-replicas-to-write`) directive, an additional field is +provided: + +- `min_slaves_good_slaves`: Number of replicas currently considered good + +For each replica, the following line is added: + +- `slaveXXX`: id, IP address, port, state, offset, lag + +Here is the meaning of all fields in the **cpu** section: + +- `used_cpu_sys`: System CPU consumed by the Redis server +- `used_cpu_user`:User CPU consumed by the Redis server +- `used_cpu_sys_children`: System CPU consumed by the background processes +- `used_cpu_user_children`: User CPU consumed by the background processes + +The **commandstats** section provides statistics based on the command type, +including the number of calls, the total CPU time consumed by these commands, +and the average CPU consumed per command execution. + +For each command type, the following line is added: + +- `cmdstat_XXX`: `calls=XXX,usec=XXX,usec_per_call=XXX` + +The **cluster** section currently only contains a unique field: + +- `cluster_enabled`: Indicate Redis cluster is enabled + +The **modules** section contains additional information about loaded modules if +the modules provide it. The field part of properties lines in this section is +always prefixed with the module's name. + +The **keyspace** section provides statistics on the main dictionary of each +database. The statistics are the number of keys, and the number of keys with an +expiration. + +For each database, the following line is added: + +- `dbXXX`: `keys=XXX,expires=XXX` + +[hcgcpgp]: http://code.google.com/p/google-perftools/ + +**A note about the word slave used in this man page**: Starting with Redis 5, if +not for backward compatibility, the Redis project no longer uses the word slave. +Unfortunately in this command the word slave is part of the protocol, so we'll +be able to remove such occurrences only when this API will be naturally +deprecated. + +**Modules generated sections**: Starting with Redis 6, modules can inject their +info into the `INFO` command, these are excluded by default even when the `all` +argument is provided (it will include a list of loaded modules but not their +generated info fields). To get these you must use either the `modules` argument +or `everything`., diff --git a/iredis/data/commands/keys.md b/iredis/data/commands/keys.md new file mode 100644 index 0000000..8991beb --- /dev/null +++ b/iredis/data/commands/keys.md @@ -0,0 +1,37 @@ +Returns all keys matching `pattern`. + +While the time complexity for this operation is O(N), the constant times are +fairly low. For example, Redis running on an entry level laptop can scan a 1 +million key database in 40 milliseconds. + +**Warning**: consider `KEYS` as a command that should only be used in production +environments with extreme care. It may ruin performance when it is executed +against large databases. This command is intended for debugging and special +operations, such as changing your keyspace layout. Don't use `KEYS` in your +regular application code. If you're looking for a way to find keys in a subset +of your keyspace, consider using `SCAN` or [sets][tdts]. + +[tdts]: /topics/data-types#sets + +Supported glob-style patterns: + +- `h?llo` matches `hello`, `hallo` and `hxllo` +- `h*llo` matches `hllo` and `heeeello` +- `h[ae]llo` matches `hello` and `hallo,` but not `hillo` +- `h[^e]llo` matches `hallo`, `hbllo`, ... but not `hello` +- `h[a-b]llo` matches `hallo` and `hbllo` + +Use `\` to escape special characters if you want to match them verbatim. + +@return + +@array-reply: list of keys matching `pattern`. + +@examples + +```cli +MSET firstname Jack lastname Stuntman age 35 +KEYS *name* +KEYS a?? +KEYS * +``` diff --git a/iredis/data/commands/lastsave.md b/iredis/data/commands/lastsave.md new file mode 100644 index 0000000..8a4dea1 --- /dev/null +++ b/iredis/data/commands/lastsave.md @@ -0,0 +1,8 @@ +Return the UNIX TIME of the last DB save executed with success. A client may +check if a `BGSAVE` command succeeded reading the `LASTSAVE` value, then issuing +a `BGSAVE` command and checking at regular intervals every N seconds if +`LASTSAVE` changed. + +@return + +@integer-reply: an UNIX time stamp. diff --git a/iredis/data/commands/latency-doctor.md b/iredis/data/commands/latency-doctor.md new file mode 100644 index 0000000..e089abb --- /dev/null +++ b/iredis/data/commands/latency-doctor.md @@ -0,0 +1,47 @@ +The `LATENCY DOCTOR` command reports about different latency-related issues and +advises about possible remedies. + +This command is the most powerful analysis tool in the latency monitoring +framework, and is able to provide additional statistical data like the average +period between latency spikes, the median deviation, and a human-readable +analysis of the event. For certain events, like `fork`, additional information +is provided, like the rate at which the system forks processes. + +This is the output you should post in the Redis mailing list if you are looking +for help about Latency related issues. + +@example + +``` +127.0.0.1:6379> latency doctor + +Dave, I have observed latency spikes in this Redis instance. +You don't mind talking about it, do you Dave? + +1. command: 5 latency spikes (average 300ms, mean deviation 120ms, + period 73.40 sec). Worst all time event 500ms. + +I have a few advices for you: + +- Your current Slow Log configuration only logs events that are + slower than your configured latency monitor threshold. Please + use 'CONFIG SET slowlog-log-slower-than 1000'. +- Check your Slow Log to understand what are the commands you are + running which are too slow to execute. Please check + http://redis.io/commands/slowlog for more information. +- Deleting, expiring or evicting (because of maxmemory policy) + large objects is a blocking operation. If you have very large + objects that are often deleted, expired, or evicted, try to + fragment those objects into multiple smaller objects. +``` + +**Note:** the doctor has erratic psychological behaviors, so we recommend +interacting with it carefully. + +For more information refer to the [Latency Monitoring Framework page][lm]. + +[lm]: /topics/latency-monitor + +@return + +@bulk-string-reply diff --git a/iredis/data/commands/latency-graph.md b/iredis/data/commands/latency-graph.md new file mode 100644 index 0000000..ee3d93f --- /dev/null +++ b/iredis/data/commands/latency-graph.md @@ -0,0 +1,68 @@ +Produces an ASCII-art style graph for the specified event. + +`LATENCY GRAPH` lets you intuitively understand the latency trend of an `event` +via state-of-the-art visualization. It can be used for quickly grasping the +situation before resorting to means such parsing the raw data from +`LATENCY HISTORY` or external tooling. + +Valid values for `event` are: + +- `active-defrag-cycle` +- `aof-fsync-always` +- `aof-stat` +- `aof-rewrite-diff-write` +- `aof-rename` +- `aof-write` +- `aof-write-active-child` +- `aof-write-alone` +- `aof-write-pending-fsync` +- `command` +- `expire-cycle` +- `eviction-cycle` +- `eviction-del` +- `fast-command` +- `fork` +- `rdb-unlink-temp-file` + +@example + +``` +127.0.0.1:6379> latency reset command +(integer) 0 +127.0.0.1:6379> debug sleep .1 +OK +127.0.0.1:6379> debug sleep .2 +OK +127.0.0.1:6379> debug sleep .3 +OK +127.0.0.1:6379> debug sleep .5 +OK +127.0.0.1:6379> debug sleep .4 +OK +127.0.0.1:6379> latency graph command +command - high 500 ms, low 101 ms (all time high 500 ms) +-------------------------------------------------------------------------------- + #_ + _|| + _||| +_|||| + +11186 +542ss +sss +``` + +The vertical labels under each graph column represent the amount of seconds, +minutes, hours or days ago the event happened. For example "15s" means that the +first graphed event happened 15 seconds ago. + +The graph is normalized in the min-max scale so that the zero (the underscore in +the lower row) is the minimum, and a # in the higher row is the maximum. + +For more information refer to the [Latency Monitoring Framework page][lm]. + +[lm]: /topics/latency-monitor + +@return + +@bulk-string-reply diff --git a/iredis/data/commands/latency-help.md b/iredis/data/commands/latency-help.md new file mode 100644 index 0000000..8077bf0 --- /dev/null +++ b/iredis/data/commands/latency-help.md @@ -0,0 +1,10 @@ +The `LATENCY HELP` command returns a helpful text describing the different +subcommands. + +For more information refer to the [Latency Monitoring Framework page][lm]. + +[lm]: /topics/latency-monitor + +@return + +@array-reply: a list of subcommands and their descriptions diff --git a/iredis/data/commands/latency-history.md b/iredis/data/commands/latency-history.md new file mode 100644 index 0000000..de48ecd --- /dev/null +++ b/iredis/data/commands/latency-history.md @@ -0,0 +1,47 @@ +The `LATENCY HISTORY` command returns the raw data of the `event`'s latency +spikes time series. + +This is useful to an application that wants to fetch raw data in order to +perform monitoring, display graphs, and so forth. + +The command will return up to 160 timestamp-latency pairs for the `event`. + +Valid values for `event` are: + +- `active-defrag-cycle` +- `aof-fsync-always` +- `aof-stat` +- `aof-rewrite-diff-write` +- `aof-rename` +- `aof-write` +- `aof-write-active-child` +- `aof-write-alone` +- `aof-write-pending-fsync` +- `command` +- `expire-cycle` +- `eviction-cycle` +- `eviction-del` +- `fast-command` +- `fork` +- `rdb-unlink-temp-file` + +@example + +``` +127.0.0.1:6379> latency history command +1) 1) (integer) 1405067822 + 2) (integer) 251 +2) 1) (integer) 1405067941 + 2) (integer) 1001 +``` + +For more information refer to the [Latency Monitoring Framework page][lm]. + +[lm]: /topics/latency-monitor + +@return + +@array-reply: specifically: + +The command returns an array where each element is a two elements array +representing the timestamp and the latency of the event. diff --git a/iredis/data/commands/latency-latest.md b/iredis/data/commands/latency-latest.md new file mode 100644 index 0000000..883f0a5 --- /dev/null +++ b/iredis/data/commands/latency-latest.md @@ -0,0 +1,38 @@ +The `LATENCY LATEST` command reports the latest latency events logged. + +Each reported event has the following fields: + +- Event name. +- Unix timestamp of the latest latency spike for the event. +- Latest event latency in millisecond. +- All-time maximum latency for this event. + +"All-time" means the maximum latency since the Redis instance was started, or +the time that events were reset `LATENCY RESET`. + +@example: + +``` +127.0.0.1:6379> debug sleep 1 +OK +(1.00s) +127.0.0.1:6379> debug sleep .25 +OK +127.0.0.1:6379> latency latest +1) 1) "command" + 2) (integer) 1405067976 + 3) (integer) 251 + 4) (integer) 1001 +``` + +For more information refer to the [Latency Monitoring Framework page][lm]. + +[lm]: /topics/latency-monitor + +@return + +@array-reply: specifically: + +The command returns an array where each element is a four elements array +representing the event's name, timestamp, latest and all-time latency +measurements. diff --git a/iredis/data/commands/latency-reset.md b/iredis/data/commands/latency-reset.md new file mode 100644 index 0000000..cec6f06 --- /dev/null +++ b/iredis/data/commands/latency-reset.md @@ -0,0 +1,36 @@ +The `LATENCY RESET` command resets the latency spikes time series of all, or +only some, events. + +When the command is called without arguments, it resets all the events, +discarding the currently logged latency spike events, and resetting the maximum +event time register. + +It is possible to reset only specific events by providing the `event` names as +arguments. + +Valid values for `event` are: + +- `active-defrag-cycle` +- `aof-fsync-always` +- `aof-stat` +- `aof-rewrite-diff-write` +- `aof-rename` +- `aof-write` +- `aof-write-active-child` +- `aof-write-alone` +- `aof-write-pending-fsync` +- `command` +- `expire-cycle` +- `eviction-cycle` +- `eviction-del` +- `fast-command` +- `fork` +- `rdb-unlink-temp-file` + +For more information refer to the [Latency Monitoring Framework page][lm]. + +[lm]: /topics/latency-monitor + +@reply + +@integer-reply: the number of event time series that were reset. diff --git a/iredis/data/commands/lindex.md b/iredis/data/commands/lindex.md new file mode 100644 index 0000000..c0f6ac3 --- /dev/null +++ b/iredis/data/commands/lindex.md @@ -0,0 +1,22 @@ +Returns the element at index `index` in the list stored at `key`. The index is +zero-based, so `0` means the first element, `1` the second element and so on. +Negative indices can be used to designate elements starting at the tail of the +list. Here, `-1` means the last element, `-2` means the penultimate and so +forth. + +When the value at `key` is not a list, an error is returned. + +@return + +@bulk-string-reply: the requested element, or `nil` when `index` is out of +range. + +@examples + +```cli +LPUSH mylist "World" +LPUSH mylist "Hello" +LINDEX mylist 0 +LINDEX mylist -1 +LINDEX mylist 3 +``` diff --git a/iredis/data/commands/linsert.md b/iredis/data/commands/linsert.md new file mode 100644 index 0000000..6ff8060 --- /dev/null +++ b/iredis/data/commands/linsert.md @@ -0,0 +1,21 @@ +Inserts `element` in the list stored at `key` either before or after the +reference value `pivot`. + +When `key` does not exist, it is considered an empty list and no operation is +performed. + +An error is returned when `key` exists but does not hold a list value. + +@return + +@integer-reply: the length of the list after the insert operation, or `-1` when +the value `pivot` was not found. + +@examples + +```cli +RPUSH mylist "Hello" +RPUSH mylist "World" +LINSERT mylist BEFORE "World" "There" +LRANGE mylist 0 -1 +``` diff --git a/iredis/data/commands/llen.md b/iredis/data/commands/llen.md new file mode 100644 index 0000000..17ac581 --- /dev/null +++ b/iredis/data/commands/llen.md @@ -0,0 +1,15 @@ +Returns the length of the list stored at `key`. If `key` does not exist, it is +interpreted as an empty list and `0` is returned. An error is returned when the +value stored at `key` is not a list. + +@return + +@integer-reply: the length of the list at `key`. + +@examples + +```cli +LPUSH mylist "World" +LPUSH mylist "Hello" +LLEN mylist +``` diff --git a/iredis/data/commands/lolwut.md b/iredis/data/commands/lolwut.md new file mode 100644 index 0000000..c19567c --- /dev/null +++ b/iredis/data/commands/lolwut.md @@ -0,0 +1,36 @@ +The LOLWUT command displays the Redis version: however as a side effect of doing +so, it also creates a piece of generative computer art that is different with +each version of Redis. The command was introduced in Redis 5 and announced with +this [blog post](http://antirez.com/news/123). + +By default the `LOLWUT` command will display the piece corresponding to the +current Redis version, however it is possible to display a specific version +using the following form: + + LOLWUT VERSION 5 ... other optional arguments ... + +Of course the "5" above is an example. Each LOLWUT version takes a different set +of arguments in order to change the output. The user is encouraged to play with +it to discover how the output changes adding more numerical arguments. + +LOLWUT wants to be a reminder that there is more in programming than just +putting some code together in order to create something useful. Every LOLWUT +version should have the following properties: + +1. It should display some computer art. There are no limits as long as the + output works well in a normal terminal display. However the output should not + be limited to graphics (like LOLWUT 5 and 6 actually do), but can be + generative poetry and other non graphical things. +2. LOLWUT output should be completely useless. Displaying some useful Redis + internal metrics does not count as a valid LOLWUT. +3. LOLWUT output should be fast to generate so that the command can be called in + production instances without issues. It should remain fast even when the user + experiments with odd parameters. +4. LOLWUT implementations should be safe and carefully checked for security, and + resist to untrusted inputs if they take arguments. +5. LOLWUT must always display the Redis version at the end. + +@return + +@bulk-string-reply (or verbatim reply when using the RESP3 protocol): the string +containing the generative computer art, and a text with the Redis version. diff --git a/iredis/data/commands/lpop.md b/iredis/data/commands/lpop.md new file mode 100644 index 0000000..6049176 --- /dev/null +++ b/iredis/data/commands/lpop.md @@ -0,0 +1,16 @@ +Removes and returns the first element of the list stored at `key`. + +@return + +@bulk-string-reply: the value of the first element, or `nil` when `key` does not +exist. + +@examples + +```cli +RPUSH mylist "one" +RPUSH mylist "two" +RPUSH mylist "three" +LPOP mylist +LRANGE mylist 0 -1 +``` diff --git a/iredis/data/commands/lpos.md b/iredis/data/commands/lpos.md new file mode 100644 index 0000000..c256e0e --- /dev/null +++ b/iredis/data/commands/lpos.md @@ -0,0 +1,95 @@ +The command returns the index of matching elements inside a Redis list. By +default, when no options are given, it will scan the list from head to tail, +looking for the first match of "element". If the element is found, its index +(the zero-based position in the list) is returned. Otherwise, if no match is +found, NULL is returned. + +``` +> RPUSH mylist a b c 1 2 3 c c +> LPOS mylist c +2 +``` + +The optional arguments and options can modify the command's behavior. The `RANK` +option specifies the "rank" of the first element to return, in case there are +multiple matches. A rank of 1 means to return the first match, 2 to return the +second match, and so forth. + +For instance, in the above example the element "c" is present multiple times, if +I want the index of the second match, I'll write: + +``` +> LPOS mylist c RANK 2 +6 +``` + +That is, the second occurrence of "c" is at position 6. A negative "rank" as the +`RANK` argument tells `LPOS` to invert the search direction, starting from the +tail to the head. + +So, we want to say, give me the first element starting from the tail of the +list: + +``` +> LPOS mylist c RANK -1 +7 +``` + +Note that the indexes are still reported in the "natural" way, that is, +considering the first element starting from the head of the list at index 0, the +next element at index 1, and so forth. This basically means that the returned +indexes are stable whatever the rank is positive or negative. + +Sometimes we want to return not just the Nth matching element, but the position +of all the first N matching elements. This can be achieved using the `COUNT` +option. + +``` +> LPOS mylist c COUNT 2 +[2,6] +``` + +We can combine `COUNT` and `RANK`, so that `COUNT` will try to return up to the +specified number of matches, but starting from the Nth match, as specified by +the `RANK` option. + +``` +> LPOS mylist c RANK -1 COUNT 2 +[7,6] +``` + +When `COUNT` is used, it is possible to specify 0 as the number of matches, as a +way to tell the command we want all the matches found returned as an array of +indexes. This is better than giving a very large `COUNT` option because it is +more general. + +``` +> LPOS mylist COUNT 0 +[2,6,7] +``` + +When `COUNT` is used and no match is found, an empty array is returned. However +when `COUNT` is not used and there are no matches, the command returns NULL. + +Finally, the `MAXLEN` option tells the command to compare the provided element +only with a given maximum number of list items. So for instance specifying +`MAXLEN 1000` will make sure that the command performs only 1000 comparisons, +effectively running the algorithm on a subset of the list (the first part or the +last part depending on the fact we use a positive or negative rank). This is +useful to limit the maximum complexity of the command. It is also useful when we +expect the match to be found very early, but want to be sure that in case this +is not true, the command does not take too much time to run. + +@return + +The command returns the integer representing the matching element, or null if +there is no match. However, if the `COUNT` option is given the command returns +an array (empty if there are no matches). + +@examples + +```cli +RPUSH mylist a b c d 1 2 3 4 3 3 3 +LPOS mylist 3 +LPOS mylist 3 COUNT 0 RANK 2 +``` diff --git a/iredis/data/commands/lpush.md b/iredis/data/commands/lpush.md new file mode 100644 index 0000000..2140857 --- /dev/null +++ b/iredis/data/commands/lpush.md @@ -0,0 +1,27 @@ +Insert all the specified values at the head of the list stored at `key`. If +`key` does not exist, it is created as empty list before performing the push +operations. When `key` holds a value that is not a list, an error is returned. + +It is possible to push multiple elements using a single command call just +specifying multiple arguments at the end of the command. Elements are inserted +one after the other to the head of the list, from the leftmost element to the +rightmost element. So for instance the command `LPUSH mylist a b c` will result +into a list containing `c` as first element, `b` as second element and `a` as +third element. + +@return + +@integer-reply: the length of the list after the push operations. + +@history + +- `>= 2.4`: Accepts multiple `element` arguments. In Redis versions older than + 2.4 it was possible to push a single value per command. + +@examples + +```cli +LPUSH mylist "world" +LPUSH mylist "hello" +LRANGE mylist 0 -1 +``` diff --git a/iredis/data/commands/lpushx.md b/iredis/data/commands/lpushx.md new file mode 100644 index 0000000..4cc505f --- /dev/null +++ b/iredis/data/commands/lpushx.md @@ -0,0 +1,22 @@ +Inserts specified values at the head of the list stored at `key`, only if `key` +already exists and holds a list. In contrary to `LPUSH`, no operation will be +performed when `key` does not yet exist. + +@return + +@integer-reply: the length of the list after the push operation. + +@history + +- `>= 4.0`: Accepts multiple `element` arguments. In Redis versions older than + 4.0 it was possible to push a single value per command. + +@examples + +```cli +LPUSH mylist "World" +LPUSHX mylist "Hello" +LPUSHX myotherlist "Hello" +LRANGE mylist 0 -1 +LRANGE myotherlist 0 -1 +``` diff --git a/iredis/data/commands/lrange.md b/iredis/data/commands/lrange.md new file mode 100644 index 0000000..923b542 --- /dev/null +++ b/iredis/data/commands/lrange.md @@ -0,0 +1,37 @@ +Returns the specified elements of the list stored at `key`. The offsets `start` +and `stop` are zero-based indexes, with `0` being the first element of the list +(the head of the list), `1` being the next element and so on. + +These offsets can also be negative numbers indicating offsets starting at the +end of the list. For example, `-1` is the last element of the list, `-2` the +penultimate, and so on. + +## Consistency with range functions in various programming languages + +Note that if you have a list of numbers from 0 to 100, `LRANGE list 0 10` will +return 11 elements, that is, the rightmost item is included. This **may or may +not** be consistent with behavior of range-related functions in your programming +language of choice (think Ruby's `Range.new`, `Array#slice` or Python's +`range()` function). + +## Out-of-range indexes + +Out of range indexes will not produce an error. If `start` is larger than the +end of the list, an empty list is returned. If `stop` is larger than the actual +end of the list, Redis will treat it like the last element of the list. + +@return + +@array-reply: list of elements in the specified range. + +@examples + +```cli +RPUSH mylist "one" +RPUSH mylist "two" +RPUSH mylist "three" +LRANGE mylist 0 0 +LRANGE mylist -3 2 +LRANGE mylist -100 100 +LRANGE mylist 5 10 +``` diff --git a/iredis/data/commands/lrem.md b/iredis/data/commands/lrem.md new file mode 100644 index 0000000..c06dda7 --- /dev/null +++ b/iredis/data/commands/lrem.md @@ -0,0 +1,28 @@ +Removes the first `count` occurrences of elements equal to `element` from the +list stored at `key`. The `count` argument influences the operation in the +following ways: + +- `count > 0`: Remove elements equal to `element` moving from head to tail. +- `count < 0`: Remove elements equal to `element` moving from tail to head. +- `count = 0`: Remove all elements equal to `element`. + +For example, `LREM list -2 "hello"` will remove the last two occurrences of +`"hello"` in the list stored at `list`. + +Note that non-existing keys are treated like empty lists, so when `key` does not +exist, the command will always return `0`. + +@return + +@integer-reply: the number of removed elements. + +@examples + +```cli +RPUSH mylist "hello" +RPUSH mylist "hello" +RPUSH mylist "foo" +RPUSH mylist "hello" +LREM mylist -2 "hello" +LRANGE mylist 0 -1 +``` diff --git a/iredis/data/commands/lset.md b/iredis/data/commands/lset.md new file mode 100644 index 0000000..fad99ce --- /dev/null +++ b/iredis/data/commands/lset.md @@ -0,0 +1,19 @@ +Sets the list element at `index` to `element`. For more information on the +`index` argument, see `LINDEX`. + +An error is returned for out of range indexes. + +@return + +@simple-string-reply + +@examples + +```cli +RPUSH mylist "one" +RPUSH mylist "two" +RPUSH mylist "three" +LSET mylist 0 "four" +LSET mylist -2 "five" +LRANGE mylist 0 -1 +``` diff --git a/iredis/data/commands/ltrim.md b/iredis/data/commands/ltrim.md new file mode 100644 index 0000000..fd7fca5 --- /dev/null +++ b/iredis/data/commands/ltrim.md @@ -0,0 +1,42 @@ +Trim an existing list so that it will contain only the specified range of +elements specified. Both `start` and `stop` are zero-based indexes, where `0` is +the first element of the list (the head), `1` the next element and so on. + +For example: `LTRIM foobar 0 2` will modify the list stored at `foobar` so that +only the first three elements of the list will remain. + +`start` and `end` can also be negative numbers indicating offsets from the end +of the list, where `-1` is the last element of the list, `-2` the penultimate +element and so on. + +Out of range indexes will not produce an error: if `start` is larger than the +end of the list, or `start > end`, the result will be an empty list (which +causes `key` to be removed). If `end` is larger than the end of the list, Redis +will treat it like the last element of the list. + +A common use of `LTRIM` is together with `LPUSH` / `RPUSH`. For example: + +``` +LPUSH mylist someelement +LTRIM mylist 0 99 +``` + +This pair of commands will push a new element on the list, while making sure +that the list will not grow larger than 100 elements. This is very useful when +using Redis to store logs for example. It is important to note that when used in +this way `LTRIM` is an O(1) operation because in the average case just one +element is removed from the tail of the list. + +@return + +@simple-string-reply + +@examples + +```cli +RPUSH mylist "one" +RPUSH mylist "two" +RPUSH mylist "three" +LTRIM mylist 1 -1 +LRANGE mylist 0 -1 +``` diff --git a/iredis/data/commands/memory-doctor.md b/iredis/data/commands/memory-doctor.md new file mode 100644 index 0000000..0c9a172 --- /dev/null +++ b/iredis/data/commands/memory-doctor.md @@ -0,0 +1,6 @@ +The `MEMORY DOCTOR` command reports about different memory-related issues that +the Redis server experiences, and advises about possible remedies. + +@return + +@bulk-string-reply diff --git a/iredis/data/commands/memory-help.md b/iredis/data/commands/memory-help.md new file mode 100644 index 0000000..c0f4086 --- /dev/null +++ b/iredis/data/commands/memory-help.md @@ -0,0 +1,6 @@ +The `MEMORY HELP` command returns a helpful text describing the different +subcommands. + +@return + +@array-reply: a list of subcommands and their descriptions diff --git a/iredis/data/commands/memory-malloc-stats.md b/iredis/data/commands/memory-malloc-stats.md new file mode 100644 index 0000000..8da8e72 --- /dev/null +++ b/iredis/data/commands/memory-malloc-stats.md @@ -0,0 +1,9 @@ +The `MEMORY MALLOC-STATS` command provides an internal statistics report from +the memory allocator. + +This command is currently implemented only when using **jemalloc** as an +allocator, and evaluates to a benign NOOP for all others. + +@return + +@bulk-string-reply: the memory allocator's internal statistics report diff --git a/iredis/data/commands/memory-purge.md b/iredis/data/commands/memory-purge.md new file mode 100644 index 0000000..5ebe433 --- /dev/null +++ b/iredis/data/commands/memory-purge.md @@ -0,0 +1,9 @@ +The `MEMORY PURGE` command attempts to purge dirty pages so these can be +reclaimed by the allocator. + +This command is currently implemented only when using **jemalloc** as an +allocator, and evaluates to a benign NOOP for all others. + +@return + +@simple-string-reply diff --git a/iredis/data/commands/memory-stats.md b/iredis/data/commands/memory-stats.md new file mode 100644 index 0000000..4820c6a --- /dev/null +++ b/iredis/data/commands/memory-stats.md @@ -0,0 +1,50 @@ +The `MEMORY STATS` command returns an @array-reply about the memory usage of the +server. + +The information about memory usage is provided as metrics and their respective +values. The following metrics are reported: + +- `peak.allocated`: Peak memory consumed by Redis in bytes (see `INFO`'s + `used_memory_peak`) +- `total.allocated`: Total number of bytes allocated by Redis using its + allocator (see `INFO`'s `used_memory`) +- `startup.allocated`: Initial amount of memory consumed by Redis at startup in + bytes (see `INFO`'s `used_memory_startup`) +- `replication.backlog`: Size in bytes of the replication backlog (see `INFO`'s + `repl_backlog_active`) +- `clients.slaves`: The total size in bytes of all replicas overheads (output + and query buffers, connection contexts) +- `clients.normal`: The total size in bytes of all clients overheads (output and + query buffers, connection contexts) +- `aof.buffer`: The summed size in bytes of the current and rewrite AOF buffers + (see `INFO`'s `aof_buffer_length` and `aof_rewrite_buffer_length`, + respectively) +- `lua.caches`: the summed size in bytes of the overheads of the Lua scripts' + caches +- `dbXXX`: For each of the server's databases, the overheads of the main and + expiry dictionaries (`overhead.hashtable.main` and + `overhead.hashtable.expires`, respectively) are reported in bytes +- `overhead.total`: The sum of all overheads, i.e. `startup.allocated`, + `replication.backlog`, `clients.slaves`, `clients.normal`, `aof.buffer` and + those of the internal data structures that are used in managing the Redis + keyspace (see `INFO`'s `used_memory_overhead`) +- `keys.count`: The total number of keys stored across all databases in the + server +- `keys.bytes-per-key`: The ratio between **net memory usage** + (`total.allocated` minus `startup.allocated`) and `keys.count` +- `dataset.bytes`: The size in bytes of the dataset, i.e. `overhead.total` + subtracted from `total.allocated` (see `INFO`'s `used_memory_dataset`) +- `dataset.percentage`: The percentage of `dataset.bytes` out of the net memory + usage +- `peak.percentage`: The percentage of `peak.allocated` out of `total.allocated` +- `fragmentation`: See `INFO`'s `mem_fragmentation_ratio` + +@return + +@array-reply: nested list of memory usage metrics and their values + +**A note about the word slave used in this man page**: Starting with Redis 5, if +not for backward compatibility, the Redis project no longer uses the word slave. +Unfortunately in this command the word slave is part of the protocol, so we'll +be able to remove such occurrences only when this API will be naturally +deprecated. diff --git a/iredis/data/commands/memory-usage.md b/iredis/data/commands/memory-usage.md new file mode 100644 index 0000000..73e26d7 --- /dev/null +++ b/iredis/data/commands/memory-usage.md @@ -0,0 +1,40 @@ +The `MEMORY USAGE` command reports the number of bytes that a key and its value +require to be stored in RAM. + +The reported usage is the total of memory allocations for data and +administrative overheads that a key its value require. + +For nested data types, the optional `SAMPLES` option can be provided, where +`count` is the number of sampled nested values. By default, this option is set +to `5`. To sample the all of the nested values, use `SAMPLES 0`. + +@examples + +With Redis v4.0.1 64-bit and **jemalloc**, the empty string measures as follows: + +``` +> SET "" "" +OK +> MEMORY USAGE "" +(integer) 51 +``` + +These bytes are pure overhead at the moment as no actual data is stored, and are +used for maintaining the internal data structures of the server. Longer keys and +values show asymptotically linear usage. + +``` +> SET foo bar +OK +> MEMORY USAGE foo +(integer) 54 +> SET cento 01234567890123456789012345678901234567890123 +45678901234567890123456789012345678901234567890123456789 +OK +127.0.0.1:6379> MEMORY USAGE cento +(integer) 153 +``` + +@return + +@integer-reply: the memory usage in bytes diff --git a/iredis/data/commands/mget.md b/iredis/data/commands/mget.md new file mode 100644 index 0000000..130f935 --- /dev/null +++ b/iredis/data/commands/mget.md @@ -0,0 +1,15 @@ +Returns the values of all specified keys. For every key that does not hold a +string value or does not exist, the special value `nil` is returned. Because of +this, the operation never fails. + +@return + +@array-reply: list of values at the specified keys. + +@examples + +```cli +SET key1 "Hello" +SET key2 "World" +MGET key1 key2 nonexisting +``` diff --git a/iredis/data/commands/migrate.md b/iredis/data/commands/migrate.md new file mode 100644 index 0000000..cc4561c --- /dev/null +++ b/iredis/data/commands/migrate.md @@ -0,0 +1,78 @@ +Atomically transfer a key from a source Redis instance to a destination Redis +instance. On success the key is deleted from the original instance and is +guaranteed to exist in the target instance. + +The command is atomic and blocks the two instances for the time required to +transfer the key, at any given time the key will appear to exist in a given +instance or in the other instance, unless a timeout error occurs. In 3.2 and +above, multiple keys can be pipelined in a single call to `MIGRATE` by passing +the empty string ("") as key and adding the `KEYS` clause. + +The command internally uses `DUMP` to generate the serialized version of the key +value, and `RESTORE` in order to synthesize the key in the target instance. The +source instance acts as a client for the target instance. If the target instance +returns OK to the `RESTORE` command, the source instance deletes the key using +`DEL`. + +The timeout specifies the maximum idle time in any moment of the communication +with the destination instance in milliseconds. This means that the operation +does not need to be completed within the specified amount of milliseconds, but +that the transfer should make progresses without blocking for more than the +specified amount of milliseconds. + +`MIGRATE` needs to perform I/O operations and to honor the specified timeout. +When there is an I/O error during the transfer or if the timeout is reached the +operation is aborted and the special error - `IOERR` returned. When this happens +the following two cases are possible: + +- The key may be on both the instances. +- The key may be only in the source instance. + +It is not possible for the key to get lost in the event of a timeout, but the +client calling `MIGRATE`, in the event of a timeout error, should check if the +key is _also_ present in the target instance and act accordingly. + +When any other error is returned (starting with `ERR`) `MIGRATE` guarantees that +the key is still only present in the originating instance (unless a key with the +same name was also _already_ present on the target instance). + +If there are no keys to migrate in the source instance `NOKEY` is returned. +Because missing keys are possible in normal conditions, from expiry for example, +`NOKEY` isn't an error. + +## Migrating multiple keys with a single command call + +Starting with Redis 3.0.6 `MIGRATE` supports a new bulk-migration mode that uses +pipelining in order to migrate multiple keys between instances without incurring +in the round trip time latency and other overheads that there are when moving +each key with a single `MIGRATE` call. + +In order to enable this form, the `KEYS` option is used, and the normal _key_ +argument is set to an empty string. The actual key names will be provided after +the `KEYS` argument itself, like in the following example: + + MIGRATE 192.168.1.34 6379 "" 0 5000 KEYS key1 key2 key3 + +When this form is used the `NOKEY` status code is only returned when none of the +keys is present in the instance, otherwise the command is executed, even if just +a single key exists. + +## Options + +- `COPY` -- Do not remove the key from the local instance. +- `REPLACE` -- Replace existing key on the remote instance. +- `KEYS` -- If the key argument is an empty string, the command will instead + migrate all the keys that follow the `KEYS` option (see the above section for + more info). +- `AUTH` -- Authenticate with the given password to the remote instance. +- `AUTH2` -- Authenticate with the given username and password pair (Redis 6 or + greater ACL auth style). + +`COPY` and `REPLACE` are available only in 3.0 and above. `KEYS` is available +starting with Redis 3.0.6. `AUTH` is available starting with Redis 4.0.7. +`AUTH2` is available starting with Redis 6.0.0. + +@return + +@simple-string-reply: The command returns OK on success, or `NOKEY` if no keys +were found in the source instance. diff --git a/iredis/data/commands/module-list.md b/iredis/data/commands/module-list.md new file mode 100644 index 0000000..d951f23 --- /dev/null +++ b/iredis/data/commands/module-list.md @@ -0,0 +1,10 @@ +Returns information about the modules loaded to the server. + +@return + +@array-reply: list of loaded modules. Each element in the list represents a +module, and is in itself a list of property names and their values. The +following properties is reported for each loaded module: + +- `name`: Name of the module +- `ver`: Version of the module diff --git a/iredis/data/commands/module-load.md b/iredis/data/commands/module-load.md new file mode 100644 index 0000000..c5919c0 --- /dev/null +++ b/iredis/data/commands/module-load.md @@ -0,0 +1,13 @@ +Loads a module from a dynamic library at runtime. + +This command loads and initializes the Redis module from the dynamic library +specified by the `path` argument. The `path` should be the absolute path of the +library, including the full filename. Any additional arguments are passed +unmodified to the module. + +**Note**: modules can also be loaded at server startup with 'loadmodule' +configuration directive in `redis.conf`. + +@return + +@simple-string-reply: `OK` if module was loaded. diff --git a/iredis/data/commands/module-unload.md b/iredis/data/commands/module-unload.md new file mode 100644 index 0000000..c5ce38e --- /dev/null +++ b/iredis/data/commands/module-unload.md @@ -0,0 +1,13 @@ +Unloads a module. + +This command unloads the module specified by `name`. Note that the module's name +is reported by the `MODULE LIST` command, and may differ from the dynamic +library's filename. + +Known limitations: + +- Modules that register custom data types can not be unloaded. + +@return + +@simple-string-reply: `OK` if module was unloaded. diff --git a/iredis/data/commands/monitor.md b/iredis/data/commands/monitor.md new file mode 100644 index 0000000..7900787 --- /dev/null +++ b/iredis/data/commands/monitor.md @@ -0,0 +1,93 @@ +`MONITOR` is a debugging command that streams back every command processed by +the Redis server. It can help in understanding what is happening to the +database. This command can both be used via `redis-cli` and via `telnet`. + +The ability to see all the requests processed by the server is useful in order +to spot bugs in an application both when using Redis as a database and as a +distributed caching system. + +``` +$ redis-cli monitor +1339518083.107412 [0 127.0.0.1:60866] "keys" "*" +1339518087.877697 [0 127.0.0.1:60866] "dbsize" +1339518090.420270 [0 127.0.0.1:60866] "set" "x" "6" +1339518096.506257 [0 127.0.0.1:60866] "get" "x" +1339518099.363765 [0 127.0.0.1:60866] "del" "x" +1339518100.544926 [0 127.0.0.1:60866] "get" "x" +``` + +Use `SIGINT` (Ctrl-C) to stop a `MONITOR` stream running via `redis-cli`. + +``` +$ telnet localhost 6379 +Trying 127.0.0.1... +Connected to localhost. +Escape character is '^]'. +MONITOR ++OK ++1339518083.107412 [0 127.0.0.1:60866] "keys" "*" ++1339518087.877697 [0 127.0.0.1:60866] "dbsize" ++1339518090.420270 [0 127.0.0.1:60866] "set" "x" "6" ++1339518096.506257 [0 127.0.0.1:60866] "get" "x" ++1339518099.363765 [0 127.0.0.1:60866] "del" "x" ++1339518100.544926 [0 127.0.0.1:60866] "get" "x" +QUIT ++OK +Connection closed by foreign host. +``` + +Manually issue the `QUIT` command to stop a `MONITOR` stream running via +`telnet`. + +## Commands not logged by MONITOR + +Because of security concerns, all administrative commands are not logged by +`MONITOR`'s output. + +Furthermore, the following commands are also not logged: + +- `AUTH` +- `EXEC` +- `HELLO` +- `QUIT` + +## Cost of running MONITOR + +Because `MONITOR` streams back **all** commands, its use comes at a cost. The +following (totally unscientific) benchmark numbers illustrate what the cost of +running `MONITOR` can be. + +Benchmark result **without** `MONITOR` running: + +``` +$ src/redis-benchmark -c 10 -n 100000 -q +PING_INLINE: 101936.80 requests per second +PING_BULK: 102880.66 requests per second +SET: 95419.85 requests per second +GET: 104275.29 requests per second +INCR: 93283.58 requests per second +``` + +Benchmark result **with** `MONITOR` running (`redis-cli monitor > /dev/null`): + +``` +$ src/redis-benchmark -c 10 -n 100000 -q +PING_INLINE: 58479.53 requests per second +PING_BULK: 59136.61 requests per second +SET: 41823.50 requests per second +GET: 45330.91 requests per second +INCR: 41771.09 requests per second +``` + +In this particular case, running a single `MONITOR` client can reduce the +throughput by more than 50%. Running more `MONITOR` clients will reduce +throughput even more. + +@return + +**Non standard return value**, just dumps the received commands in an infinite +flow. + +@history + +- `>=6.0`: `AUTH` excluded from the command's output. diff --git a/iredis/data/commands/move.md b/iredis/data/commands/move.md new file mode 100644 index 0000000..e007a18 --- /dev/null +++ b/iredis/data/commands/move.md @@ -0,0 +1,11 @@ +Move `key` from the currently selected database (see `SELECT`) to the specified +destination database. When `key` already exists in the destination database, or +it does not exist in the source database, it does nothing. It is possible to use +`MOVE` as a locking primitive because of this. + +@return + +@integer-reply, specifically: + +- `1` if `key` was moved. +- `0` if `key` was not moved. diff --git a/iredis/data/commands/mset.md b/iredis/data/commands/mset.md new file mode 100644 index 0000000..9c17f86 --- /dev/null +++ b/iredis/data/commands/mset.md @@ -0,0 +1,18 @@ +Sets the given keys to their respective values. `MSET` replaces existing values +with new values, just as regular `SET`. See `MSETNX` if you don't want to +overwrite existing values. + +`MSET` is atomic, so all given keys are set at once. It is not possible for +clients to see that some of the keys were updated while others are unchanged. + +@return + +@simple-string-reply: always `OK` since `MSET` can't fail. + +@examples + +```cli +MSET key1 "Hello" key2 "World" +GET key1 +GET key2 +``` diff --git a/iredis/data/commands/msetnx.md b/iredis/data/commands/msetnx.md new file mode 100644 index 0000000..e332223 --- /dev/null +++ b/iredis/data/commands/msetnx.md @@ -0,0 +1,24 @@ +Sets the given keys to their respective values. `MSETNX` will not perform any +operation at all even if just a single key already exists. + +Because of this semantic `MSETNX` can be used in order to set different keys +representing different fields of an unique logic object in a way that ensures +that either all the fields or none at all are set. + +`MSETNX` is atomic, so all given keys are set at once. It is not possible for +clients to see that some of the keys were updated while others are unchanged. + +@return + +@integer-reply, specifically: + +- `1` if the all the keys were set. +- `0` if no key was set (at least one key already existed). + +@examples + +```cli +MSETNX key1 "Hello" key2 "there" +MSETNX key2 "new" key3 "world" +MGET key1 key2 key3 +``` diff --git a/iredis/data/commands/multi.md b/iredis/data/commands/multi.md new file mode 100644 index 0000000..9ed46b6 --- /dev/null +++ b/iredis/data/commands/multi.md @@ -0,0 +1,8 @@ +Marks the start of a [transaction][tt] block. Subsequent commands will be queued +for atomic execution using `EXEC`. + +[tt]: /topics/transactions + +@return + +@simple-string-reply: always `OK`. diff --git a/iredis/data/commands/object.md b/iredis/data/commands/object.md new file mode 100644 index 0000000..e5321f7 --- /dev/null +++ b/iredis/data/commands/object.md @@ -0,0 +1,80 @@ +The `OBJECT` command allows to inspect the internals of Redis Objects associated +with keys. It is useful for debugging or to understand if your keys are using +the specially encoded data types to save space. Your application may also use +the information reported by the `OBJECT` command to implement application level +key eviction policies when using Redis as a Cache. + +The `OBJECT` command supports multiple sub commands: + +- `OBJECT REFCOUNT <key>` returns the number of references of the value + associated with the specified key. This command is mainly useful for + debugging. +- `OBJECT ENCODING <key>` returns the kind of internal representation used in + order to store the value associated with a key. +- `OBJECT IDLETIME <key>` returns the number of seconds since the object stored + at the specified key is idle (not requested by read or write operations). + While the value is returned in seconds the actual resolution of this timer is + 10 seconds, but may vary in future implementations. This subcommand is + available when `maxmemory-policy` is set to an LRU policy or `noeviction` and + `maxmemory` is set. +- `OBJECT FREQ <key>` returns the logarithmic access frequency counter of the + object stored at the specified key. This subcommand is available when + `maxmemory-policy` is set to an LFU policy. +- `OBJECT HELP` returns a succinct help text. + +Objects can be encoded in different ways: + +- Strings can be encoded as `raw` (normal string encoding) or `int` (strings + representing integers in a 64 bit signed interval are encoded in this way in + order to save space). +- Lists can be encoded as `ziplist` or `linkedlist`. The `ziplist` is the + special representation that is used to save space for small lists. +- Sets can be encoded as `intset` or `hashtable`. The `intset` is a special + encoding used for small sets composed solely of integers. +- Hashes can be encoded as `ziplist` or `hashtable`. The `ziplist` is a special + encoding used for small hashes. +- Sorted Sets can be encoded as `ziplist` or `skiplist` format. As for the List + type small sorted sets can be specially encoded using `ziplist`, while the + `skiplist` encoding is the one that works with sorted sets of any size. + +All the specially encoded types are automatically converted to the general type +once you perform an operation that makes it impossible for Redis to retain the +space saving encoding. + +@return + +Different return values are used for different subcommands. + +- Subcommands `refcount` and `idletime` return integers. +- Subcommand `encoding` returns a bulk reply. + +If the object you try to inspect is missing, a null bulk reply is returned. + +@examples + +``` +redis> lpush mylist "Hello World" +(integer) 4 +redis> object refcount mylist +(integer) 1 +redis> object encoding mylist +"ziplist" +redis> object idletime mylist +(integer) 10 +``` + +In the following example you can see how the encoding changes once Redis is no +longer able to use the space saving encoding. + +``` +redis> set foo 1000 +OK +redis> object encoding foo +"int" +redis> append foo bar +(integer) 7 +redis> get foo +"1000bar" +redis> object encoding foo +"raw" +``` diff --git a/iredis/data/commands/persist.md b/iredis/data/commands/persist.md new file mode 100644 index 0000000..4819230 --- /dev/null +++ b/iredis/data/commands/persist.md @@ -0,0 +1,20 @@ +Remove the existing timeout on `key`, turning the key from _volatile_ (a key +with an expire set) to _persistent_ (a key that will never expire as no timeout +is associated). + +@return + +@integer-reply, specifically: + +- `1` if the timeout was removed. +- `0` if `key` does not exist or does not have an associated timeout. + +@examples + +```cli +SET mykey "Hello" +EXPIRE mykey 10 +TTL mykey +PERSIST mykey +TTL mykey +``` diff --git a/iredis/data/commands/pexpire.md b/iredis/data/commands/pexpire.md new file mode 100644 index 0000000..ae5f775 --- /dev/null +++ b/iredis/data/commands/pexpire.md @@ -0,0 +1,18 @@ +This command works exactly like `EXPIRE` but the time to live of the key is +specified in milliseconds instead of seconds. + +@return + +@integer-reply, specifically: + +- `1` if the timeout was set. +- `0` if `key` does not exist. + +@examples + +```cli +SET mykey "Hello" +PEXPIRE mykey 1500 +TTL mykey +PTTL mykey +``` diff --git a/iredis/data/commands/pexpireat.md b/iredis/data/commands/pexpireat.md new file mode 100644 index 0000000..4b3ebb7 --- /dev/null +++ b/iredis/data/commands/pexpireat.md @@ -0,0 +1,18 @@ +`PEXPIREAT` has the same effect and semantic as `EXPIREAT`, but the Unix time at +which the key will expire is specified in milliseconds instead of seconds. + +@return + +@integer-reply, specifically: + +- `1` if the timeout was set. +- `0` if `key` does not exist. + +@examples + +```cli +SET mykey "Hello" +PEXPIREAT mykey 1555555555005 +TTL mykey +PTTL mykey +``` diff --git a/iredis/data/commands/pfadd.md b/iredis/data/commands/pfadd.md new file mode 100644 index 0000000..e8e3f03 --- /dev/null +++ b/iredis/data/commands/pfadd.md @@ -0,0 +1,33 @@ +Adds all the element arguments to the HyperLogLog data structure stored at the +variable name specified as first argument. + +As a side effect of this command the HyperLogLog internals may be updated to +reflect a different estimation of the number of unique items added so far (the +cardinality of the set). + +If the approximated cardinality estimated by the HyperLogLog changed after +executing the command, `PFADD` returns 1, otherwise 0 is returned. The command +automatically creates an empty HyperLogLog structure (that is, a Redis String of +a specified length and with a given encoding) if the specified key does not +exist. + +To call the command without elements but just the variable name is valid, this +will result into no operation performed if the variable already exists, or just +the creation of the data structure if the key does not exist (in the latter case +1 is returned). + +For an introduction to HyperLogLog data structure check the `PFCOUNT` command +page. + +@return + +@integer-reply, specifically: + +- 1 if at least 1 HyperLogLog internal register was altered. 0 otherwise. + +@examples + +```cli +PFADD hll a b c d e f g +PFCOUNT hll +``` diff --git a/iredis/data/commands/pfcount.md b/iredis/data/commands/pfcount.md new file mode 100644 index 0000000..e39b19e --- /dev/null +++ b/iredis/data/commands/pfcount.md @@ -0,0 +1,93 @@ +When called with a single key, returns the approximated cardinality computed by +the HyperLogLog data structure stored at the specified variable, which is 0 if +the variable does not exist. + +When called with multiple keys, returns the approximated cardinality of the +union of the HyperLogLogs passed, by internally merging the HyperLogLogs stored +at the provided keys into a temporary HyperLogLog. + +The HyperLogLog data structure can be used in order to count **unique** elements +in a set using just a small constant amount of memory, specifically 12k bytes +for every HyperLogLog (plus a few bytes for the key itself). + +The returned cardinality of the observed set is not exact, but approximated with +a standard error of 0.81%. + +For example in order to take the count of all the unique search queries +performed in a day, a program needs to call `PFADD` every time a query is +processed. The estimated number of unique queries can be retrieved with +`PFCOUNT` at any time. + +Note: as a side effect of calling this function, it is possible that the +HyperLogLog is modified, since the last 8 bytes encode the latest computed +cardinality for caching purposes. So `PFCOUNT` is technically a write command. + +@return + +@integer-reply, specifically: + +- The approximated number of unique elements observed via `PFADD`. + +@examples + +```cli +PFADD hll foo bar zap +PFADD hll zap zap zap +PFADD hll foo bar +PFCOUNT hll +PFADD some-other-hll 1 2 3 +PFCOUNT hll some-other-hll +``` + +## Performances + +When `PFCOUNT` is called with a single key, performances are excellent even if +in theory constant times to process a dense HyperLogLog are high. This is +possible because the `PFCOUNT` uses caching in order to remember the cardinality +previously computed, that rarely changes because most `PFADD` operations will +not update any register. Hundreds of operations per second are possible. + +When `PFCOUNT` is called with multiple keys, an on-the-fly merge of the +HyperLogLogs is performed, which is slow, moreover the cardinality of the union +can't be cached, so when used with multiple keys `PFCOUNT` may take a time in +the order of magnitude of the millisecond, and should be not abused. + +The user should take in mind that single-key and multiple-keys executions of +this command are semantically different and have different performances. + +## HyperLogLog representation + +Redis HyperLogLogs are represented using a double representation: the _sparse_ +representation suitable for HLLs counting a small number of elements (resulting +in a small number of registers set to non-zero value), and a _dense_ +representation suitable for higher cardinalities. Redis automatically switches +from the sparse to the dense representation when needed. + +The sparse representation uses a run-length encoding optimized to store +efficiently a big number of registers set to zero. The dense representation is a +Redis string of 12288 bytes in order to store 16384 6-bit counters. The need for +the double representation comes from the fact that using 12k (which is the dense +representation memory requirement) to encode just a few registers for smaller +cardinalities is extremely suboptimal. + +Both representations are prefixed with a 16 bytes header, that includes a magic, +an encoding / version field, and the cached cardinality estimation computed, +stored in little endian format (the most significant bit is 1 if the estimation +is invalid since the HyperLogLog was updated since the cardinality was +computed). + +The HyperLogLog, being a Redis string, can be retrieved with `GET` and restored +with `SET`. Calling `PFADD`, `PFCOUNT` or `PFMERGE` commands with a corrupted +HyperLogLog is never a problem, it may return random values but does not affect +the stability of the server. Most of the times when corrupting a sparse +representation, the server recognizes the corruption and returns an error. + +The representation is neutral from the point of view of the processor word size +and endianness, so the same representation is used by 32 bit and 64 bit +processor, big endian or little endian. + +More details about the Redis HyperLogLog implementation can be found in +[this blog post](http://antirez.com/news/75). The source code of the +implementation in the `hyperloglog.c` file is also easy to read and understand, +and includes a full specification for the exact encoding used for the sparse and +dense representations. diff --git a/iredis/data/commands/pfmerge.md b/iredis/data/commands/pfmerge.md new file mode 100644 index 0000000..90e1dc9 --- /dev/null +++ b/iredis/data/commands/pfmerge.md @@ -0,0 +1,22 @@ +Merge multiple HyperLogLog values into an unique value that will approximate the +cardinality of the union of the observed Sets of the source HyperLogLog +structures. + +The computed merged HyperLogLog is set to the destination variable, which is +created if does not exist (defaulting to an empty HyperLogLog). + +If the destination variable exists, it is treated as one of the source sets and +its cardinality will be included in the cardinality of the computed HyperLogLog. + +@return + +@simple-string-reply: The command just returns `OK`. + +@examples + +```cli +PFADD hll1 foo bar zap a +PFADD hll2 a b c foo +PFMERGE hll3 hll1 hll2 +PFCOUNT hll3 +``` diff --git a/iredis/data/commands/ping.md b/iredis/data/commands/ping.md new file mode 100644 index 0000000..251cc07 --- /dev/null +++ b/iredis/data/commands/ping.md @@ -0,0 +1,20 @@ +Returns `PONG` if no argument is provided, otherwise return a copy of the +argument as a bulk. This command is often used to test if a connection is still +alive, or to measure latency. + +If the client is subscribed to a channel or a pattern, it will instead return a +multi-bulk with a "pong" in the first position and an empty bulk in the second +position, unless an argument is provided in which case it returns a copy of the +argument. + +@return + +@simple-string-reply + +@examples + +```cli +PING + +PING "hello world" +``` diff --git a/iredis/data/commands/psetex.md b/iredis/data/commands/psetex.md new file mode 100644 index 0000000..3e9988e --- /dev/null +++ b/iredis/data/commands/psetex.md @@ -0,0 +1,10 @@ +`PSETEX` works exactly like `SETEX` with the sole difference that the expire +time is specified in milliseconds instead of seconds. + +@examples + +```cli +PSETEX mykey 1000 "Hello" +PTTL mykey +GET mykey +``` diff --git a/iredis/data/commands/psubscribe.md b/iredis/data/commands/psubscribe.md new file mode 100644 index 0000000..fb14ca1 --- /dev/null +++ b/iredis/data/commands/psubscribe.md @@ -0,0 +1,9 @@ +Subscribes the client to the given patterns. + +Supported glob-style patterns: + +- `h?llo` subscribes to `hello`, `hallo` and `hxllo` +- `h*llo` subscribes to `hllo` and `heeeello` +- `h[ae]llo` subscribes to `hello` and `hallo,` but not `hillo` + +Use `\` to escape special characters if you want to match them verbatim. diff --git a/iredis/data/commands/psync.md b/iredis/data/commands/psync.md new file mode 100644 index 0000000..756a3b6 --- /dev/null +++ b/iredis/data/commands/psync.md @@ -0,0 +1,14 @@ +Initiates a replication stream from the master. + +The `PSYNC` command is called by Redis replicas for initiating a replication +stream from the master. + +For more information about replication in Redis please check the [replication +page][tr]. + +[tr]: /topics/replication + +@return + +**Non standard return value**, a bulk transfer of the data followed by `PING` +and write requests from the master. diff --git a/iredis/data/commands/pttl.md b/iredis/data/commands/pttl.md new file mode 100644 index 0000000..49eea99 --- /dev/null +++ b/iredis/data/commands/pttl.md @@ -0,0 +1,24 @@ +Like `TTL` this command returns the remaining time to live of a key that has an +expire set, with the sole difference that `TTL` returns the amount of remaining +time in seconds while `PTTL` returns it in milliseconds. + +In Redis 2.6 or older the command returns `-1` if the key does not exist or if +the key exist but has no associated expire. + +Starting with Redis 2.8 the return value in case of error changed: + +- The command returns `-2` if the key does not exist. +- The command returns `-1` if the key exists but has no associated expire. + +@return + +@integer-reply: TTL in milliseconds, or a negative value in order to signal an +error (see the description above). + +@examples + +```cli +SET mykey "Hello" +EXPIRE mykey 1 +PTTL mykey +``` diff --git a/iredis/data/commands/publish.md b/iredis/data/commands/publish.md new file mode 100644 index 0000000..e4b338a --- /dev/null +++ b/iredis/data/commands/publish.md @@ -0,0 +1,5 @@ +Posts a message to the given channel. + +@return + +@integer-reply: the number of clients that received the message. diff --git a/iredis/data/commands/pubsub.md b/iredis/data/commands/pubsub.md new file mode 100644 index 0000000..0a8c0a2 --- /dev/null +++ b/iredis/data/commands/pubsub.md @@ -0,0 +1,44 @@ +The PUBSUB command is an introspection command that allows to inspect the state +of the Pub/Sub subsystem. It is composed of subcommands that are documented +separately. The general form is: + + PUBSUB <subcommand> ... args ... + +# PUBSUB CHANNELS [pattern] + +Lists the currently _active channels_. An active channel is a Pub/Sub channel +with one or more subscribers (not including clients subscribed to patterns). + +If no `pattern` is specified, all the channels are listed, otherwise if pattern +is specified only channels matching the specified glob-style pattern are listed. + +@return + +@array-reply: a list of active channels, optionally matching the specified +pattern. + +# `PUBSUB NUMSUB [channel-1 ... channel-N]` + +Returns the number of subscribers (not counting clients subscribed to patterns) +for the specified channels. + +@return + +@array-reply: a list of channels and number of subscribers for every channel. +The format is channel, count, channel, count, ..., so the list is flat. The +order in which the channels are listed is the same as the order of the channels +specified in the command call. + +Note that it is valid to call this command without channels. In this case it +will just return an empty list. + +# `PUBSUB NUMPAT` + +Returns the number of subscriptions to patterns (that are performed using the +`PSUBSCRIBE` command). Note that this is not just the count of clients +subscribed to patterns but the total number of patterns all the clients are +subscribed to. + +@return + +@integer-reply: the number of patterns all the clients are subscribed to. diff --git a/iredis/data/commands/punsubscribe.md b/iredis/data/commands/punsubscribe.md new file mode 100644 index 0000000..03ed279 --- /dev/null +++ b/iredis/data/commands/punsubscribe.md @@ -0,0 +1,6 @@ +Unsubscribes the client from the given patterns, or from all of them if none is +given. + +When no patterns are specified, the client is unsubscribed from all the +previously subscribed patterns. In this case, a message for every unsubscribed +pattern will be sent to the client. diff --git a/iredis/data/commands/quit.md b/iredis/data/commands/quit.md new file mode 100644 index 0000000..b6ce3bf --- /dev/null +++ b/iredis/data/commands/quit.md @@ -0,0 +1,6 @@ +Ask the server to close the connection. The connection is closed as soon as all +pending replies have been written to the client. + +@return + +@simple-string-reply: always OK. diff --git a/iredis/data/commands/randomkey.md b/iredis/data/commands/randomkey.md new file mode 100644 index 0000000..d823322 --- /dev/null +++ b/iredis/data/commands/randomkey.md @@ -0,0 +1,5 @@ +Return a random key from the currently selected database. + +@return + +@bulk-string-reply: the random key, or `nil` when the database is empty. diff --git a/iredis/data/commands/readonly.md b/iredis/data/commands/readonly.md new file mode 100644 index 0000000..00e8aad --- /dev/null +++ b/iredis/data/commands/readonly.md @@ -0,0 +1,21 @@ +Enables read queries for a connection to a Redis Cluster replica node. + +Normally replica nodes will redirect clients to the authoritative master for the +hash slot involved in a given command, however clients can use replicas in order +to scale reads using the `READONLY` command. + +`READONLY` tells a Redis Cluster replica node that the client is willing to read +possibly stale data and is not interested in running write queries. + +When the connection is in readonly mode, the cluster will send a redirection to +the client only if the operation involves keys not served by the replica's +master node. This may happen because: + +1. The client sent a command about hash slots never served by the master of this + replica. +2. The cluster was reconfigured (for example resharded) and the replica is no + longer able to serve commands for a given hash slot. + +@return + +@simple-string-reply diff --git a/iredis/data/commands/readwrite.md b/iredis/data/commands/readwrite.md new file mode 100644 index 0000000..f31a1f9 --- /dev/null +++ b/iredis/data/commands/readwrite.md @@ -0,0 +1,10 @@ +Disables read queries for a connection to a Redis Cluster slave node. + +Read queries against a Redis Cluster slave node are disabled by default, but you +can use the `READONLY` command to change this behavior on a per- connection +basis. The `READWRITE` command resets the readonly mode flag of a connection +back to readwrite. + +@return + +@simple-string-reply diff --git a/iredis/data/commands/rename.md b/iredis/data/commands/rename.md new file mode 100644 index 0000000..5a38f63 --- /dev/null +++ b/iredis/data/commands/rename.md @@ -0,0 +1,26 @@ +Renames `key` to `newkey`. It returns an error when `key` does not exist. If +`newkey` already exists it is overwritten, when this happens `RENAME` executes +an implicit `DEL` operation, so if the deleted key contains a very big value it +may cause high latency even if `RENAME` itself is usually a constant-time +operation. + +In Cluster mode, both `key` and `newkey` must be in the same **hash slot**, +meaning that in practice only keys that have the same hash tag can be reliably +renamed in cluster. + +@history + +- `<= 3.2.0`: Before Redis 3.2.0, an error is returned if source and destination + names are the same. + +@return + +@simple-string-reply + +@examples + +```cli +SET mykey "Hello" +RENAME mykey myotherkey +GET myotherkey +``` diff --git a/iredis/data/commands/renamenx.md b/iredis/data/commands/renamenx.md new file mode 100644 index 0000000..8f98ec5 --- /dev/null +++ b/iredis/data/commands/renamenx.md @@ -0,0 +1,27 @@ +Renames `key` to `newkey` if `newkey` does not yet exist. It returns an error +when `key` does not exist. + +In Cluster mode, both `key` and `newkey` must be in the same **hash slot**, +meaning that in practice only keys that have the same hash tag can be reliably +renamed in cluster. + +@history + +- `<= 3.2.0`: Before Redis 3.2.0, an error is returned if source and destination + names are the same. + +@return + +@integer-reply, specifically: + +- `1` if `key` was renamed to `newkey`. +- `0` if `newkey` already exists. + +@examples + +```cli +SET mykey "Hello" +SET myotherkey "World" +RENAMENX mykey myotherkey +GET myotherkey +``` diff --git a/iredis/data/commands/replicaof.md b/iredis/data/commands/replicaof.md new file mode 100644 index 0000000..c8c839c --- /dev/null +++ b/iredis/data/commands/replicaof.md @@ -0,0 +1,21 @@ +The `REPLICAOF` command can change the replication settings of a replica on the +fly. + +If a Redis server is already acting as replica, the command `REPLICAOF` NO ONE +will turn off the replication, turning the Redis server into a MASTER. In the +proper form `REPLICAOF` hostname port will make the server a replica of another +server listening at the specified hostname and port. + +If a server is already a replica of some master, `REPLICAOF` hostname port will +stop the replication against the old server and start the synchronization +against the new one, discarding the old dataset. + +The form `REPLICAOF` NO ONE will stop replication, turning the server into a +MASTER, but will not discard the replication. So, if the old master stops +working, it is possible to turn the replica into a master and set the +application to use this new master in read/write. Later when the other Redis +server is fixed, it can be reconfigured to work as a replica. + +@return + +@simple-string-reply diff --git a/iredis/data/commands/restore.md b/iredis/data/commands/restore.md new file mode 100644 index 0000000..50632e4 --- /dev/null +++ b/iredis/data/commands/restore.md @@ -0,0 +1,41 @@ +Create a key associated with a value that is obtained by deserializing the +provided serialized value (obtained via `DUMP`). + +If `ttl` is 0 the key is created without any expire, otherwise the specified +expire time (in milliseconds) is set. + +If the `ABSTTL` modifier was used, `ttl` should represent an absolute [Unix +timestamp][hewowu] (in milliseconds) in which the key will expire. (Redis 5.0 or +greater). + +[hewowu]: http://en.wikipedia.org/wiki/Unix_time + +For eviction purposes, you may use the `IDLETIME` or `FREQ` modifiers. See +`OBJECT` for more information (Redis 5.0 or greater). + +`RESTORE` will return a "Target key name is busy" error when `key` already +exists unless you use the `REPLACE` modifier (Redis 3.0 or greater). + +`RESTORE` checks the RDB version and data checksum. If they don't match an error +is returned. + +@return + +@simple-string-reply: The command returns OK on success. + +@examples + +``` +redis> DEL mykey +0 +redis> RESTORE mykey 0 "\n\x17\x17\x00\x00\x00\x12\x00\x00\x00\x03\x00\ + x00\xc0\x01\x00\x04\xc0\x02\x00\x04\xc0\x03\x00\ + xff\x04\x00u#<\xc0;.\xe9\xdd" +OK +redis> TYPE mykey +list +redis> LRANGE mykey 0 -1 +1) "1" +2) "2" +3) "3" +``` diff --git a/iredis/data/commands/role.md b/iredis/data/commands/role.md new file mode 100644 index 0000000..c42fa74 --- /dev/null +++ b/iredis/data/commands/role.md @@ -0,0 +1,105 @@ +Provide information on the role of a Redis instance in the context of +replication, by returning if the instance is currently a `master`, `slave`, or +`sentinel`. The command also returns additional information about the state of +the replication (if the role is master or slave) or the list of monitored master +names (if the role is sentinel). + +## Output format + +The command returns an array of elements. The first element is the role of the +instance, as one of the following three strings: + +- "master" +- "slave" +- "sentinel" + +The additional elements of the array depends on the role. + +## Master output + +An example of output when `ROLE` is called in a master instance: + +``` +1) "master" +2) (integer) 3129659 +3) 1) 1) "127.0.0.1" + 2) "9001" + 3) "3129242" + 2) 1) "127.0.0.1" + 2) "9002" + 3) "3129543" +``` + +The master output is composed of the following parts: + +1. The string `master`. +2. The current master replication offset, which is an offset that masters and + replicas share to understand, in partial resynchronizations, the part of the + replication stream the replicas needs to fetch to continue. +3. An array composed of three elements array representing the connected + replicas. Every sub-array contains the replica IP, port, and the last + acknowledged replication offset. + +## Output of the command on replicas + +An example of output when `ROLE` is called in a replica instance: + +``` +1) "slave" +2) "127.0.0.1" +3) (integer) 9000 +4) "connected" +5) (integer) 3167038 +``` + +The replica output is composed of the following parts: + +1. The string `slave`, because of backward compatibility (see note at the end of + this page). +2. The IP of the master. +3. The port number of the master. +4. The state of the replication from the point of view of the master, that can + be `connect` (the instance needs to connect to its master), `connecting` (the + master-replica connection is in progress), `sync` (the master and replica are + trying to perform the synchronization), `connected` (the replica is online). +5. The amount of data received from the replica so far in terms of master + replication offset. + +## Sentinel output + +An example of Sentinel output: + +``` +1) "sentinel" +2) 1) "resque-master" + 2) "html-fragments-master" + 3) "stats-master" + 4) "metadata-master" +``` + +The sentinel output is composed of the following parts: + +1. The string `sentinel`. +2. An array of master names monitored by this Sentinel instance. + +@return + +@array-reply: where the first element is one of `master`, `slave`, `sentinel` +and the additional elements are role-specific as illustrated above. + +@history + +- This command was introduced in the middle of a Redis stable release, + specifically with Redis 2.8.12. + +@examples + +```cli +ROLE +``` + +**A note about the word slave used in this man page**: Starting with Redis 5, if +not for backward compatibility, the Redis project no longer uses the word slave. +Unfortunately in this command the word slave is part of the protocol, so we'll +be able to remove such occurrences only when this API will be naturally +deprecated. diff --git a/iredis/data/commands/rpop.md b/iredis/data/commands/rpop.md new file mode 100644 index 0000000..9c03902 --- /dev/null +++ b/iredis/data/commands/rpop.md @@ -0,0 +1,16 @@ +Removes and returns the last element of the list stored at `key`. + +@return + +@bulk-string-reply: the value of the last element, or `nil` when `key` does not +exist. + +@examples + +```cli +RPUSH mylist "one" +RPUSH mylist "two" +RPUSH mylist "three" +RPOP mylist +LRANGE mylist 0 -1 +``` diff --git a/iredis/data/commands/rpoplpush.md b/iredis/data/commands/rpoplpush.md new file mode 100644 index 0000000..7a5b662 --- /dev/null +++ b/iredis/data/commands/rpoplpush.md @@ -0,0 +1,71 @@ +Atomically returns and removes the last element (tail) of the list stored at +`source`, and pushes the element at the first element (head) of the list stored +at `destination`. + +For example: consider `source` holding the list `a,b,c`, and `destination` +holding the list `x,y,z`. Executing `RPOPLPUSH` results in `source` holding +`a,b` and `destination` holding `c,x,y,z`. + +If `source` does not exist, the value `nil` is returned and no operation is +performed. If `source` and `destination` are the same, the operation is +equivalent to removing the last element from the list and pushing it as first +element of the list, so it can be considered as a list rotation command. + +@return + +@bulk-string-reply: the element being popped and pushed. + +@examples + +```cli +RPUSH mylist "one" +RPUSH mylist "two" +RPUSH mylist "three" +RPOPLPUSH mylist myotherlist +LRANGE mylist 0 -1 +LRANGE myotherlist 0 -1 +``` + +## Pattern: Reliable queue + +Redis is often used as a messaging server to implement processing of background +jobs or other kinds of messaging tasks. A simple form of queue is often obtained +pushing values into a list in the producer side, and waiting for this values in +the consumer side using `RPOP` (using polling), or `BRPOP` if the client is +better served by a blocking operation. + +However in this context the obtained queue is not _reliable_ as messages can be +lost, for example in the case there is a network problem or if the consumer +crashes just after the message is received but it is still to process. + +`RPOPLPUSH` (or `BRPOPLPUSH` for the blocking variant) offers a way to avoid +this problem: the consumer fetches the message and at the same time pushes it +into a _processing_ list. It will use the `LREM` command in order to remove the +message from the _processing_ list once the message has been processed. + +An additional client may monitor the _processing_ list for items that remain +there for too much time, and will push those timed out items into the queue +again if needed. + +## Pattern: Circular list + +Using `RPOPLPUSH` with the same source and destination key, a client can visit +all the elements of an N-elements list, one after the other, in O(N) without +transferring the full list from the server to the client using a single `LRANGE` +operation. + +The above pattern works even if the following two conditions: + +- There are multiple clients rotating the list: they'll fetch different + elements, until all the elements of the list are visited, and the process + restarts. +- Even if other clients are actively pushing new items at the end of the list. + +The above makes it very simple to implement a system where a set of items must +be processed by N workers continuously as fast as possible. An example is a +monitoring system that must check that a set of web sites are reachable, with +the smallest delay possible, using a number of parallel workers. + +Note that this implementation of workers is trivially scalable and reliable, +because even if a message is lost the item is still in the queue and will be +processed at the next iteration. diff --git a/iredis/data/commands/rpush.md b/iredis/data/commands/rpush.md new file mode 100644 index 0000000..14a796b --- /dev/null +++ b/iredis/data/commands/rpush.md @@ -0,0 +1,27 @@ +Insert all the specified values at the tail of the list stored at `key`. If +`key` does not exist, it is created as empty list before performing the push +operation. When `key` holds a value that is not a list, an error is returned. + +It is possible to push multiple elements using a single command call just +specifying multiple arguments at the end of the command. Elements are inserted +one after the other to the tail of the list, from the leftmost element to the +rightmost element. So for instance the command `RPUSH mylist a b c` will result +into a list containing `a` as first element, `b` as second element and `c` as +third element. + +@return + +@integer-reply: the length of the list after the push operation. + +@history + +- `>= 2.4`: Accepts multiple `element` arguments. In Redis versions older than + 2.4 it was possible to push a single value per command. + +@examples + +```cli +RPUSH mylist "hello" +RPUSH mylist "world" +LRANGE mylist 0 -1 +``` diff --git a/iredis/data/commands/rpushx.md b/iredis/data/commands/rpushx.md new file mode 100644 index 0000000..0345367 --- /dev/null +++ b/iredis/data/commands/rpushx.md @@ -0,0 +1,22 @@ +Inserts specified values at the tail of the list stored at `key`, only if `key` +already exists and holds a list. In contrary to `RPUSH`, no operation will be +performed when `key` does not yet exist. + +@return + +@integer-reply: the length of the list after the push operation. + +@history + +- `>= 4.0`: Accepts multiple `element` arguments. In Redis versions older than + 4.0 it was possible to push a single value per command. + +@examples + +```cli +RPUSH mylist "Hello" +RPUSHX mylist "World" +RPUSHX myotherlist "World" +LRANGE mylist 0 -1 +LRANGE myotherlist 0 -1 +``` diff --git a/iredis/data/commands/sadd.md b/iredis/data/commands/sadd.md new file mode 100644 index 0000000..a8b280e --- /dev/null +++ b/iredis/data/commands/sadd.md @@ -0,0 +1,24 @@ +Add the specified members to the set stored at `key`. Specified members that are +already a member of this set are ignored. If `key` does not exist, a new set is +created before adding the specified members. + +An error is returned when the value stored at `key` is not a set. + +@return + +@integer-reply: the number of elements that were added to the set, not including +all the elements already present into the set. + +@history + +- `>= 2.4`: Accepts multiple `member` arguments. Redis versions before 2.4 are + only able to add a single member per call. + +@examples + +```cli +SADD myset "Hello" +SADD myset "World" +SADD myset "World" +SMEMBERS myset +``` diff --git a/iredis/data/commands/save.md b/iredis/data/commands/save.md new file mode 100644 index 0000000..540dc7a --- /dev/null +++ b/iredis/data/commands/save.md @@ -0,0 +1,17 @@ +The `SAVE` commands performs a **synchronous** save of the dataset producing a +_point in time_ snapshot of all the data inside the Redis instance, in the form +of an RDB file. + +You almost never want to call `SAVE` in production environments where it will +block all the other clients. Instead usually `BGSAVE` is used. However in case +of issues preventing Redis to create the background saving child (for instance +errors in the fork(2) system call), the `SAVE` command can be a good last resort +to perform the dump of the latest dataset. + +Please refer to the [persistence documentation][tp] for detailed information. + +[tp]: /topics/persistence + +@return + +@simple-string-reply: The commands returns OK on success. diff --git a/iredis/data/commands/scan.md b/iredis/data/commands/scan.md new file mode 100644 index 0000000..f1674fd --- /dev/null +++ b/iredis/data/commands/scan.md @@ -0,0 +1,341 @@ +The `SCAN` command and the closely related commands `SSCAN`, `HSCAN` and `ZSCAN` +are used in order to incrementally iterate over a collection of elements. + +- `SCAN` iterates the set of keys in the currently selected Redis database. +- `SSCAN` iterates elements of Sets types. +- `HSCAN` iterates fields of Hash types and their associated values. +- `ZSCAN` iterates elements of Sorted Set types and their associated scores. + +Since these commands allow for incremental iteration, returning only a small +number of elements per call, they can be used in production without the downside +of commands like `KEYS` or `SMEMBERS` that may block the server for a long time +(even several seconds) when called against big collections of keys or elements. + +However while blocking commands like `SMEMBERS` are able to provide all the +elements that are part of a Set in a given moment, The SCAN family of commands +only offer limited guarantees about the returned elements since the collection +that we incrementally iterate can change during the iteration process. + +Note that `SCAN`, `SSCAN`, `HSCAN` and `ZSCAN` all work very similarly, so this +documentation covers all the four commands. However an obvious difference is +that in the case of `SSCAN`, `HSCAN` and `ZSCAN` the first argument is the name +of the key holding the Set, Hash or Sorted Set value. The `SCAN` command does +not need any key name argument as it iterates keys in the current database, so +the iterated object is the database itself. + +## SCAN basic usage + +SCAN is a cursor based iterator. This means that at every call of the command, +the server returns an updated cursor that the user needs to use as the cursor +argument in the next call. + +An iteration starts when the cursor is set to 0, and terminates when the cursor +returned by the server is 0. The following is an example of SCAN iteration: + +``` +redis 127.0.0.1:6379> scan 0 +1) "17" +2) 1) "key:12" + 2) "key:8" + 3) "key:4" + 4) "key:14" + 5) "key:16" + 6) "key:17" + 7) "key:15" + 8) "key:10" + 9) "key:3" + 10) "key:7" + 11) "key:1" +redis 127.0.0.1:6379> scan 17 +1) "0" +2) 1) "key:5" + 2) "key:18" + 3) "key:0" + 4) "key:2" + 5) "key:19" + 6) "key:13" + 7) "key:6" + 8) "key:9" + 9) "key:11" +``` + +In the example above, the first call uses zero as a cursor, to start the +iteration. The second call uses the cursor returned by the previous call as the +first element of the reply, that is, 17. + +As you can see the **SCAN return value** is an array of two values: the first +value is the new cursor to use in the next call, the second value is an array of +elements. + +Since in the second call the returned cursor is 0, the server signaled to the +caller that the iteration finished, and the collection was completely explored. +Starting an iteration with a cursor value of 0, and calling `SCAN` until the +returned cursor is 0 again is called a **full iteration**. + +## Scan guarantees + +The `SCAN` command, and the other commands in the `SCAN` family, are able to +provide to the user a set of guarantees associated to full iterations. + +- A full iteration always retrieves all the elements that were present in the + collection from the start to the end of a full iteration. This means that if a + given element is inside the collection when an iteration is started, and is + still there when an iteration terminates, then at some point `SCAN` returned + it to the user. +- A full iteration never returns any element that was NOT present in the + collection from the start to the end of a full iteration. So if an element was + removed before the start of an iteration, and is never added back to the + collection for all the time an iteration lasts, `SCAN` ensures that this + element will never be returned. + +However because `SCAN` has very little state associated (just the cursor) it has +the following drawbacks: + +- A given element may be returned multiple times. It is up to the application to + handle the case of duplicated elements, for example only using the returned + elements in order to perform operations that are safe when re-applied multiple + times. +- Elements that were not constantly present in the collection during a full + iteration, may be returned or not: it is undefined. + +## Number of elements returned at every SCAN call + +`SCAN` family functions do not guarantee that the number of elements returned +per call are in a given range. The commands are also allowed to return zero +elements, and the client should not consider the iteration complete as long as +the returned cursor is not zero. + +However the number of returned elements is reasonable, that is, in practical +terms SCAN may return a maximum number of elements in the order of a few tens of +elements when iterating a large collection, or may return all the elements of +the collection in a single call when the iterated collection is small enough to +be internally represented as an encoded data structure (this happens for small +sets, hashes and sorted sets). + +However there is a way for the user to tune the order of magnitude of the number +of returned elements per call using the **COUNT** option. + +## The COUNT option + +While `SCAN` does not provide guarantees about the number of elements returned +at every iteration, it is possible to empirically adjust the behavior of `SCAN` +using the **COUNT** option. Basically with COUNT the user specified the _amount +of work that should be done at every call in order to retrieve elements from the +collection_. This is **just a hint** for the implementation, however generally +speaking this is what you could expect most of the times from the +implementation. + +- The default COUNT value is 10. +- When iterating the key space, or a Set, Hash or Sorted Set that is big enough + to be represented by a hash table, assuming no **MATCH** option is used, the + server will usually return _count_ or a bit more than _count_ elements per + call. Please check the _why SCAN may return all the elements at once_ section + later in this document. +- When iterating Sets encoded as intsets (small sets composed of just integers), + or Hashes and Sorted Sets encoded as ziplists (small hashes and sets composed + of small individual values), usually all the elements are returned in the + first `SCAN` call regardless of the COUNT value. + +Important: **there is no need to use the same COUNT value** for every iteration. +The caller is free to change the count from one iteration to the other as +required, as long as the cursor passed in the next call is the one obtained in +the previous call to the command. + +## The MATCH option + +It is possible to only iterate elements matching a given glob-style pattern, +similarly to the behavior of the `KEYS` command that takes a pattern as only +argument. + +To do so, just append the `MATCH <pattern>` arguments at the end of the `SCAN` +command (it works with all the SCAN family commands). + +This is an example of iteration using **MATCH**: + +``` +redis 127.0.0.1:6379> sadd myset 1 2 3 foo foobar feelsgood +(integer) 6 +redis 127.0.0.1:6379> sscan myset 0 match f* +1) "0" +2) 1) "foo" + 2) "feelsgood" + 3) "foobar" +redis 127.0.0.1:6379> +``` + +It is important to note that the **MATCH** filter is applied after elements are +retrieved from the collection, just before returning data to the client. This +means that if the pattern matches very little elements inside the collection, +`SCAN` will likely return no elements in most iterations. An example is shown +below: + +``` +redis 127.0.0.1:6379> scan 0 MATCH *11* +1) "288" +2) 1) "key:911" +redis 127.0.0.1:6379> scan 288 MATCH *11* +1) "224" +2) (empty list or set) +redis 127.0.0.1:6379> scan 224 MATCH *11* +1) "80" +2) (empty list or set) +redis 127.0.0.1:6379> scan 80 MATCH *11* +1) "176" +2) (empty list or set) +redis 127.0.0.1:6379> scan 176 MATCH *11* COUNT 1000 +1) "0" +2) 1) "key:611" + 2) "key:711" + 3) "key:118" + 4) "key:117" + 5) "key:311" + 6) "key:112" + 7) "key:111" + 8) "key:110" + 9) "key:113" + 10) "key:211" + 11) "key:411" + 12) "key:115" + 13) "key:116" + 14) "key:114" + 15) "key:119" + 16) "key:811" + 17) "key:511" + 18) "key:11" +redis 127.0.0.1:6379> +``` + +As you can see most of the calls returned zero elements, but the last call where +a COUNT of 1000 was used in order to force the command to do more scanning for +that iteration. + +## The TYPE option + +As of version 6.0 you can use this option to ask `SCAN` to only return objects +that match a given `type`, allowing you to iterate through the database looking +for keys of a specific type. The **TYPE** option is only available on the +whole-database `SCAN`, not `HSCAN` or `ZSCAN` etc. + +The `type` argument is the same string name that the `TYPE` command returns. +Note a quirk where some Redis types, such as GeoHashes, HyperLogLogs, Bitmaps, +and Bitfields, may internally be implemented using other Redis types, such as a +string or zset, so can't be distinguished from other keys of that same type by +`SCAN`. For example, a ZSET and GEOHASH: + +``` +redis 127.0.0.1:6379> GEOADD geokey 0 0 value +(integer) 1 +redis 127.0.0.1:6379> ZADD zkey 1000 value +(integer) 1 +redis 127.0.0.1:6379> TYPE geokey +zset +redis 127.0.0.1:6379> TYPE zkey +zset +redis 127.0.0.1:6379> SCAN 0 TYPE zset +1) "0" +2) 1) "geokey" + 2) "zkey" +``` + +It is important to note that the **TYPE** filter is also applied after elements +are retrieved from the database, so the option does not reduce the amount of +work the server has to do to complete a full iteration, and for rare types you +may receive no elements in many iterations. + +## Multiple parallel iterations + +It is possible for an infinite number of clients to iterate the same collection +at the same time, as the full state of the iterator is in the cursor, that is +obtained and returned to the client at every call. Server side no state is taken +at all. + +## Terminating iterations in the middle + +Since there is no state server side, but the full state is captured by the +cursor, the caller is free to terminate an iteration half-way without signaling +this to the server in any way. An infinite number of iterations can be started +and never terminated without any issue. + +## Calling SCAN with a corrupted cursor + +Calling `SCAN` with a broken, negative, out of range, or otherwise invalid +cursor, will result into undefined behavior but never into a crash. What will be +undefined is that the guarantees about the returned elements can no longer be +ensured by the `SCAN` implementation. + +The only valid cursors to use are: + +- The cursor value of 0 when starting an iteration. +- The cursor returned by the previous call to SCAN in order to continue the + iteration. + +## Guarantee of termination + +The `SCAN` algorithm is guaranteed to terminate only if the size of the iterated +collection remains bounded to a given maximum size, otherwise iterating a +collection that always grows may result into `SCAN` to never terminate a full +iteration. + +This is easy to see intuitively: if the collection grows there is more and more +work to do in order to visit all the possible elements, and the ability to +terminate the iteration depends on the number of calls to `SCAN` and its COUNT +option value compared with the rate at which the collection grows. + +## Why SCAN may return all the items of an aggregate data type in a single call? + +In the `COUNT` option documentation, we state that sometimes this family of +commands may return all the elements of a Set, Hash or Sorted Set at once in a +single call, regardless of the `COUNT` option value. The reason why this happens +is that the cursor-based iterator can be implemented, and is useful, only when +the aggregate data type that we are scanning is represented as an hash table. +However Redis uses a [memory optimization](/topics/memory-optimization) where +small aggregate data types, until they reach a given amount of items or a given +max size of single elements, are represented using a compact single-allocation +packed encoding. When this is the case, `SCAN` has no meaningful cursor to +return, and must iterate the whole data structure at once, so the only sane +behavior it has is to return everything in a call. + +However once the data structures are bigger and are promoted to use real hash +tables, the `SCAN` family of commands will resort to the normal behavior. Note +that since this special behavior of returning all the elements is true only for +small aggregates, it has no effects on the command complexity or latency. +However the exact limits to get converted into real hash tables are +[user configurable](/topics/memory-optimization), so the maximum number of +elements you can see returned in a single call depends on how big an aggregate +data type could be and still use the packed representation. + +Also note that this behavior is specific of `SSCAN`, `HSCAN` and `ZSCAN`. `SCAN` +itself never shows this behavior because the key space is always represented by +hash tables. + +## Return value + +`SCAN`, `SSCAN`, `HSCAN` and `ZSCAN` return a two elements multi-bulk reply, +where the first element is a string representing an unsigned 64 bit number (the +cursor), and the second element is a multi-bulk with an array of elements. + +- `SCAN` array of elements is a list of keys. +- `SSCAN` array of elements is a list of Set members. +- `HSCAN` array of elements contain two elements, a field and a value, for every + returned element of the Hash. +- `ZSCAN` array of elements contain two elements, a member and its associated + score, for every returned element of the sorted set. + +@history + +- `>= 6.0`: Supports the `TYPE` subcommand. + +## Additional examples + +Iteration of a Hash value. + +``` +redis 127.0.0.1:6379> hmset hash name Jack age 33 +OK +redis 127.0.0.1:6379> hscan hash 0 +1) "0" +2) 1) "name" + 2) "Jack" + 3) "age" + 4) "33" +``` diff --git a/iredis/data/commands/scard.md b/iredis/data/commands/scard.md new file mode 100644 index 0000000..85d3c01 --- /dev/null +++ b/iredis/data/commands/scard.md @@ -0,0 +1,14 @@ +Returns the set cardinality (number of elements) of the set stored at `key`. + +@return + +@integer-reply: the cardinality (number of elements) of the set, or `0` if `key` +does not exist. + +@examples + +```cli +SADD myset "Hello" +SADD myset "World" +SCARD myset +``` diff --git a/iredis/data/commands/script-debug.md b/iredis/data/commands/script-debug.md new file mode 100644 index 0000000..67502b2 --- /dev/null +++ b/iredis/data/commands/script-debug.md @@ -0,0 +1,26 @@ +Set the debug mode for subsequent scripts executed with `EVAL`. Redis includes a +complete Lua debugger, codename LDB, that can be used to make the task of +writing complex scripts much simpler. In debug mode Redis acts as a remote +debugging server and a client, such as `redis-cli`, can execute scripts step by +step, set breakpoints, inspect variables and more - for additional information +about LDB refer to the [Redis Lua debugger](/topics/ldb) page. + +**Important note:** avoid debugging Lua scripts using your Redis production +server. Use a development server instead. + +LDB can be enabled in one of two modes: asynchronous or synchronous. In +asynchronous mode the server creates a forked debugging session that does not +block and all changes to the data are **rolled back** after the session +finishes, so debugging can be restarted using the same initial state. The +alternative synchronous debug mode blocks the server while the debugging session +is active and retains all changes to the data set once it ends. + +- `YES`. Enable non-blocking asynchronous debugging of Lua scripts (changes are + discarded). +- `SYNC`. Enable blocking synchronous debugging of Lua scripts (saves changes to + data). +- `NO`. Disables scripts debug mode. + +@return + +@simple-string-reply: `OK`. diff --git a/iredis/data/commands/script-exists.md b/iredis/data/commands/script-exists.md new file mode 100644 index 0000000..d27d771 --- /dev/null +++ b/iredis/data/commands/script-exists.md @@ -0,0 +1,18 @@ +Returns information about the existence of the scripts in the script cache. + +This command accepts one or more SHA1 digests and returns a list of ones or +zeros to signal if the scripts are already defined or not inside the script +cache. This can be useful before a pipelining operation to ensure that scripts +are loaded (and if not, to load them using `SCRIPT LOAD`) so that the pipelining +operation can be performed solely using `EVALSHA` instead of `EVAL` to save +bandwidth. + +Please refer to the `EVAL` documentation for detailed information about Redis +Lua scripting. + +@return + +@array-reply The command returns an array of integers that correspond to the +specified SHA1 digest arguments. For every corresponding SHA1 digest of a script +that actually exists in the script cache, an 1 is returned, otherwise 0 is +returned. diff --git a/iredis/data/commands/script-flush.md b/iredis/data/commands/script-flush.md new file mode 100644 index 0000000..833732d --- /dev/null +++ b/iredis/data/commands/script-flush.md @@ -0,0 +1,8 @@ +Flush the Lua scripts cache. + +Please refer to the `EVAL` documentation for detailed information about Redis +Lua scripting. + +@return + +@simple-string-reply diff --git a/iredis/data/commands/script-kill.md b/iredis/data/commands/script-kill.md new file mode 100644 index 0000000..225798b --- /dev/null +++ b/iredis/data/commands/script-kill.md @@ -0,0 +1,19 @@ +Kills the currently executing Lua script, assuming no write operation was yet +performed by the script. + +This command is mainly useful to kill a script that is running for too much +time(for instance because it entered an infinite loop because of a bug). The +script will be killed and the client currently blocked into EVAL will see the +command returning with an error. + +If the script already performed write operations it can not be killed in this +way because it would violate Lua script atomicity contract. In such a case only +`SHUTDOWN NOSAVE` is able to kill the script, killing the Redis process in an +hard way preventing it to persist with half-written information. + +Please refer to the `EVAL` documentation for detailed information about Redis +Lua scripting. + +@return + +@simple-string-reply diff --git a/iredis/data/commands/script-load.md b/iredis/data/commands/script-load.md new file mode 100644 index 0000000..839b247 --- /dev/null +++ b/iredis/data/commands/script-load.md @@ -0,0 +1,18 @@ +Load a script into the scripts cache, without executing it. After the specified +command is loaded into the script cache it will be callable using `EVALSHA` with +the correct SHA1 digest of the script, exactly like after the first successful +invocation of `EVAL`. + +The script is guaranteed to stay in the script cache forever (unless +`SCRIPT FLUSH` is called). + +The command works in the same way even if the script was already present in the +script cache. + +Please refer to the `EVAL` documentation for detailed information about Redis +Lua scripting. + +@return + +@bulk-string-reply This command returns the SHA1 digest of the script added into +the script cache. diff --git a/iredis/data/commands/sdiff.md b/iredis/data/commands/sdiff.md new file mode 100644 index 0000000..5d458ec --- /dev/null +++ b/iredis/data/commands/sdiff.md @@ -0,0 +1,29 @@ +Returns the members of the set resulting from the difference between the first +set and all the successive sets. + +For example: + +``` +key1 = {a,b,c,d} +key2 = {c} +key3 = {a,c,e} +SDIFF key1 key2 key3 = {b,d} +``` + +Keys that do not exist are considered to be empty sets. + +@return + +@array-reply: list with members of the resulting set. + +@examples + +```cli +SADD key1 "a" +SADD key1 "b" +SADD key1 "c" +SADD key2 "c" +SADD key2 "d" +SADD key2 "e" +SDIFF key1 key2 +``` diff --git a/iredis/data/commands/sdiffstore.md b/iredis/data/commands/sdiffstore.md new file mode 100644 index 0000000..e941016 --- /dev/null +++ b/iredis/data/commands/sdiffstore.md @@ -0,0 +1,21 @@ +This command is equal to `SDIFF`, but instead of returning the resulting set, it +is stored in `destination`. + +If `destination` already exists, it is overwritten. + +@return + +@integer-reply: the number of elements in the resulting set. + +@examples + +```cli +SADD key1 "a" +SADD key1 "b" +SADD key1 "c" +SADD key2 "c" +SADD key2 "d" +SADD key2 "e" +SDIFFSTORE key key1 key2 +SMEMBERS key +``` diff --git a/iredis/data/commands/select.md b/iredis/data/commands/select.md new file mode 100644 index 0000000..ff366c6 --- /dev/null +++ b/iredis/data/commands/select.md @@ -0,0 +1,27 @@ +Select the Redis logical database having the specified zero-based numeric index. +New connections always use the database 0. + +Selectable Redis databases are a form of namespacing: all databases are still +persisted in the same RDB / AOF file. However different databases can have keys +with the same name, and commands like `FLUSHDB`, `SWAPDB` or `RANDOMKEY` work on +specific databases. + +In practical terms, Redis databases should be used to separate different keys +belonging to the same application (if needed), and not to use a single Redis +instance for multiple unrelated applications. + +When using Redis Cluster, the `SELECT` command cannot be used, since Redis +Cluster only supports database zero. In the case of a Redis Cluster, having +multiple databases would be useless and an unnecessary source of complexity. +Commands operating atomically on a single database would not be possible with +the Redis Cluster design and goals. + +Since the currently selected database is a property of the connection, clients +should track the currently selected database and re-select it on reconnection. +While there is no command in order to query the selected database in the current +connection, the `CLIENT LIST` output shows, for each client, the currently +selected database. + +@return + +@simple-string-reply diff --git a/iredis/data/commands/set.md b/iredis/data/commands/set.md new file mode 100644 index 0000000..2cf4afa --- /dev/null +++ b/iredis/data/commands/set.md @@ -0,0 +1,73 @@ +Set `key` to hold the string `value`. If `key` already holds a value, it is +overwritten, regardless of its type. Any previous time to live associated with +the key is discarded on successful `SET` operation. + +## Options + +The `SET` command supports a set of options that modify its behavior: + +- `EX` _seconds_ -- Set the specified expire time, in seconds. +- `PX` _milliseconds_ -- Set the specified expire time, in milliseconds. +- `NX` -- Only set the key if it does not already exist. +- `XX` -- Only set the key if it already exist. +- `KEEPTTL` -- Retain the time to live associated with the key. + +Note: Since the `SET` command options can replace `SETNX`, `SETEX`, `PSETEX`, it +is possible that in future versions of Redis these three commands will be +deprecated and finally removed. + +@return + +@simple-string-reply: `OK` if `SET` was executed correctly. @nil-reply: a Null +Bulk Reply is returned if the `SET` operation was not performed because the user +specified the `NX` or `XX` option but the condition was not met. + +@history + +- `>= 2.6.12`: Added the `EX`, `PX`, `NX` and `XX` options. +- `>= 6.0`: Added the `KEEPTTL` option. + +@examples + +```cli +SET mykey "Hello" +GET mykey + +SET anotherkey "will expire in a minute" EX 60 +``` + +## Patterns + +**Note:** The following pattern is discouraged in favor of +[the Redlock algorithm](http://redis.io/topics/distlock) which is only a bit +more complex to implement, but offers better guarantees and is fault tolerant. + +The command `SET resource-name anystring NX EX max-lock-time` is a simple way to +implement a locking system with Redis. + +A client can acquire the lock if the above command returns `OK` (or retry after +some time if the command returns Nil), and remove the lock just using `DEL`. + +The lock will be auto-released after the expire time is reached. + +It is possible to make this system more robust modifying the unlock schema as +follows: + +- Instead of setting a fixed string, set a non-guessable large random string, + called token. +- Instead of releasing the lock with `DEL`, send a script that only removes the + key if the value matches. + +This avoids that a client will try to release the lock after the expire time +deleting the key created by another client that acquired the lock later. + +An example of unlock script would be similar to the following: + + if redis.call("get",KEYS[1]) == ARGV[1] + then + return redis.call("del",KEYS[1]) + else + return 0 + end + +The script should be called with `EVAL ...script... 1 resource-name token-value` diff --git a/iredis/data/commands/setbit.md b/iredis/data/commands/setbit.md new file mode 100644 index 0000000..a6b64f2 --- /dev/null +++ b/iredis/data/commands/setbit.md @@ -0,0 +1,158 @@ +Sets or clears the bit at _offset_ in the string value stored at _key_. + +The bit is either set or cleared depending on _value_, which can be either 0 +or 1. + +When _key_ does not exist, a new string value is created. The string is grown to +make sure it can hold a bit at _offset_. The _offset_ argument is required to be +greater than or equal to 0, and smaller than 2^32 (this limits bitmaps to +512MB). When the string at _key_ is grown, added bits are set to 0. + +**Warning**: When setting the last possible bit (_offset_ equal to 2^32 -1) and +the string value stored at _key_ does not yet hold a string value, or holds a +small string value, Redis needs to allocate all intermediate memory which can +block the server for some time. On a 2010 MacBook Pro, setting bit number 2^32 +-1 (512MB allocation) takes ~300ms, setting bit number 2^30 -1 (128MB +allocation) takes ~80ms, setting bit number 2^28 -1 (32MB allocation) takes +~30ms and setting bit number 2^26 -1 (8MB allocation) takes ~8ms. Note that once +this first allocation is done, subsequent calls to `SETBIT` for the same _key_ +will not have the allocation overhead. + +@return + +@integer-reply: the original bit value stored at _offset_. + +@examples + +```cli +SETBIT mykey 7 1 +SETBIT mykey 7 0 +GET mykey +``` + +## Pattern: accessing the entire bitmap + +There are cases when you need to set all the bits of single bitmap at once, for +example when initializing it to a default non-zero value. It is possible to do +this with multiple calls to the `SETBIT` command, one for each bit that needs to +be set. However, so as an optimization you can use a single `SET` command to set +the entire bitmap. + +Bitmaps are not an actual data type, but a set of bit-oriented operations +defined on the String type (for more information refer to the [Bitmaps section +of the Data Types Introduction page][ti]). This means that bitmaps can be used +with string commands, and most importantly with `SET` and `GET`. + +Because Redis' strings are binary-safe, a bitmap is trivially encoded as a bytes +stream. The first byte of the string corresponds to offsets 0..7 of the bitmap, +the second byte to the 8..15 range, and so forth. + +For example, after setting a few bits, getting the string value of the bitmap +would look like this: + +``` +> SETBIT bitmapsarestrings 2 1 +> SETBIT bitmapsarestrings 3 1 +> SETBIT bitmapsarestrings 5 1 +> SETBIT bitmapsarestrings 10 1 +> SETBIT bitmapsarestrings 11 1 +> SETBIT bitmapsarestrings 14 1 +> GET bitmapsarestrings +"42" +``` + +By getting the string representation of a bitmap, the client can then parse the +response's bytes by extracting the bit values using native bit operations in its +native programming language. Symmetrically, it is also possible to set an entire +bitmap by performing the bits-to-bytes encoding in the client and calling `SET` +with the resultant string. + +[ti]: /topics/data-types-intro#bitmaps + +## Pattern: setting multiple bits + +`SETBIT` excels at setting single bits, and can be called several times when +multiple bits need to be set. To optimize this operation you can replace +multiple `SETBIT` calls with a single call to the variadic `BITFIELD` command +and the use of fields of type `u1`. + +For example, the example above could be replaced by: + +``` +> BITFIELD bitsinabitmap SET u1 2 1 SET u1 3 1 SET u1 5 1 SET u1 10 1 SET u1 11 1 SET u1 14 1 +``` + +## Advanced Pattern: accessing bitmap ranges + +It is also possible to use the `GETRANGE` and `SETRANGE` string commands to +efficiently access a range of bit offsets in a bitmap. Below is a sample +implementation in idiomatic Redis Lua scripting that can be run with the `EVAL` +command: + +``` +--[[ +Sets a bitmap range + +Bitmaps are stored as Strings in Redis. A range spans one or more bytes, +so we can call `SETRANGE` when entire bytes need to be set instead of flipping +individual bits. Also, to avoid multiple internal memory allocations in +Redis, we traverse in reverse. +Expected input: + KEYS[1] - bitfield key + ARGV[1] - start offset (0-based, inclusive) + ARGV[2] - end offset (same, should be bigger than start, no error checking) + ARGV[3] - value (should be 0 or 1, no error checking) +]]-- + +-- A helper function to stringify a binary string to semi-binary format +local function tobits(str) + local r = '' + for i = 1, string.len(str) do + local c = string.byte(str, i) + local b = ' ' + for j = 0, 7 do + b = tostring(bit.band(c, 1)) .. b + c = bit.rshift(c, 1) + end + r = r .. b + end + return r +end + +-- Main +local k = KEYS[1] +local s, e, v = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3]) + +-- First treat the dangling bits in the last byte +local ms, me = s % 8, (e + 1) % 8 +if me > 0 then + local t = math.max(e - me + 1, s) + for i = e, t, -1 do + redis.call('SETBIT', k, i, v) + end + e = t +end + +-- Then the danglings in the first byte +if ms > 0 then + local t = math.min(s - ms + 7, e) + for i = s, t, 1 do + redis.call('SETBIT', k, i, v) + end + s = t + 1 +end + +-- Set a range accordingly, if at all +local rs, re = s / 8, (e + 1) / 8 +local rl = re - rs +if rl > 0 then + local b = '\255' + if 0 == v then + b = '\0' + end + redis.call('SETRANGE', k, rs, string.rep(b, rl)) +end +``` + +**Note:** the implementation for getting a range of bit offsets from a bitmap is +left as an exercise to the reader. diff --git a/iredis/data/commands/setex.md b/iredis/data/commands/setex.md new file mode 100644 index 0000000..6181b73 --- /dev/null +++ b/iredis/data/commands/setex.md @@ -0,0 +1,27 @@ +Set `key` to hold the string `value` and set `key` to timeout after a given +number of seconds. This command is equivalent to executing the following +commands: + +``` +SET mykey value +EXPIRE mykey seconds +``` + +`SETEX` is atomic, and can be reproduced by using the previous two commands +inside an `MULTI` / `EXEC` block. It is provided as a faster alternative to the +given sequence of operations, because this operation is very common when Redis +is used as a cache. + +An error is returned when `seconds` is invalid. + +@return + +@simple-string-reply + +@examples + +```cli +SETEX mykey 10 "Hello" +TTL mykey +GET mykey +``` diff --git a/iredis/data/commands/setnx.md b/iredis/data/commands/setnx.md new file mode 100644 index 0000000..889f10c --- /dev/null +++ b/iredis/data/commands/setnx.md @@ -0,0 +1,102 @@ +Set `key` to hold string `value` if `key` does not exist. In that case, it is +equal to `SET`. When `key` already holds a value, no operation is performed. +`SETNX` is short for "**SET** if **N**ot e**X**ists". + +@return + +@integer-reply, specifically: + +- `1` if the key was set +- `0` if the key was not set + +@examples + +```cli +SETNX mykey "Hello" +SETNX mykey "World" +GET mykey +``` + +## Design pattern: Locking with `!SETNX` + +**Please note that:** + +1. The following pattern is discouraged in favor of + [the Redlock algorithm](http://redis.io/topics/distlock) which is only a bit + more complex to implement, but offers better guarantees and is fault + tolerant. +2. We document the old pattern anyway because certain existing implementations + link to this page as a reference. Moreover it is an interesting example of + how Redis commands can be used in order to mount programming primitives. +3. Anyway even assuming a single-instance locking primitive, starting with + 2.6.12 it is possible to create a much simpler locking primitive, equivalent + to the one discussed here, using the `SET` command to acquire the lock, and a + simple Lua script to release the lock. The pattern is documented in the `SET` + command page. + +That said, `SETNX` can be used, and was historically used, as a locking +primitive. For example, to acquire the lock of the key `foo`, the client could +try the following: + +``` +SETNX lock.foo <current Unix time + lock timeout + 1> +``` + +If `SETNX` returns `1` the client acquired the lock, setting the `lock.foo` key +to the Unix time at which the lock should no longer be considered valid. The +client will later use `DEL lock.foo` in order to release the lock. + +If `SETNX` returns `0` the key is already locked by some other client. We can +either return to the caller if it's a non blocking lock, or enter a loop +retrying to hold the lock until we succeed or some kind of timeout expires. + +### Handling deadlocks + +In the above locking algorithm there is a problem: what happens if a client +fails, crashes, or is otherwise not able to release the lock? It's possible to +detect this condition because the lock key contains a UNIX timestamp. If such a +timestamp is equal to the current Unix time the lock is no longer valid. + +When this happens we can't just call `DEL` against the key to remove the lock +and then try to issue a `SETNX`, as there is a race condition here, when +multiple clients detected an expired lock and are trying to release it. + +- C1 and C2 read `lock.foo` to check the timestamp, because they both received + `0` after executing `SETNX`, as the lock is still held by C3 that crashed + after holding the lock. +- C1 sends `DEL lock.foo` +- C1 sends `SETNX lock.foo` and it succeeds +- C2 sends `DEL lock.foo` +- C2 sends `SETNX lock.foo` and it succeeds +- **ERROR**: both C1 and C2 acquired the lock because of the race condition. + +Fortunately, it's possible to avoid this issue using the following algorithm. +Let's see how C4, our sane client, uses the good algorithm: + +- C4 sends `SETNX lock.foo` in order to acquire the lock + +- The crashed client C3 still holds it, so Redis will reply with `0` to C4. + +- C4 sends `GET lock.foo` to check if the lock expired. If it is not, it will + sleep for some time and retry from the start. + +- Instead, if the lock is expired because the Unix time at `lock.foo` is older + than the current Unix time, C4 tries to perform: + + ``` + GETSET lock.foo <current Unix timestamp + lock timeout + 1> + ``` + +- Because of the `GETSET` semantic, C4 can check if the old value stored at + `key` is still an expired timestamp. If it is, the lock was acquired. + +- If another client, for instance C5, was faster than C4 and acquired the lock + with the `GETSET` operation, the C4 `GETSET` operation will return a non + expired timestamp. C4 will simply restart from the first step. Note that even + if C4 set the key a bit a few seconds in the future this is not a problem. + +In order to make this locking algorithm more robust, a client holding a lock +should always check the timeout didn't expire before unlocking the key with +`DEL` because client failures can be complex, not just crashing but also +blocking a lot of time against some operations and trying to issue `DEL` after a +lot of time (when the LOCK is already held by another client). diff --git a/iredis/data/commands/setrange.md b/iredis/data/commands/setrange.md new file mode 100644 index 0000000..078fb34 --- /dev/null +++ b/iredis/data/commands/setrange.md @@ -0,0 +1,47 @@ +Overwrites part of the string stored at _key_, starting at the specified offset, +for the entire length of _value_. If the offset is larger than the current +length of the string at _key_, the string is padded with zero-bytes to 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_. + +Note that the maximum offset that you can set is 2^29 -1 (536870911), as Redis +Strings are limited to 512 megabytes. If you need to grow beyond this size, you +can use multiple keys. + +**Warning**: When setting the last possible byte and the string value stored at +_key_ does not yet hold a string value, or holds a small string value, Redis +needs to allocate all intermediate memory which can block the server for some +time. On a 2010 MacBook Pro, setting byte number 536870911 (512MB allocation) +takes ~300ms, setting byte number 134217728 (128MB allocation) takes ~80ms, +setting bit number 33554432 (32MB allocation) takes ~30ms and setting bit number +8388608 (8MB allocation) takes ~8ms. Note that once this first allocation is +done, subsequent calls to `SETRANGE` for the same _key_ will not have the +allocation overhead. + +## Patterns + +Thanks to `SETRANGE` and the analogous `GETRANGE` commands, you can use Redis +strings as a linear array with O(1) random access. This is a very fast and +efficient storage in many real world use cases. + +@return + +@integer-reply: the length of the string after it was modified by the command. + +@examples + +Basic usage: + +```cli +SET key1 "Hello World" +SETRANGE key1 6 "Redis" +GET key1 +``` + +Example of zero padding: + +```cli +SETRANGE key2 6 "Redis" +GET key2 +``` diff --git a/iredis/data/commands/shutdown.md b/iredis/data/commands/shutdown.md new file mode 100644 index 0000000..cd48260 --- /dev/null +++ b/iredis/data/commands/shutdown.md @@ -0,0 +1,62 @@ +The command behavior is the following: + +- Stop all the clients. +- Perform a blocking SAVE if at least one **save point** is configured. +- Flush the Append Only File if AOF is enabled. +- Quit the server. + +If persistence is enabled this commands makes sure that Redis is switched off +without the lost of any data. This is not guaranteed if the client uses simply +`SAVE` and then `QUIT` because other clients may alter the DB data between the +two commands. + +Note: A Redis instance that is configured for not persisting on disk (no AOF +configured, nor "save" directive) will not dump the RDB file on `SHUTDOWN`, as +usually you don't want Redis instances used only for caching to block on when +shutting down. + +## SAVE and NOSAVE modifiers + +It is possible to specify an optional modifier to alter the behavior of the +command. Specifically: + +- **SHUTDOWN SAVE** will force a DB saving operation even if no save points are + configured. +- **SHUTDOWN NOSAVE** will prevent a DB saving operation even if one or more + save points are configured. (You can think of this variant as an hypothetical + **ABORT** command that just stops the server). + +## Conditions where a SHUTDOWN fails + +When the Append Only File is enabled the shutdown may fail because the system is +in a state that does not allow to safely immediately persist on disk. + +Normally if there is an AOF child process performing an AOF rewrite, Redis will +simply kill it and exit. However there are two conditions where it is unsafe to +do so, and the **SHUTDOWN** command will be refused with an error instead. This +happens when: + +- The user just turned on AOF, and the server triggered the first AOF rewrite in + order to create the initial AOF file. In this context, stopping will result in + losing the dataset at all: once restarted, the server will potentially have + AOF enabled without having any AOF file at all. +- A replica with AOF enabled, reconnected with its master, performed a full + resynchronization, and restarted the AOF file, triggering the initial AOF + creation process. In this case not completing the AOF rewrite is dangerous + because the latest dataset received from the master would be lost. The new + master can actually be even a different instance (if the **REPLICAOF** or + **SLAVEOF** command was used in order to reconfigure the replica), so it is + important to finish the AOF rewrite and start with the correct data set + representing the data set in memory when the server was terminated. + +There are conditions when we want just to terminate a Redis instance ASAP, +regardless of what its content is. In such a case, the right combination of +commands is to send a **CONFIG appendonly no** followed by a **SHUTDOWN +NOSAVE**. The first command will turn off the AOF if needed, and will terminate +the AOF rewriting child if there is one active. The second command will not have +any problem to execute since the AOF is no longer enabled. + +@return + +@simple-string-reply on error. On success nothing is returned since the server +quits and the connection is closed. diff --git a/iredis/data/commands/sinter.md b/iredis/data/commands/sinter.md new file mode 100644 index 0000000..e4ab023 --- /dev/null +++ b/iredis/data/commands/sinter.md @@ -0,0 +1,31 @@ +Returns the members of the set resulting from the intersection of all the given +sets. + +For example: + +``` +key1 = {a,b,c,d} +key2 = {c} +key3 = {a,c,e} +SINTER key1 key2 key3 = {c} +``` + +Keys that do not exist are considered to be empty sets. With one of the keys +being an empty set, the resulting set is also empty (since set intersection with +an empty set always results in an empty set). + +@return + +@array-reply: list with members of the resulting set. + +@examples + +```cli +SADD key1 "a" +SADD key1 "b" +SADD key1 "c" +SADD key2 "c" +SADD key2 "d" +SADD key2 "e" +SINTER key1 key2 +``` diff --git a/iredis/data/commands/sinterstore.md b/iredis/data/commands/sinterstore.md new file mode 100644 index 0000000..17dd0bf --- /dev/null +++ b/iredis/data/commands/sinterstore.md @@ -0,0 +1,21 @@ +This command is equal to `SINTER`, but instead of returning the resulting set, +it is stored in `destination`. + +If `destination` already exists, it is overwritten. + +@return + +@integer-reply: the number of elements in the resulting set. + +@examples + +```cli +SADD key1 "a" +SADD key1 "b" +SADD key1 "c" +SADD key2 "c" +SADD key2 "d" +SADD key2 "e" +SINTERSTORE key key1 key2 +SMEMBERS key +``` diff --git a/iredis/data/commands/sismember.md b/iredis/data/commands/sismember.md new file mode 100644 index 0000000..051f87d --- /dev/null +++ b/iredis/data/commands/sismember.md @@ -0,0 +1,16 @@ +Returns if `member` is a member of the set stored at `key`. + +@return + +@integer-reply, specifically: + +- `1` if the element is a member of the set. +- `0` if the element is not a member of the set, or if `key` does not exist. + +@examples + +```cli +SADD myset "one" +SISMEMBER myset "one" +SISMEMBER myset "two" +``` diff --git a/iredis/data/commands/slaveof.md b/iredis/data/commands/slaveof.md new file mode 100644 index 0000000..1250d43 --- /dev/null +++ b/iredis/data/commands/slaveof.md @@ -0,0 +1,25 @@ +**A note about the word slave used in this man page and command name**: Starting +with Redis 5 this command: starting with Redis version 5, if not for backward +compatibility, the Redis project no longer uses the word slave. Please use the +new command `REPLICAOF`. The command `SLAVEOF` will continue to work for +backward compatibility. + +The `SLAVEOF` command can change the replication settings of a replica on the +fly. If a Redis server is already acting as replica, the command `SLAVEOF` NO +ONE will turn off the replication, turning the Redis server into a MASTER. In +the proper form `SLAVEOF` hostname port will make the server a replica of +another server listening at the specified hostname and port. + +If a server is already a replica of some master, `SLAVEOF` hostname port will +stop the replication against the old server and start the synchronization +against the new one, discarding the old dataset. + +The form `SLAVEOF` NO ONE will stop replication, turning the server into a +MASTER, but will not discard the replication. So, if the old master stops +working, it is possible to turn the replica into a master and set the +application to use this new master in read/write. Later when the other Redis +server is fixed, it can be reconfigured to work as a replica. + +@return + +@simple-string-reply diff --git a/iredis/data/commands/slowlog.md b/iredis/data/commands/slowlog.md new file mode 100644 index 0000000..267a6bb --- /dev/null +++ b/iredis/data/commands/slowlog.md @@ -0,0 +1,84 @@ +This command is used in order to read and reset the Redis slow queries log. + +## Redis slow log overview + +The Redis Slow Log is a system to log queries that exceeded a specified +execution time. The execution time does not include I/O operations like talking +with the client, sending the reply and so forth, but just the time needed to +actually execute the command (this is the only stage of command execution where +the thread is blocked and can not serve other requests in the meantime). + +You can configure the slow log with two parameters: _slowlog-log-slower-than_ +tells Redis what is the execution time, in microseconds, to exceed in order for +the command to get logged. Note that a negative number disables the slow log, +while a value of zero forces the logging of every command. _slowlog-max-len_ is +the length of the slow log. The minimum value is zero. When a new command is +logged and the slow log is already at its maximum length, the oldest one is +removed from the queue of logged commands in order to make space. + +The configuration can be done by editing `redis.conf` or while the server is +running using the `CONFIG GET` and `CONFIG SET` commands. + +## Reading the slow log + +The slow log is accumulated in memory, so no file is written with information +about the slow command executions. This makes the slow log remarkably fast at +the point that you can enable the logging of all the commands (setting the +_slowlog-log-slower-than_ config parameter to zero) with minor performance hit. + +To read the slow log the **SLOWLOG GET** command is used, that returns every +entry in the slow log. It is possible to return only the N most recent entries +passing an additional argument to the command (for instance **SLOWLOG GET 10**). + +Note that you need a recent version of redis-cli in order to read the slow log +output, since it uses some features of the protocol that were not formerly +implemented in redis-cli (deeply nested multi bulk replies). + +## Output format + +``` +redis 127.0.0.1:6379> slowlog get 2 +1) 1) (integer) 14 + 2) (integer) 1309448221 + 3) (integer) 15 + 4) 1) "ping" +2) 1) (integer) 13 + 2) (integer) 1309448128 + 3) (integer) 30 + 4) 1) "slowlog" + 2) "get" + 3) "100" +``` + +There are also optional fields emitted only by Redis 4.0 or greater: + +``` +5) "127.0.0.1:58217" +6) "worker-123" +``` + +Every entry is composed of four (or six starting with Redis 4.0) fields: + +- A unique progressive identifier for every slow log entry. +- The unix timestamp at which the logged command was processed. +- The amount of time needed for its execution, in microseconds. +- The array composing the arguments of the command. +- Client IP address and port (4.0 only). +- Client name if set via the `CLIENT SETNAME` command (4.0 only). + +The entry's unique ID can be used in order to avoid processing slow log entries +multiple times (for instance you may have a script sending you an email alert +for every new slow log entry). + +The ID is never reset in the course of the Redis server execution, only a server +restart will reset it. + +## Obtaining the current length of the slow log + +It is possible to get just the length of the slow log using the command +**SLOWLOG LEN**. + +## Resetting the slow log. + +You can reset the slow log using the **SLOWLOG RESET** command. Once deleted the +information is lost forever. diff --git a/iredis/data/commands/smembers.md b/iredis/data/commands/smembers.md new file mode 100644 index 0000000..2272859 --- /dev/null +++ b/iredis/data/commands/smembers.md @@ -0,0 +1,15 @@ +Returns all the members of the set value stored at `key`. + +This has the same effect as running `SINTER` with one argument `key`. + +@return + +@array-reply: all elements of the set. + +@examples + +```cli +SADD myset "Hello" +SADD myset "World" +SMEMBERS myset +``` diff --git a/iredis/data/commands/smove.md b/iredis/data/commands/smove.md new file mode 100644 index 0000000..d8c12fa --- /dev/null +++ b/iredis/data/commands/smove.md @@ -0,0 +1,28 @@ +Move `member` from the set at `source` to the set at `destination`. This +operation is atomic. In every given moment the element will appear to be a +member of `source` **or** `destination` for other clients. + +If the source set does not exist or does not contain the specified element, no +operation is performed and `0` is returned. Otherwise, the element is removed +from the source set and added to the destination set. When the specified element +already exists in the destination set, it is only removed from the source set. + +An error is returned if `source` or `destination` does not hold a set value. + +@return + +@integer-reply, specifically: + +- `1` if the element is moved. +- `0` if the element is not a member of `source` and no operation was performed. + +@examples + +```cli +SADD myset "one" +SADD myset "two" +SADD myotherset "three" +SMOVE myset myotherset "two" +SMEMBERS myset +SMEMBERS myotherset +``` diff --git a/iredis/data/commands/sort.md b/iredis/data/commands/sort.md new file mode 100644 index 0000000..0703b5e --- /dev/null +++ b/iredis/data/commands/sort.md @@ -0,0 +1,136 @@ +Returns or stores the elements contained in the [list][tdtl], [set][tdts] or +[sorted set][tdtss] at `key`. By default, sorting is numeric and elements are +compared by their value interpreted as double precision floating point number. +This is `SORT` in its simplest form: + +[tdtl]: /topics/data-types#lists +[tdts]: /topics/data-types#set +[tdtss]: /topics/data-types#sorted-sets + +``` +SORT mylist +``` + +Assuming `mylist` is a list of numbers, this command will return the same list +with the elements sorted from small to large. In order to sort the numbers from +large to small, use the `!DESC` modifier: + +``` +SORT mylist DESC +``` + +When `mylist` contains string values and you want to sort them +lexicographically, use the `!ALPHA` modifier: + +``` +SORT mylist ALPHA +``` + +Redis is UTF-8 aware, assuming you correctly set the `!LC_COLLATE` environment +variable. + +The number of returned elements can be limited using the `!LIMIT` modifier. This +modifier takes the `offset` argument, specifying the number of elements to skip +and the `count` argument, specifying the number of elements to return from +starting at `offset`. The following example will return 10 elements of the +sorted version of `mylist`, starting at element 0 (`offset` is zero-based): + +``` +SORT mylist LIMIT 0 10 +``` + +Almost all modifiers can be used together. The following example will return the +first 5 elements, lexicographically sorted in descending order: + +``` +SORT mylist LIMIT 0 5 ALPHA DESC +``` + +## Sorting by external keys + +Sometimes you want to sort elements using external keys as weights to compare +instead of comparing the actual elements in the list, set or sorted set. Let's +say the list `mylist` contains the elements `1`, `2` and `3` representing unique +IDs of objects stored in `object_1`, `object_2` and `object_3`. When these +objects have associated weights stored in `weight_1`, `weight_2` and `weight_3`, +`SORT` can be instructed to use these weights to sort `mylist` with the +following statement: + +``` +SORT mylist BY weight_* +``` + +The `BY` option takes a pattern (equal to `weight_*` in this example) that is +used to generate the keys that are used for sorting. These key names are +obtained substituting the first occurrence of `*` with the actual value of the +element in the list (`1`, `2` and `3` in this example). + +## Skip sorting the elements + +The `!BY` option can also take a non-existent key, which causes `SORT` to skip +the sorting operation. This is useful if you want to retrieve external keys (see +the `!GET` option below) without the overhead of sorting. + +``` +SORT mylist BY nosort +``` + +## Retrieving external keys + +Our previous example returns just the sorted IDs. In some cases, it is more +useful to get the actual objects instead of their IDs (`object_1`, `object_2` +and `object_3`). Retrieving external keys based on the elements in a list, set +or sorted set can be done with the following command: + +``` +SORT mylist BY weight_* GET object_* +``` + +The `!GET` option can be used multiple times in order to get more keys for every +element of the original list, set or sorted set. + +It is also possible to `!GET` the element itself using the special pattern `#`: + +``` +SORT mylist BY weight_* GET object_* GET # +``` + +## Storing the result of a SORT operation + +By default, `SORT` returns the sorted elements to the client. With the `!STORE` +option, the result will be stored as a list at the specified key instead of +being returned to the client. + +``` +SORT mylist BY weight_* STORE resultkey +``` + +An interesting pattern using `SORT ... STORE` consists in associating an +`EXPIRE` timeout to the resulting key so that in applications where the result +of a `SORT` operation can be cached for some time. Other clients will use the +cached list instead of calling `SORT` for every request. When the key will +timeout, an updated version of the cache can be created by calling +`SORT ... STORE` again. + +Note that for correctly implementing this pattern it is important to avoid +multiple clients rebuilding the cache at the same time. Some kind of locking is +needed here (for instance using `SETNX`). + +## Using hashes in `!BY` and `!GET` + +It is possible to use `!BY` and `!GET` options against hash fields with the +following syntax: + +``` +SORT mylist BY weight_*->fieldname GET object_*->fieldname +``` + +The string `->` is used to separate the key name from the hash field name. The +key is substituted as documented above, and the hash stored at the resulting key +is accessed to retrieve the specified hash field. + +@return + +@array-reply: without passing the `store` option the command returns a list of +sorted elements. @integer-reply: when the `store` option is specified the +command returns the number of sorted elements in the destination list. diff --git a/iredis/data/commands/spop.md b/iredis/data/commands/spop.md new file mode 100644 index 0000000..76b1c89 --- /dev/null +++ b/iredis/data/commands/spop.md @@ -0,0 +1,41 @@ +Removes and returns one or more random elements from the set value store at +`key`. + +This operation is similar to `SRANDMEMBER`, that returns one or more random +elements from a set but does not remove it. + +The `count` argument is available since version 3.2. + +@return + +@bulk-string-reply: the removed element, or `nil` when `key` does not exist. + +@examples + +```cli +SADD myset "one" +SADD myset "two" +SADD myset "three" +SPOP myset +SMEMBERS myset +SADD myset "four" +SADD myset "five" +SPOP myset 3 +SMEMBERS myset +``` + +## Specification of the behavior when count is passed + +If count is bigger than the number of elements inside the Set, the command will +only return the whole set without additional elements. + +## Distribution of returned elements + +Note that this command is not suitable when you need a guaranteed uniform +distribution of the returned elements. For more information about the algorithms +used for SPOP, look up both the Knuth sampling and Floyd sampling algorithms. + +## Count argument extension + +Redis 3.2 introduced an optional `count` argument that can be passed to `SPOP` +in order to retrieve multiple elements in a single call. diff --git a/iredis/data/commands/srandmember.md b/iredis/data/commands/srandmember.md new file mode 100644 index 0000000..99f9b7e --- /dev/null +++ b/iredis/data/commands/srandmember.md @@ -0,0 +1,63 @@ +When called with just the `key` argument, return a random element from the set +value stored at `key`. + +Starting from Redis version 2.6, when called with the additional `count` +argument, return an array of `count` **distinct elements** if `count` is +positive. If called with a negative `count` the behavior changes and the command +is allowed to return the **same element multiple times**. In this case the +number of returned elements is the absolute value of the specified `count`. + +When called with just the key argument, the operation is similar to `SPOP`, +however while `SPOP` also removes the randomly selected element from the set, +`SRANDMEMBER` will just return a random element without altering the original +set in any way. + +@return + +@bulk-string-reply: without the additional `count` argument the command returns +a Bulk Reply with the randomly selected element, or `nil` when `key` does not +exist. @array-reply: when the additional `count` argument is passed the command +returns an array of elements, or an empty array when `key` does not exist. + +@examples + +```cli +SADD myset one two three +SRANDMEMBER myset +SRANDMEMBER myset 2 +SRANDMEMBER myset -5 +``` + +## Specification of the behavior when count is passed + +When a count argument is passed and is positive, the elements are returned as if +every selected element is removed from the set (like the extraction of numbers +in the game of Bingo). However elements are **not removed** from the Set. So +basically: + +- No repeated elements are returned. +- If count is bigger than the number of elements inside the Set, the command + will only return the whole set without additional elements. + +When instead the count is negative, the behavior changes and the extraction +happens as if you put the extracted element inside the bag again after every +extraction, so repeated elements are possible, and the number of elements +requested is always returned as we can repeat the same elements again and again, +with the exception of an empty Set (non existing key) that will always produce +an empty array as a result. + +## Distribution of returned elements + +The distribution of the returned elements is far from perfect when the number of +elements in the set is small, this is due to the fact that we used an +approximated random element function that does not really guarantees good +distribution. + +The algorithm used, that is implemented inside dict.c, samples the hash table +buckets to find a non-empty one. Once a non empty bucket is found, since we use +chaining in our hash table implementation, the number of elements inside the +bucket is checked and a random element is selected. + +This means that if you have two non-empty buckets in the entire hash table, and +one has three elements while one has just one, the element that is alone in its +bucket will be returned with much higher probability. diff --git a/iredis/data/commands/srem.md b/iredis/data/commands/srem.md new file mode 100644 index 0000000..6ead535 --- /dev/null +++ b/iredis/data/commands/srem.md @@ -0,0 +1,26 @@ +Remove the specified members from the set stored at `key`. Specified members +that are not a member of this set are ignored. If `key` does not exist, it is +treated as an empty set and this command returns `0`. + +An error is returned when the value stored at `key` is not a set. + +@return + +@integer-reply: the number of members that were removed from the set, not +including non existing members. + +@history + +- `>= 2.4`: Accepts multiple `member` arguments. Redis versions older than 2.4 + can only remove a set member per call. + +@examples + +```cli +SADD myset "one" +SADD myset "two" +SADD myset "three" +SREM myset "one" +SREM myset "four" +SMEMBERS myset +``` diff --git a/iredis/data/commands/sscan.md b/iredis/data/commands/sscan.md new file mode 100644 index 0000000..c19f3b1 --- /dev/null +++ b/iredis/data/commands/sscan.md @@ -0,0 +1 @@ +See `SCAN` for `SSCAN` documentation. diff --git a/iredis/data/commands/stralgo.md b/iredis/data/commands/stralgo.md new file mode 100644 index 0000000..ccd72fe --- /dev/null +++ b/iredis/data/commands/stralgo.md @@ -0,0 +1,121 @@ +The STRALGO implements complex algorithms that operate on strings. Right now the +only algorithm implemented is the LCS algorithm (longest common substring). +However new algorithms could be implemented in the future. The goal of this +command is to provide to Redis users algorithms that need fast implementations +and are normally not provided in the standard library of most programming +languages. + +The first argument of the command selects the algorithm to use, right now the +argument must be "LCS", since this is the only implemented one. + +## LCS algorithm + +``` +STRALGO LCS [KEYS ...] [STRINGS ...] [LEN] [IDX] [MINMATCHLEN <len>] [WITHMATCHLEN] +``` + +The LCS subcommand implements the longest common subsequence algorithm. Note +that this is different than the longest common string algorithm, since matching +characters in the string does not need to be contiguous. + +For instance the LCS between "foo" and "fao" is "fo", since scanning the two +strings from left to right, the longest common set of characters is composed of +the first "f" and then the "o". + +LCS is very useful in order to evaluate how similar two strings are. Strings can +represent many things. For instance if two strings are DNA sequences, the LCS +will provide a measure of similarity between the two DNA sequences. If the +strings represent some text edited by some user, the LCS could represent how +different the new text is compared to the old one, and so forth. + +Note that this algorithm runs in `O(N*M)` time, where N is the length of the +first string and M is the length of the second string. So either spin a +different Redis instance in order to run this algorithm, or make sure to run it +against very small strings. + +The basic usage is the following: + +``` +> STRALGO LCS STRINGS ohmytext mynewtext +"mytext" +``` + +It is also possible to compute the LCS between the content of two keys: + +``` +> MSET key1 ohmytext key2 mynewtext +OK +> STRALGO LCS KEYS key1 key2 +"mytext" +``` + +Sometimes we need just the length of the match: + +``` +> STRALGO LCS STRINGS ohmytext mynewtext LEN +6 +``` + +However what is often very useful, is to know the match position in each +strings: + +``` +> STRALGO LCS KEYS key1 key2 IDX +1) "matches" +2) 1) 1) 1) (integer) 4 + 2) (integer) 7 + 2) 1) (integer) 5 + 2) (integer) 8 + 2) 1) 1) (integer) 2 + 2) (integer) 3 + 2) 1) (integer) 0 + 2) (integer) 1 +3) "len" +4) (integer) 6 +``` + +Matches are produced from the last one to the first one, since this is how the +algorithm works, and it more efficient to emit things in the same order. The +above array means that the first match (second element of the array) is between +positions 2-3 of the first string and 0-1 of the second. Then there is another +match between 4-7 and 5-8. + +To restrict the list of matches to the ones of a given minimal length: + +``` +> STRALGO LCS KEYS key1 key2 IDX MINMATCHLEN 4 +1) "matches" +2) 1) 1) 1) (integer) 4 + 2) (integer) 7 + 2) 1) (integer) 5 + 2) (integer) 8 +3) "len" +4) (integer) 6 +``` + +Finally to also have the match len: + +``` +> STRALGO LCS KEYS key1 key2 IDX MINMATCHLEN 4 WITHMATCHLEN +1) "matches" +2) 1) 1) 1) (integer) 4 + 2) (integer) 7 + 2) 1) (integer) 5 + 2) (integer) 8 + 3) (integer) 4 +3) "len" +4) (integer) 6 +``` + +@return + +For the LCS algorithm: + +- Without modifiers the string representing the longest common substring is + returned. +- When LEN is given the command returns the length of the longest common + substring. +- When IDX is given the command returns an array with the LCS length and all the + ranges in both the strings, start and end offset for each string, where there + are matches. When WITHMATCHLEN is given each array representing a match will + also have the length of the match (see examples). diff --git a/iredis/data/commands/strlen.md b/iredis/data/commands/strlen.md new file mode 100644 index 0000000..99a9c55 --- /dev/null +++ b/iredis/data/commands/strlen.md @@ -0,0 +1,15 @@ +Returns the length of the string value stored at `key`. An error is returned +when `key` holds a non-string value. + +@return + +@integer-reply: the length of the string at `key`, or `0` when `key` does not +exist. + +@examples + +```cli +SET mykey "Hello world" +STRLEN mykey +STRLEN nonexisting +``` diff --git a/iredis/data/commands/subscribe.md b/iredis/data/commands/subscribe.md new file mode 100644 index 0000000..997670c --- /dev/null +++ b/iredis/data/commands/subscribe.md @@ -0,0 +1,5 @@ +Subscribes the client to the specified channels. + +Once the client enters the subscribed state it is not supposed to issue any +other commands, except for additional `SUBSCRIBE`, `PSUBSCRIBE`, `UNSUBSCRIBE`, +`PUNSUBSCRIBE`, `PING` and `QUIT` commands. diff --git a/iredis/data/commands/sunion.md b/iredis/data/commands/sunion.md new file mode 100644 index 0000000..2056468 --- /dev/null +++ b/iredis/data/commands/sunion.md @@ -0,0 +1,28 @@ +Returns the members of the set resulting from the union of all the given sets. + +For example: + +``` +key1 = {a,b,c,d} +key2 = {c} +key3 = {a,c,e} +SUNION key1 key2 key3 = {a,b,c,d,e} +``` + +Keys that do not exist are considered to be empty sets. + +@return + +@array-reply: list with members of the resulting set. + +@examples + +```cli +SADD key1 "a" +SADD key1 "b" +SADD key1 "c" +SADD key2 "c" +SADD key2 "d" +SADD key2 "e" +SUNION key1 key2 +``` diff --git a/iredis/data/commands/sunionstore.md b/iredis/data/commands/sunionstore.md new file mode 100644 index 0000000..716caf1 --- /dev/null +++ b/iredis/data/commands/sunionstore.md @@ -0,0 +1,21 @@ +This command is equal to `SUNION`, but instead of returning the resulting set, +it is stored in `destination`. + +If `destination` already exists, it is overwritten. + +@return + +@integer-reply: the number of elements in the resulting set. + +@examples + +```cli +SADD key1 "a" +SADD key1 "b" +SADD key1 "c" +SADD key2 "c" +SADD key2 "d" +SADD key2 "e" +SUNIONSTORE key key1 key2 +SMEMBERS key +``` diff --git a/iredis/data/commands/swapdb.md b/iredis/data/commands/swapdb.md new file mode 100644 index 0000000..708096a --- /dev/null +++ b/iredis/data/commands/swapdb.md @@ -0,0 +1,19 @@ +This command swaps two Redis databases, so that immediately all the clients +connected to a given database will see the data of the other database, and the +other way around. Example: + + SWAPDB 0 1 + +This will swap database 0 with database 1. All the clients connected with +database 0 will immediately see the new data, exactly like all the clients +connected with database 1 will see the data that was formerly of database 0. + +@return + +@simple-string-reply: `OK` if `SWAPDB` was executed correctly. + +@examples + +``` +SWAPDB 0 1 +``` diff --git a/iredis/data/commands/sync.md b/iredis/data/commands/sync.md new file mode 100644 index 0000000..48250b4 --- /dev/null +++ b/iredis/data/commands/sync.md @@ -0,0 +1,15 @@ +Initiates a replication stream from the master. + +The `SYNC` command is called by Redis replicas for initiating a replication +stream from the master. It has been replaced in newer versions of Redis by +`PSYNC`. + +For more information about replication in Redis please check the [replication +page][tr]. + +[tr]: /topics/replication + +@return + +**Non standard return value**, a bulk transfer of the data followed by `PING` +and write requests from the master. diff --git a/iredis/data/commands/time.md b/iredis/data/commands/time.md new file mode 100644 index 0000000..441376e --- /dev/null +++ b/iredis/data/commands/time.md @@ -0,0 +1,20 @@ +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 `gettimeofday` system +call. + +@return + +@array-reply, specifically: + +A multi bulk reply containing two elements: + +- unix time in seconds. +- microseconds. + +@examples + +```cli +TIME +TIME +``` diff --git a/iredis/data/commands/touch.md b/iredis/data/commands/touch.md new file mode 100644 index 0000000..eee3365 --- /dev/null +++ b/iredis/data/commands/touch.md @@ -0,0 +1,13 @@ +Alters the last access time of a key(s). A key is ignored if it does not exist. + +@return + +@integer-reply: The number of keys that were touched. + +@examples + +```cli +SET key1 "Hello" +SET key2 "World" +TOUCH key1 key2 +``` diff --git a/iredis/data/commands/ttl.md b/iredis/data/commands/ttl.md new file mode 100644 index 0000000..c36557c --- /dev/null +++ b/iredis/data/commands/ttl.md @@ -0,0 +1,27 @@ +Returns the remaining time to live of a key that has a timeout. This +introspection capability allows a Redis client to check how many seconds a given +key will continue to be part of the dataset. + +In Redis 2.6 or older the command returns `-1` if the key does not exist or if +the key exist but has no associated expire. + +Starting with Redis 2.8 the return value in case of error changed: + +- The command returns `-2` if the key does not exist. +- The command returns `-1` if the key exists but has no associated expire. + +See also the `PTTL` command that returns the same information with milliseconds +resolution (Only available in Redis 2.6 or greater). + +@return + +@integer-reply: TTL in seconds, or a negative value in order to signal an error +(see the description above). + +@examples + +```cli +SET mykey "Hello" +EXPIRE mykey 10 +TTL mykey +``` diff --git a/iredis/data/commands/type.md b/iredis/data/commands/type.md new file mode 100644 index 0000000..d27a7e8 --- /dev/null +++ b/iredis/data/commands/type.md @@ -0,0 +1,18 @@ +Returns the string representation of the type of the value stored at `key`. The +different types that can be returned are: `string`, `list`, `set`, `zset`, +`hash` and `stream`. + +@return + +@simple-string-reply: type of `key`, or `none` when `key` does not exist. + +@examples + +```cli +SET key1 "value" +LPUSH key2 "value" +SADD key3 "value" +TYPE key1 +TYPE key2 +TYPE key3 +``` diff --git a/iredis/data/commands/unlink.md b/iredis/data/commands/unlink.md new file mode 100644 index 0000000..e305440 --- /dev/null +++ b/iredis/data/commands/unlink.md @@ -0,0 +1,18 @@ +This command is very similar to `DEL`: it removes the specified keys. Just like +`DEL` a key is ignored if it does not exist. However the command performs the +actual memory reclaiming in a different thread, so it is not blocking, while +`DEL` is. This is where the command name comes from: the command just +**unlinks** the keys from the keyspace. The actual removal will happen later +asynchronously. + +@return + +@integer-reply: The number of keys that were unlinked. + +@examples + +```cli +SET key1 "Hello" +SET key2 "World" +UNLINK key1 key2 key3 +``` diff --git a/iredis/data/commands/unsubscribe.md b/iredis/data/commands/unsubscribe.md new file mode 100644 index 0000000..78c4d0c --- /dev/null +++ b/iredis/data/commands/unsubscribe.md @@ -0,0 +1,6 @@ +Unsubscribes the client from the given channels, or from all of them if none is +given. + +When no channels are specified, the client is unsubscribed from all the +previously subscribed channels. In this case, a message for every unsubscribed +channel will be sent to the client. diff --git a/iredis/data/commands/unwatch.md b/iredis/data/commands/unwatch.md new file mode 100644 index 0000000..b60bcb8 --- /dev/null +++ b/iredis/data/commands/unwatch.md @@ -0,0 +1,9 @@ +Flushes all the previously watched keys for a [transaction][tt]. + +[tt]: /topics/transactions + +If you call `EXEC` or `DISCARD`, there's no need to manually call `UNWATCH`. + +@return + +@simple-string-reply: always `OK`. diff --git a/iredis/data/commands/wait.md b/iredis/data/commands/wait.md new file mode 100644 index 0000000..e5a179c --- /dev/null +++ b/iredis/data/commands/wait.md @@ -0,0 +1,75 @@ +This command blocks the current client until all the previous write commands are +successfully transferred and acknowledged by at least the specified number of +replicas. If the timeout, specified in milliseconds, is reached, the command +returns even if the specified number of replicas were not yet reached. + +The command **will always return** the number of replicas that acknowledged the +write commands sent before the `WAIT` command, both in the case where the +specified number of replicas are reached, or when the timeout is reached. + +A few remarks: + +1. When `WAIT` returns, all the previous write commands sent in the context of + the current connection are guaranteed to be received by the number of + replicas returned by `WAIT`. +2. If the command is sent as part of a `MULTI` transaction, the command does not + block but instead just return ASAP the number of replicas that acknowledged + the previous write commands. +3. A timeout of 0 means to block forever. +4. Since `WAIT` returns the number of replicas reached both in case of failure + and success, the client should check that the returned value is equal or + greater to the replication level it demanded. + +## Consistency and WAIT + +Note that `WAIT` does not make Redis a strongly consistent store: while +synchronous replication is part of a replicated state machine, it is not the +only thing needed. However in the context of Sentinel or Redis Cluster failover, +`WAIT` improves the real world data safety. + +Specifically if a given write is transferred to one or more replicas, it is more +likely (but not guaranteed) that if the master fails, we'll be able to promote, +during a failover, a replica that received the write: both Sentinel and Redis +Cluster will do a best-effort attempt to promote the best replica among the set +of available replicas. + +However this is just a best-effort attempt so it is possible to still lose a +write synchronously replicated to multiple replicas. + +## Implementation details + +Since the introduction of partial resynchronization with replicas (PSYNC +feature) Redis replicas asynchronously ping their master with the offset they +already processed in the replication stream. This is used in multiple ways: + +1. Detect timed out replicas. +2. Perform a partial resynchronization after a disconnection. +3. Implement `WAIT`. + +In the specific case of the implementation of `WAIT`, Redis remembers, for each +client, the replication offset of the produced replication stream when a given +write command was executed in the context of a given client. When `WAIT` is +called Redis checks if the specified number of replicas already acknowledged +this offset or a greater one. + +@return + +@integer-reply: The command returns the number of replicas reached by all the +writes performed in the context of the current connection. + +@examples + +``` +> SET foo bar +OK +> WAIT 1 0 +(integer) 1 +> WAIT 2 1000 +(integer) 1 +``` + +In the following example the first call to `WAIT` does not use a timeout and +asks for the write to reach 1 replica. It returns with success. In the second +attempt instead we put a timeout, and ask for the replication of the write to +two replicas. Since there is a single replica available, after one second `WAIT` +unblocks and returns 1, the number of replicas reached. diff --git a/iredis/data/commands/watch.md b/iredis/data/commands/watch.md new file mode 100644 index 0000000..08f823f --- /dev/null +++ b/iredis/data/commands/watch.md @@ -0,0 +1,8 @@ +Marks the given keys to be watched for conditional execution of a +[transaction][tt]. + +[tt]: /topics/transactions + +@return + +@simple-string-reply: always `OK`. diff --git a/iredis/data/commands/xack.md b/iredis/data/commands/xack.md new file mode 100644 index 0000000..76b7c13 --- /dev/null +++ b/iredis/data/commands/xack.md @@ -0,0 +1,25 @@ +The `XACK` command removes one or multiple messages from the _pending entries +list_ (PEL) of a stream consumer group. A message is pending, and as such stored +inside the PEL, when it was delivered to some consumer, normally as a side +effect of calling `XREADGROUP`, or when a consumer took ownership of a message +calling `XCLAIM`. The pending message was delivered to some consumer but the +server is yet not sure it was processed at least once. So new calls to +`XREADGROUP` to grab the messages history for a consumer (for instance using an +ID of 0), will return such message. Similarly the pending message will be listed +by the `XPENDING` command, that inspects the PEL. + +Once a consumer _successfully_ processes a message, it should call `XACK` so +that such message does not get processed again, and as a side effect, the PEL +entry about this message is also purged, releasing memory from the Redis server. + +@return + +@integer-reply, specifically: + +The command returns the number of messages successfully acknowledged. Certain +message IDs may no longer be part of the PEL (for example because they have been +already acknowledge), and XACK will not count them as successfully acknowledged. + +```cli +XACK mystream mygroup 1526569495631-0 +``` diff --git a/iredis/data/commands/xadd.md b/iredis/data/commands/xadd.md new file mode 100644 index 0000000..d60b571 --- /dev/null +++ b/iredis/data/commands/xadd.md @@ -0,0 +1,87 @@ +Appends the specified stream entry to the stream at the specified key. If the +key does not exist, as a side effect of running this command the key is created +with a stream value. + +An entry is composed of a set of field-value pairs, it is basically a small +dictionary. The field-value pairs are stored in the same order they are given by +the user, and commands to read the stream such as `XRANGE` or `XREAD` are +guaranteed to return the fields and values exactly in the same order they were +added by `XADD`. + +`XADD` is the _only Redis command_ that can add data to a stream, but there are +other commands, such as `XDEL` and `XTRIM`, that are able to remove data from a +stream. + +## Specifying a Stream ID as an argument + +A stream entry ID identifies a given entry inside a stream. The `XADD` command +will auto-generate a unique ID for you if the ID argument specified is the `*` +character (asterisk ASCII character). However, while useful only in very rare +cases, it is possible to specify a well-formed ID, so that the new entry will be +added exactly with the specified ID. + +IDs are specified by two numbers separated by a `-` character: + + 1526919030474-55 + +Both quantities are 64-bit numbers. When an ID is auto-generated, the first part +is the Unix time in milliseconds of the Redis instance generating the ID. The +second part is just a sequence number and is used in order to distinguish IDs +generated in the same millisecond. + +IDs are guaranteed to be always incremental: If you compare the ID of the entry +just inserted it will be greater than any other past ID, so entries are totally +ordered inside a stream. In order to guarantee this property, if the current top +ID in the stream has a time greater than the current local time of the instance, +the top entry time will be used instead, and the sequence part of the ID +incremented. This may happen when, for instance, the local clock jumps backward, +or if after a failover the new master has a different absolute time. + +When a user specified an explicit ID to `XADD`, the minimum valid ID is `0-1`, +and the user _must_ specify an ID which is greater than any other ID currently +inside the stream, otherwise the command will fail. Usually resorting to +specific IDs is useful only if you have another system generating unique IDs +(for instance an SQL table) and you really want the Redis stream IDs to match +the one of this other system. + +## Capped streams + +It is possible to limit the size of the stream to a maximum number of elements +using the **MAXLEN** option. + +Trimming with **MAXLEN** can be expensive compared to just adding entries with +`XADD`: streams are represented by macro nodes into a radix tree, in order to be +very memory efficient. Altering the single macro node, consisting of a few tens +of elements, is not optimal. So it is possible to give the command in the +following special form: + + XADD mystream MAXLEN ~ 1000 * ... entry fields here ... + +The `~` argument between the **MAXLEN** option and the actual count means that +the user is not really requesting that the stream length is exactly 1000 items, +but instead it could be a few tens of entries more, but never less than 1000 +items. When this option modifier is used, the trimming is performed only when +Redis is able to remove a whole macro node. This makes it much more efficient, +and it is usually what you want. + +## Additional information about streams + +For further information about Redis streams please check our +[introduction to Redis Streams document](/topics/streams-intro). + +@return + +@bulk-string-reply, specifically: + +The command returns the ID of the added entry. The ID is the one auto-generated +if `*` is passed as ID argument, otherwise the command just returns the same ID +specified by the user during insertion. + +@examples + +```cli +XADD mystream * name Sara surname OConnor +XADD mystream * field1 value1 field2 value2 field3 value3 +XLEN mystream +XRANGE mystream - + +``` diff --git a/iredis/data/commands/xclaim.md b/iredis/data/commands/xclaim.md new file mode 100644 index 0000000..e6ee8c9 --- /dev/null +++ b/iredis/data/commands/xclaim.md @@ -0,0 +1,83 @@ +In the context of a stream consumer group, this command changes the ownership of +a pending message, so that the new owner is the consumer specified as the +command argument. Normally this is what happens: + +1. There is a stream with an associated consumer group. +2. Some consumer A reads a message via `XREADGROUP` from a stream, in the + context of that consumer group. +3. As a side effect a pending message entry is created in the pending entries + list (PEL) of the consumer group: it means the message was delivered to a + given consumer, but it was not yet acknowledged via `XACK`. +4. Then suddenly that consumer fails forever. +5. Other consumers may inspect the list of pending messages, that are stale for + quite some time, using the `XPENDING` command. In order to continue + processing such messages, they use `XCLAIM` to acquire the ownership of the + message and continue. + +This dynamic is clearly explained in the +[Stream intro documentation](/topics/streams-intro). + +Note that the message is claimed only if its idle time is greater the minimum +idle time we specify when calling `XCLAIM`. Because as a side effect `XCLAIM` +will also reset the idle time (since this is a new attempt at processing the +message), two consumers trying to claim a message at the same time will never +both succeed: only one will successfully claim the message. This avoids that we +process a given message multiple times in a trivial way (yet multiple processing +is possible and unavoidable in the general case). + +Moreover, as a side effect, `XCLAIM` will increment the count of attempted +deliveries of the message unless the `JUSTID` option has been specified (which +only delivers the message ID, not the message itself). In this way messages that +cannot be processed for some reason, for instance because the consumers crash +attempting to process them, will start to have a larger counter and can be +detected inside the system. + +## Command options + +The command has multiple options, however most are mainly for internal use in +order to transfer the effects of `XCLAIM` or other commands to the AOF file and +to propagate the same effects to the slaves, and are unlikely to be useful to +normal users: + +1. `IDLE <ms>`: Set the idle time (last time it was delivered) of the message. + If IDLE is not specified, an IDLE of 0 is assumed, that is, the time count is + reset because the message has now a new owner trying to process it. +2. `TIME <ms-unix-time>`: This is the same as IDLE but instead of a relative + amount of milliseconds, it sets the idle time to a specific Unix time (in + milliseconds). This is useful in order to rewrite the AOF file generating + `XCLAIM` commands. +3. `RETRYCOUNT <count>`: Set the retry counter to the specified value. This + counter is incremented every time a message is delivered again. Normally + `XCLAIM` does not alter this counter, which is just served to clients when + the XPENDING command is called: this way clients can detect anomalies, like + messages that are never processed for some reason after a big number of + delivery attempts. +4. `FORCE`: Creates the pending message entry in the PEL even if certain + specified IDs are not already in the PEL assigned to a different client. + However the message must be exist in the stream, otherwise the IDs of non + existing messages are ignored. +5. `JUSTID`: Return just an array of IDs of messages successfully claimed, + without returning the actual message. Using this option means the retry + counter is not incremented. + +@return + +@array-reply, specifically: + +The command returns all the messages successfully claimed, in the same format as +`XRANGE`. However if the `JUSTID` option was specified, only the message IDs are +reported, without including the actual message. + +Example: + +``` +> XCLAIM mystream mygroup Alice 3600000 1526569498055-0 +1) 1) 1526569498055-0 + 2) 1) "message" + 2) "orange" +``` + +In the above example we claim the message with ID `1526569498055-0`, only if the +message is idle for at least one hour without the original consumer or some +other consumer making progresses (acknowledging or claiming it), and assigns the +ownership to the consumer `Alice`. diff --git a/iredis/data/commands/xdel.md b/iredis/data/commands/xdel.md new file mode 100644 index 0000000..3f507a8 --- /dev/null +++ b/iredis/data/commands/xdel.md @@ -0,0 +1,51 @@ +Removes the specified entries from a stream, and returns the number of entries +deleted, that may be different from the number of IDs passed to the command in +case certain IDs do not exist. + +Normally you may think at a Redis stream as an append-only data structure, +however Redis streams are represented in memory, so we are able to also delete +entries. This may be useful, for instance, in order to comply with certain +privacy policies. + +# Understanding the low level details of entries deletion + +Redis streams are represented in a way that makes them memory efficient: a radix +tree is used in order to index macro-nodes that pack linearly tens of stream +entries. Normally what happens when you delete an entry from a stream is that +the entry is not _really_ evicted, it just gets marked as deleted. + +Eventually if all the entries in a macro-node are marked as deleted, the whole +node is destroyed and the memory reclaimed. This means that if you delete a +large amount of entries from a stream, for instance more than 50% of the entries +appended to the stream, the memory usage per entry may increment, since what +happens is that the stream will start to be fragmented. However the stream +performances will remain the same. + +In future versions of Redis it is possible that we'll trigger a node garbage +collection in case a given macro-node reaches a given amount of deleted entries. +Currently with the usage we anticipate for this data structure, it is not a good +idea to add such complexity. + +@return + +@integer-reply: the number of entries actually deleted. + +@examples + +``` +> XADD mystream * a 1 +1538561698944-0 +> XADD mystream * b 2 +1538561700640-0 +> XADD mystream * c 3 +1538561701744-0 +> XDEL mystream 1538561700640-0 +(integer) 1 +127.0.0.1:6379> XRANGE mystream - + +1) 1) 1538561698944-0 + 2) 1) "a" + 2) "1" +2) 1) 1538561701744-0 + 2) 1) "c" + 2) "3" +``` diff --git a/iredis/data/commands/xgroup.md b/iredis/data/commands/xgroup.md new file mode 100644 index 0000000..b690d87 --- /dev/null +++ b/iredis/data/commands/xgroup.md @@ -0,0 +1,64 @@ +This command is used in order to manage the consumer groups associated with a +stream data structure. Using `XGROUP` you can: + +- Create a new consumer group associated with a stream. +- Destroy a consumer group. +- Remove a specific consumer from a consumer group. +- Set the consumer group _last delivered ID_ to something else. + +To create a new consumer group, use the following form: + + XGROUP CREATE mystream consumer-group-name $ + +The last argument is the ID of the last item in the stream to consider already +delivered. In the above case we used the special ID '\$' (that means: the ID of +the last item in the stream). In this case the consumers fetching data from that +consumer group will only see new elements arriving in the stream. + +If instead you want consumers to fetch the whole stream history, use zero as the +starting ID for the consumer group: + + XGROUP CREATE mystream consumer-group-name 0 + +Of course it is also possible to use any other valid ID. If the specified +consumer group already exists, the command returns a `-BUSYGROUP` error. +Otherwise the operation is performed and OK is returned. There are no hard +limits to the number of consumer groups you can associate to a given stream. + +If the specified stream doesn't exist when creating a group, an error will be +returned. You can use the optional `MKSTREAM` subcommand as the last argument +after the `ID` to automatically create the stream, if it doesn't exist. Note +that if the stream is created in this way it will have a length of 0: + + XGROUP CREATE mystream consumer-group-name $ MKSTREAM + +A consumer group can be destroyed completely by using the following form: + + XGROUP DESTROY mystream consumer-group-name + +The consumer group will be destroyed even if there are active consumers and +pending messages, so make sure to call this command only when really needed. + +To just remove a given consumer from a consumer group, the following form is +used: + + XGROUP DELCONSUMER mystream consumer-group-name myconsumer123 + +Consumers in a consumer group are auto-created every time a new consumer name is +mentioned by some command. However sometimes it may be useful to remove old +consumers since they are no longer used. This form returns the number of pending +messages that the consumer had before it was deleted. + +Finally it possible to set the next message to deliver using the `SETID` +subcommand. Normally the next ID is set when the consumer is created, as the +last argument of `XGROUP CREATE`. However using this form the next ID can be +modified later without deleting and creating the consumer group again. For +instance if you want the consumers in a consumer group to re-process all the +messages in a stream, you may want to set its next ID to 0: + + XGROUP SETID mystream consumer-group-name 0 + +Finally to get some help if you don't remember the syntax, use the HELP +subcommand: + + XGROUP HELP diff --git a/iredis/data/commands/xinfo.md b/iredis/data/commands/xinfo.md new file mode 100644 index 0000000..b9c228d --- /dev/null +++ b/iredis/data/commands/xinfo.md @@ -0,0 +1,182 @@ +This is an introspection command used in order to retrieve different information +about the streams and associated consumer groups. Three forms are possible: + +- `XINFO STREAM <key>` + +In this form the command returns general information about the stream stored at +the specified key. + +``` +> XINFO STREAM mystream + 1) length + 2) (integer) 2 + 3) radix-tree-keys + 4) (integer) 1 + 5) radix-tree-nodes + 6) (integer) 2 + 7) groups + 8) (integer) 2 + 9) last-generated-id +10) 1538385846314-0 +11) first-entry +12) 1) 1538385820729-0 + 2) 1) "foo" + 2) "bar" +13) last-entry +14) 1) 1538385846314-0 + 2) 1) "field" + 2) "value" +``` + +In the above example you can see that the reported information are the number of +elements of the stream, details about the radix tree representing the stream +mostly useful for optimization and debugging tasks, the number of consumer +groups associated with the stream, the last generated ID that may not be the +same as the last entry ID in case some entry was deleted. Finally the full first +and last entry in the stream are shown, in order to give some sense about what +is the stream content. + +- `XINFO STREAM <key> FULL [COUNT <count>]` + +In this form the command returns the entire state of the stream, including +entries, groups, consumers and PELs. This form is available since Redis 6.0. + +``` +> XADD mystream * foo bar +"1588152471065-0" +> XADD mystream * foo bar2 +"1588152473531-0" +> XGROUP CREATE mystream mygroup 0-0 +OK +> XREADGROUP GROUP mygroup Alice COUNT 1 STREAMS mystream > +1) 1) "mystream" + 2) 1) 1) "1588152471065-0" + 2) 1) "foo" + 2) "bar" +> XINFO STREAM mystream FULL + 1) "length" + 2) (integer) 2 + 3) "radix-tree-keys" + 4) (integer) 1 + 5) "radix-tree-nodes" + 6) (integer) 2 + 7) "last-generated-id" + 8) "1588152473531-0" + 9) "entries" +10) 1) 1) "1588152471065-0" + 2) 1) "foo" + 2) "bar" + 2) 1) "1588152473531-0" + 2) 1) "foo" + 2) "bar2" +11) "groups" +12) 1) 1) "name" + 2) "mygroup" + 3) "last-delivered-id" + 4) "1588152471065-0" + 5) "pel-count" + 6) (integer) 1 + 7) "pending" + 8) 1) 1) "1588152471065-0" + 2) "Alice" + 3) (integer) 1588152520299 + 4) (integer) 1 + 9) "consumers" + 10) 1) 1) "name" + 2) "Alice" + 3) "seen-time" + 4) (integer) 1588152520299 + 5) "pel-count" + 6) (integer) 1 + 7) "pending" + 8) 1) 1) "1588152471065-0" + 2) (integer) 1588152520299 + 3) (integer) 1 +``` + +The reported information contains all of the fields reported by the simple form +of `XINFO STREAM`, with some additional information: + +1. Stream entries are returned, including fields and values. +2. Groups, consumers and PELs are returned. + +The `COUNT` option is used to limit the amount of stream/PEL entries that are +returned (The first `<count>` entries are returned). The default `COUNT` is 10 +and a `COUNT` of 0 means that all entries will be returned (Execution time may +be long if the stream has a lot of entries) + +- `XINFO GROUPS <key>` + +In this form we just get as output all the consumer groups associated with the +stream: + +``` +> XINFO GROUPS mystream +1) 1) name + 2) "mygroup" + 3) consumers + 4) (integer) 2 + 5) pending + 6) (integer) 2 + 7) last-delivered-id + 8) "1588152489012-0" +2) 1) name + 2) "some-other-group" + 3) consumers + 4) (integer) 1 + 5) pending + 6) (integer) 0 + 7) last-delivered-id + 8) "1588152498034-0" +``` + +For each consumer group listed the command also shows the number of consumers +known in that group and the pending messages (delivered but not yet +acknowledged) in that group. + +- `XINFO CONSUMERS <key> <group>` + +Finally it is possible to get the list of every consumer in a specific consumer +group: + +``` +> XINFO CONSUMERS mystream mygroup +1) 1) name + 2) "Alice" + 3) pending + 4) (integer) 1 + 5) idle + 6) (integer) 9104628 +2) 1) name + 2) "Bob" + 3) pending + 4) (integer) 1 + 5) idle + 6) (integer) 83841983 +``` + +We can see the idle time in milliseconds (last field) together with the consumer +name and the number of pending messages for this specific consumer. + +**Note that you should not rely on the fields exact position**, nor on the +number of fields, new fields may be added in the future. So a well behaving +client should fetch the whole list, and report it to the user, for example, as a +dictionary data structure. Low level clients such as C clients where the items +will likely be reported back in a linear array should document that the order is +undefined. + +Finally it is possible to get help from the command, in case the user can't +remember the exact syntax, by using the `HELP` subcommand: + +``` +> XINFO HELP +1) XINFO <subcommand> arg arg ... arg. Subcommands are: +2) CONSUMERS <key> <groupname> -- Show consumer groups of group <groupname>. +3) GROUPS <key> -- Show the stream consumer groups. +4) STREAM <key> -- Show information about the stream. +5) HELP +``` + +@history + +- `>= 6.0.0`: Added the `FULL` option to `XINFO STREAM`. diff --git a/iredis/data/commands/xlen.md b/iredis/data/commands/xlen.md new file mode 100644 index 0000000..5506449 --- /dev/null +++ b/iredis/data/commands/xlen.md @@ -0,0 +1,21 @@ +Returns the number of entries inside a stream. If the specified key does not +exist the command returns zero, as if the stream was empty. However note that +unlike other Redis types, zero-length streams are possible, so you should call +`TYPE` or `EXISTS` in order to check if a key exists or not. + +Streams are not auto-deleted once they have no entries inside (for instance +after an `XDEL` call), because the stream may have consumer groups associated +with it. + +@return + +@integer-reply: the number of entries of the stream at `key`. + +@examples + +```cli +XADD mystream * item 1 +XADD mystream * item 2 +XADD mystream * item 3 +XLEN mystream +``` diff --git a/iredis/data/commands/xpending.md b/iredis/data/commands/xpending.md new file mode 100644 index 0000000..53e3f33 --- /dev/null +++ b/iredis/data/commands/xpending.md @@ -0,0 +1,110 @@ +Fetching data from a stream via a consumer group, and not acknowledging such +data, has the effect of creating _pending entries_. This is well explained in +the `XREADGROUP` command, and even better in our +[introduction to Redis Streams](/topics/streams-intro). The `XACK` command will +immediately remove the pending entry from the Pending Entry List (PEL) since +once a message is successfully processed, there is no longer need for the +consumer group to track it and to remember the current owner of the message. + +The `XPENDING` command is the interface to inspect the list of pending messages, +and is as thus a very important command in order to observe and understand what +is happening with a streams consumer groups: what clients are active, what +messages are pending to be consumed, or to see if there are idle messages. +Moreover this command, together with `XCLAIM` is used in order to implement +recovering of consumers that are failing for a long time, and as a result +certain messages are not processed: a different consumer can claim the message +and continue. This is better explained in the +[streams intro](/topics/streams-intro) and in the `XCLAIM` command page, and is +not covered here. + +## Summary form of XPENDING + +When `XPENDING` is called with just a key name and a consumer group name, it +just outputs a summary about the pending messages in a given consumer group. In +the following example, we create a consumer group and immediately create a +pending message by reading from the group with `XREADGROUP`. + +``` +> XGROUP CREATE mystream group55 0-0 +OK + +> XREADGROUP GROUP group55 consumer-123 COUNT 1 STREAMS mystream > +1) 1) "mystream" + 2) 1) 1) 1526984818136-0 + 2) 1) "duration" + 2) "1532" + 3) "event-id" + 4) "5" + 5) "user-id" + 6) "7782813" +``` + +We expect the pending entries list for the consumer group `group55` to have a +message right now: consumer named `consumer-123` fetched the message without +acknowledging its processing. The simple `XPENDING` form will give us this +information: + +``` +> XPENDING mystream group55 +1) (integer) 1 +2) 1526984818136-0 +3) 1526984818136-0 +4) 1) 1) "consumer-123" + 2) "1" +``` + +In this form, the command outputs the total number of pending messages for this +consumer group, which is one, followed by the smallest and greatest ID among the +pending messages, and then list every consumer in the consumer group with at +least one pending message, and the number of pending messages it has. + +This is a good overview, but sometimes we are interested in the details. In +order to see all the pending messages with more associated information we need +to also pass a range of IDs, in a similar way we do it with `XRANGE`, and a non +optional _count_ argument, to limit the number of messages returned per call: + +``` +> XPENDING mystream group55 - + 10 +1) 1) 1526984818136-0 + 2) "consumer-123" + 3) (integer) 196415 + 4) (integer) 1 +``` + +In the extended form we no longer see the summary information, instead there are +detailed information for each message in the pending entries list. For each +message four attributes are returned: + +1. The ID of the message. +2. The name of the consumer that fetched the message and has still to + acknowledge it. We call it the current _owner_ of the message. +3. The number of milliseconds that elapsed since the last time this message was + delivered to this consumer. +4. The number of times this message was delivered. + +The deliveries counter, that is the fourth element in the array, is incremented +when some other consumer _claims_ the message with `XCLAIM`, or when the message +is delivered again via `XREADGROUP`, when accessing the history of a consumer in +a consumer group (see the `XREADGROUP` page for more info). + +Finally it is possible to pass an additional argument to the command, in order +to see the messages having a specific owner: + +``` +> XPENDING mystream group55 - + 10 consumer-123 +``` + +But in the above case the output would be the same, since we have pending +messages only for a single consumer. However what is important to keep in mind +is that this operation, filtering by a specific consumer, is not inefficient +even when there are many pending messages from many consumers: we have a pending +entries list data structure both globally, and for every consumer, so we can +very efficiently show just messages pending for a single consumer. + +@return + +@array-reply, specifically: + +The command returns data in different format depending on the way it is called, +as previously explained in this page. However the reply is always an array of +items. diff --git a/iredis/data/commands/xrange.md b/iredis/data/commands/xrange.md new file mode 100644 index 0000000..b9d5ab0 --- /dev/null +++ b/iredis/data/commands/xrange.md @@ -0,0 +1,183 @@ +The command returns the stream entries matching a given range of IDs. The range +is specified by a minimum and maximum ID. All the entries having an ID between +the two specified or exactly one of the two IDs specified (closed interval) are +returned. + +The `XRANGE` command has a number of applications: + +- Returning items in a specific time range. This is possible because Stream IDs + are [related to time](/topics/streams-intro). +- Iterating a stream incrementally, returning just a few items at every + iteration. However it is semantically much more robust than the `SCAN` family + of functions. +- Fetching a single entry from a stream, providing the ID of the entry to fetch + two times: as start and end of the query interval. + +The command also has a reciprocal command returning items in the reverse order, +called `XREVRANGE`, which is otherwise identical. + +## `-` and `+` special IDs + +The `-` and `+` special IDs mean respectively the minimum ID possible and the +maximum ID possible inside a stream, so the following command will just return +every entry in the stream: + +``` +> XRANGE somestream - + +1) 1) 1526985054069-0 + 2) 1) "duration" + 2) "72" + 3) "event-id" + 4) "9" + 5) "user-id" + 6) "839248" +2) 1) 1526985069902-0 + 2) 1) "duration" + 2) "415" + 3) "event-id" + 4) "2" + 5) "user-id" + 6) "772213" +... other entries here ... +``` + +The `-` ID is effectively just exactly as specifying `0-0`, while `+` is +equivalent to `18446744073709551615-18446744073709551615`, however they are +nicer to type. + +## Incomplete IDs + +Stream IDs are composed of two parts, a Unix millisecond time stamp and a +sequence number for entries inserted in the same millisecond. It is possible to +use `XRANGE` specifying just the first part of the ID, the millisecond time, +like in the following example: + +``` +> XRANGE somestream 1526985054069 1526985055069 +``` + +In this case, `XRANGE` will auto-complete the start interval with `-0` and end +interval with `-18446744073709551615`, in order to return all the entries that +were generated between a given millisecond and the end of the other specified +millisecond. This also means that repeating the same millisecond two times, we +get all the entries within such millisecond, because the sequence number range +will be from zero to the maximum. + +Used in this way `XRANGE` works as a range query command to obtain entries in a +specified time. This is very handy in order to access the history of past events +in a stream. + +## Returning a maximum number of entries + +Using the **COUNT** option it is possible to reduce the number of entries +reported. This is a very important feature even if it may look marginal, because +it allows, for instance, to model operations such as _give me the entry greater +or equal to the following_: + +``` +> XRANGE somestream 1526985054069-0 + COUNT 1 +1) 1) 1526985054069-0 + 2) 1) "duration" + 2) "72" + 3) "event-id" + 4) "9" + 5) "user-id" + 6) "839248" +``` + +In the above case the entry `1526985054069-0` exists, otherwise the server would +have sent us the next one. Using `COUNT` is also the base in order to use +`XRANGE` as an iterator. + +## Iterating a stream + +In order to iterate a stream, we can proceed as follows. Let's assume that we +want two elements per iteration. We start fetching the first two elements, which +is trivial: + +``` +> XRANGE writers - + COUNT 2 +1) 1) 1526985676425-0 + 2) 1) "name" + 2) "Virginia" + 3) "surname" + 4) "Woolf" +2) 1) 1526985685298-0 + 2) 1) "name" + 2) "Jane" + 3) "surname" + 4) "Austen" +``` + +Then instead of starting the iteration again from `-`, as the start of the range +we use the entry ID of the _last_ entry returned by the previous `XRANGE` call, +adding the sequence part of the ID by one. + +The ID of the last entry is `1526985685298-0`, so we just add 1 to the sequence +to obtain `1526985685298-1`, and continue our iteration: + +``` +> XRANGE writers 1526985685298-1 + COUNT 2 +1) 1) 1526985691746-0 + 2) 1) "name" + 2) "Toni" + 3) "surname" + 4) "Morrison" +2) 1) 1526985712947-0 + 2) 1) "name" + 2) "Agatha" + 3) "surname" + 4) "Christie" +``` + +And so forth. Eventually this will allow to visit all the entries in the stream. +Obviously, we can start the iteration from any ID, or even from a specific time, +by providing a given incomplete start ID. Moreover, we can limit the iteration +to a given ID or time, by providing an end ID or incomplete ID instead of `+`. + +The command `XREAD` is also able to iterate the stream. The command `XREVRANGE` +can iterate the stream reverse, from higher IDs (or times) to lower IDs (or +times). + +## Fetching single items + +If you look for an `XGET` command you'll be disappointed because `XRANGE` is +effectively the way to go in order to fetch a single entry from a stream. All +you have to do is to specify the ID two times in the arguments of XRANGE: + +``` +> XRANGE mystream 1526984818136-0 1526984818136-0 +1) 1) 1526984818136-0 + 2) 1) "duration" + 2) "1532" + 3) "event-id" + 4) "5" + 5) "user-id" + 6) "7782813" +``` + +## Additional information about streams + +For further information about Redis streams please check our +[introduction to Redis Streams document](/topics/streams-intro). + +@return + +@array-reply, specifically: + +The command returns the entries with IDs matching the specified range. The +returned entries are complete, that means that the ID and all the fields they +are composed are returned. Moreover, the entries are returned with their fields +and values in the exact same order as `XADD` added them. + +@examples + +```cli +XADD writers * name Virginia surname Woolf +XADD writers * name Jane surname Austen +XADD writers * name Toni surname Morrison +XADD writers * name Agatha surname Christie +XADD writers * name Ngozi surname Adichie +XLEN writers +XRANGE writers - + COUNT 2 +``` diff --git a/iredis/data/commands/xread.md b/iredis/data/commands/xread.md new file mode 100644 index 0000000..6a45fb4 --- /dev/null +++ b/iredis/data/commands/xread.md @@ -0,0 +1,210 @@ +Read data from one or multiple streams, only returning entries with an ID +greater than the last received ID reported by the caller. This command has an +option to block if items are not available, in a similar fashion to `BRPOP` or +`BZPOPMIN` and others. + +Please note that before reading this page, if you are new to streams, we +recommend to read [our introduction to Redis Streams](/topics/streams-intro). + +## Non-blocking usage + +If the **BLOCK** option is not used, the command is synchronous, and can be +considered somewhat related to `XRANGE`: it will return a range of items inside +streams, however it has two fundamental differences compared to `XRANGE` even if +we just consider the synchronous usage: + +- This command can be called with multiple streams if we want to read at the + same time from a number of keys. This is a key feature of `XREAD` because + especially when blocking with **BLOCK**, to be able to listen with a single + connection to multiple keys is a vital feature. +- While `XRANGE` returns items in a range of IDs, `XREAD` is more suited in + order to consume the stream starting from the first entry which is greater + than any other entry we saw so far. So what we pass to `XREAD` is, for each + stream, the ID of the last element that we received from that stream. + +For example, if I have two streams `mystream` and `writers`, and I want to read +data from both the streams starting from the first element they contain, I could +call `XREAD` like in the following example. + +Note: we use the **COUNT** option in the example, so that for each stream the +call will return at maximum two elements per stream. + +``` +> XREAD COUNT 2 STREAMS mystream writers 0-0 0-0 +1) 1) "mystream" + 2) 1) 1) 1526984818136-0 + 2) 1) "duration" + 2) "1532" + 3) "event-id" + 4) "5" + 5) "user-id" + 6) "7782813" + 2) 1) 1526999352406-0 + 2) 1) "duration" + 2) "812" + 3) "event-id" + 4) "9" + 5) "user-id" + 6) "388234" +2) 1) "writers" + 2) 1) 1) 1526985676425-0 + 2) 1) "name" + 2) "Virginia" + 3) "surname" + 4) "Woolf" + 2) 1) 1526985685298-0 + 2) 1) "name" + 2) "Jane" + 3) "surname" + 4) "Austen" +``` + +The **STREAMS** option is mandatory and MUST be the final option because such +option gets a variable length of argument in the following format: + + STREAMS key_1 key_2 key_3 ... key_N ID_1 ID_2 ID_3 ... ID_N + +So we start with a list of keys, and later continue with all the associated IDs, +representing _the last ID we received for that stream_, so that the call will +serve us only greater IDs from the same stream. + +For instance in the above example, the last items that we received for the +stream `mystream` has ID `1526999352406-0`, while for the stream `writers` has +the ID `1526985685298-0`. + +To continue iterating the two streams I'll call: + +``` +> XREAD COUNT 2 STREAMS mystream writers 1526999352406-0 1526985685298-0 +1) 1) "mystream" + 2) 1) 1) 1526999626221-0 + 2) 1) "duration" + 2) "911" + 3) "event-id" + 4) "7" + 5) "user-id" + 6) "9488232" +2) 1) "writers" + 2) 1) 1) 1526985691746-0 + 2) 1) "name" + 2) "Toni" + 3) "surname" + 4) "Morrison" + 2) 1) 1526985712947-0 + 2) 1) "name" + 2) "Agatha" + 3) "surname" + 4) "Christie" +``` + +And so forth. Eventually, the call will not return any item, but just an empty +array, then we know that there is nothing more to fetch from our stream (and we +would have to retry the operation, hence this command also supports a blocking +mode). + +## Incomplete IDs + +To use incomplete IDs is valid, like it is valid for `XRANGE`. However here the +sequence part of the ID, if missing, is always interpreted as zero, so the +command: + +``` +> XREAD COUNT 2 STREAMS mystream writers 0 0 +``` + +is exactly equivalent to + +``` +> XREAD COUNT 2 STREAMS mystream writers 0-0 0-0 +``` + +## Blocking for data + +In its synchronous form, the command can get new data as long as there are more +items available. However, at some point, we'll have to wait for producers of +data to use `XADD` to push new entries inside the streams we are consuming. In +order to avoid polling at a fixed or adaptive interval the command is able to +block if it could not return any data, according to the specified streams and +IDs, and automatically unblock once one of the requested keys accept data. + +It is important to understand that this command _fans out_ to all the clients +that are waiting for the same range of IDs, so every consumer will get a copy of +the data, unlike to what happens when blocking list pop operations are used. + +In order to block, the **BLOCK** option is used, together with the number of +milliseconds we want to block before timing out. Normally Redis blocking +commands take timeouts in seconds, however this command takes a millisecond +timeout, even if normally the server will have a timeout resolution near to 0.1 +seconds. This time it is possible to block for a shorter time in certain use +cases, and if the server internals will improve over time, it is possible that +the resolution of timeouts will improve. + +When the **BLOCK** command is passed, but there is data to return at least in +one of the streams passed, the command is executed synchronously _exactly like +if the BLOCK option would be missing_. + +This is an example of blocking invocation, where the command later returns a +null reply because the timeout has elapsed without new data arriving: + +``` +> XREAD BLOCK 1000 STREAMS mystream 1526999626221-0 +(nil) +``` + +## The special `$` ID. + +When blocking sometimes we want to receive just entries that are added to the +stream via `XADD` starting from the moment we block. In such a case we are not +interested in the history of already added entries. For this use case, we would +have to check the stream top element ID, and use such ID in the `XREAD` command +line. This is not clean and requires to call other commands, so instead it is +possible to use the special `$` ID to signal the stream that we want only the +new things. + +It is **very important** to understand that you should use the `$` ID only for +the first call to `XREAD`. Later the ID should be the one of the last reported +item in the stream, otherwise you could miss all the entries that are added in +between. + +This is how a typical `XREAD` call looks like in the first iteration of a +consumer willing to consume only new entries: + +``` +> XREAD BLOCK 5000 COUNT 100 STREAMS mystream $ +``` + +Once we get some replies, the next call will be something like: + +``` +> XREAD BLOCK 5000 COUNT 100 STREAMS mystream 1526999644174-3 +``` + +And so forth. + +## How multiple clients blocked on a single stream are served + +Blocking list operations on lists or sorted sets have a _pop_ behavior. +Basically, the element is removed from the list or sorted set in order to be +returned to the client. In this scenario you want the items to be consumed in a +fair way, depending on the moment clients blocked on a given key arrived. +Normally Redis uses the FIFO semantics in this use cases. + +However note that with streams this is not a problem: stream entries are not +removed from the stream when clients are served, so every client waiting will be +served as soon as an `XADD` command provides data to the stream. + +@return + +@array-reply, specifically: + +The command returns an array of results: each element of the returned array is +an array composed of a two element containing the key name and the entries +reported for that key. The entries reported are full stream entries, having IDs +and the list of all the fields and values. Field and values are guaranteed to be +reported in the same order they were added by `XADD`. + +When **BLOCK** is used, on timeout a null reply is returned. + +Reading the [Redis Streams introduction](/topics/streams-intro) is highly +suggested in order to understand more about the streams overall behavior and +semantics. diff --git a/iredis/data/commands/xreadgroup.md b/iredis/data/commands/xreadgroup.md new file mode 100644 index 0000000..fb0b21c --- /dev/null +++ b/iredis/data/commands/xreadgroup.md @@ -0,0 +1,131 @@ +The `XREADGROUP` command is a special version of the `XREAD` command with +support for consumer groups. Probably you will have to understand the `XREAD` +command before reading this page will makes sense. + +Moreover, if you are new to streams, we recommend to read our +[introduction to Redis Streams](/topics/streams-intro). Make sure to understand +the concept of consumer group in the introduction so that following how this +command works will be simpler. + +## Consumer groups in 30 seconds + +The difference between this command and the vanilla `XREAD` is that this one +supports consumer groups. + +Without consumer groups, just using `XREAD`, all the clients are served with all +the entries arriving in a stream. Instead using consumer groups with +`XREADGROUP`, it is possible to create groups of clients that consume different +parts of the messages arriving in a given stream. If, for instance, the stream +gets the new entries A, B, and C and there are two consumers reading via a +consumer group, one client will get, for instance, the messages A and C, and the +other the message B, and so forth. + +Within a consumer group, a given consumer (that is, just a client consuming +messages from the stream), has to identify with an unique _consumer name_. Which +is just a string. + +One of the guarantees of consumer groups is that a given consumer can only see +the history of messages that were delivered to it, so a message has just a +single owner. However there is a special feature called _message claiming_ that +allows other consumers to claim messages in case there is a non recoverable +failure of some consumer. In order to implement such semantics, consumer groups +require explicit acknowledgement of the messages successfully processed by the +consumer, via the `XACK` command. This is needed because the stream will track, +for each consumer group, who is processing what message. + +This is how to understand if you want to use a consumer group or not: + +1. If you have a stream and multiple clients, and you want all the clients to + get all the messages, you do not need a consumer group. +2. If you have a stream and multiple clients, and you want the stream to be + _partitioned_ or _sharded_ across your clients, so that each client will get + a sub set of the messages arriving in a stream, you need a consumer group. + +## Differences between XREAD and XREADGROUP + +From the point of view of the syntax, the commands are almost the same, however +`XREADGROUP` _requires_ a special and mandatory option: + + GROUP <group-name> <consumer-name> + +The group name is just the name of a consumer group associated to the stream. +The group is created using the `XGROUP` command. The consumer name is the string +that is used by the client to identify itself inside the group. The consumer is +auto created inside the consumer group the first time it is saw. Different +clients should select a different consumer name. + +When you read with `XREADGROUP`, the server will _remember_ that a given message +was delivered to you: the message will be stored inside the consumer group in +what is called a Pending Entries List (PEL), that is a list of message IDs +delivered but not yet acknowledged. + +The client will have to acknowledge the message processing using `XACK` in order +for the pending entry to be removed from the PEL. The PEL can be inspected using +the `XPENDING` command. + +The `NOACK` subcommand can be used to avoid adding the message to the PEL in +cases where reliability is not a requirement and the occasional message loss is +acceptable. This is equivalent to acknowledging the message when it is read. + +The ID to specify in the **STREAMS** option when using `XREADGROUP` can be one +of the following two: + +- The special `>` ID, which means that the consumer want to receive only + messages that were _never delivered to any other consumer_. It just means, + give me new messages. +- Any other ID, that is, 0 or any other valid ID or incomplete ID (just the + millisecond time part), will have the effect of returning entries that are + pending for the consumer sending the command with IDs greater than the one + provided. So basically if the ID is not `>`, then the command will just let + the client access its pending entries: messages delivered to it, but not yet + acknowledged. Note that in this case, both `BLOCK` and `NOACK` are ignored. + +Like `XREAD` the `XREADGROUP` command can be used in a blocking way. There are +no differences in this regard. + +## What happens when a message is delivered to a consumer? + +Two things: + +1. If the message was never delivered to anyone, that is, if we are talking + about a new message, then a PEL (Pending Entry List) is created. +2. If instead the message was already delivered to this consumer, and it is just + re-fetching the same message again, then the _last delivery counter_ is + updated to the current time, and the _number of deliveries_ is incremented by + one. You can access those message properties using the `XPENDING` command. + +## Usage example + +Normally you use the command like that in order to get new messages and process +them. In pseudo-code: + +``` +WHILE true + entries = XREADGROUP GROUP $GroupName $ConsumerName BLOCK 2000 COUNT 10 STREAMS mystream > + if entries == nil + puts "Timeout... try again" + CONTINUE + end + + FOREACH entries AS stream_entries + FOREACH stream_entries as message + process_message(message.id,message.fields) + + # ACK the message as processed + XACK mystream $GroupName message.id + END + END +END +``` + +In this way the example consumer code will fetch only new messages, process +them, and acknowledge them via `XACK`. However the example code above is not +complete, because it does not handle recovering after a crash. What will happen +if we crash in the middle of processing messages, is that our messages will +remain in the pending entries list, so we can access our history by giving +`XREADGROUP` initially an ID of 0, and performing the same loop. Once providing +an ID of 0 the reply is an empty set of messages, we know that we processed and +acknowledged all the pending messages: we can start to use `>` as ID, in order +to get the new messages and rejoin the consumers that are processing new things. + +To see how the command actually replies, please check the `XREAD` command page. diff --git a/iredis/data/commands/xrevrange.md b/iredis/data/commands/xrevrange.md new file mode 100644 index 0000000..35d7438 --- /dev/null +++ b/iredis/data/commands/xrevrange.md @@ -0,0 +1,85 @@ +This command is exactly like `XRANGE`, but with the notable difference of +returning the entries in reverse order, and also taking the start-end range in +reverse order: in `XREVRANGE` you need to state the _end_ ID and later the +_start_ ID, and the command will produce all the element between (or exactly +like) the two IDs, starting from the _end_ side. + +So for instance, to get all the elements from the higher ID to the lower ID one +could use: + + XREVRANGE somestream + - + +Similarly to get just the last element added into the stream it is enough to +send: + + XREVRANGE somestream + - COUNT 1 + +## Iterating with XREVRANGE + +Like `XRANGE` this command can be used in order to iterate the whole stream +content, however note that in this case, the next command calls should use the +ID of the last entry, with the sequence number decremented by one. However if +the sequence number is already 0, the time part of the ID should be decremented +by 1, and the sequence part should be set to the maximum possible sequence +number, that is, 18446744073709551615, or could be omitted at all, and the +command will automatically assume it to be such a number (see `XRANGE` for more +info about incomplete IDs). + +Example: + +``` +> XREVRANGE writers + - COUNT 2 +1) 1) 1526985723355-0 + 2) 1) "name" + 2) "Ngozi" + 3) "surname" + 4) "Adichie" +2) 1) 1526985712947-0 + 2) 1) "name" + 2) "Agatha" + 3) "surname" + 4) "Christie" +``` + +The last ID returned is `1526985712947-0`, since the sequence number is already +zero, the next ID I'll use instead of the `+` special ID will be +`1526985712946-18446744073709551615`, or just `18446744073709551615`: + +``` +> XREVRANGE writers 1526985712946-18446744073709551615 - COUNT 2 +1) 1) 1526985691746-0 + 2) 1) "name" + 2) "Toni" + 3) "surname" + 4) "Morrison" +2) 1) 1526985685298-0 + 2) 1) "name" + 2) "Jane" + 3) "surname" + 4) "Austen" +``` + +And so for until the iteration is complete and no result is returned. See the +`XRANGE` page about iterating for more information. + +@return + +@array-reply, specifically: + +The command returns the entries with IDs matching the specified range, from the +higher ID to the lower ID matching. The returned entries are complete, that +means that the ID and all the fields they are composed are returned. Moreover +the entries are returned with their fields and values in the exact same order as +`XADD` added them. + +@examples + +```cli +XADD writers * name Virginia surname Woolf +XADD writers * name Jane surname Austen +XADD writers * name Toni surname Morrison +XADD writers * name Agatha surname Christie +XADD writers * name Ngozi surname Adichie +XLEN writers +XREVRANGE writers + - COUNT 1 +``` diff --git a/iredis/data/commands/xtrim.md b/iredis/data/commands/xtrim.md new file mode 100644 index 0000000..090650b --- /dev/null +++ b/iredis/data/commands/xtrim.md @@ -0,0 +1,37 @@ +`XTRIM` trims the stream to a given number of items, evicting older items (items +with lower IDs) if needed. The command is conceived to accept multiple trimming +strategies, however currently only a single one is implemented, which is +`MAXLEN`, and works exactly as the `MAXLEN` option in `XADD`. + +For example the following command will trim the stream to exactly the latest +1000 items: + +``` +XTRIM mystream MAXLEN 1000 +``` + +It is possible to give the command in the following special form in order to +make it more efficient: + +``` +XTRIM mystream MAXLEN ~ 1000 +``` + +The `~` argument between the **MAXLEN** option and the actual count means that +the user is not really requesting that the stream length is exactly 1000 items, +but instead it could be a few tens of entries more, but never less than 1000 +items. When this option modifier is used, the trimming is performed only when +Redis is able to remove a whole macro node. This makes it much more efficient, +and it is usually what you want. + +@return + +@integer-reply, specifically: + +The command returns the number of entries deleted from the stream. + +```cli +XADD mystream * field1 A field2 B field3 C field4 D +XTRIM mystream MAXLEN 2 +XRANGE mystream - + +``` diff --git a/iredis/data/commands/zadd.md b/iredis/data/commands/zadd.md new file mode 100644 index 0000000..589eaf3 --- /dev/null +++ b/iredis/data/commands/zadd.md @@ -0,0 +1,98 @@ +Adds all the specified members with the specified scores to the sorted set +stored at `key`. It is possible to specify multiple score / member pairs. If a +specified member is already a member of the sorted set, the score is updated and +the element reinserted at the right position to ensure the correct ordering. + +If `key` does not exist, a new sorted set with the specified members as sole +members is created, like if the sorted set was empty. If the key exists but does +not hold a sorted set, an error is returned. + +The score values should be the string representation of a double precision +floating point number. `+inf` and `-inf` values are valid values as well. + +## ZADD options (Redis 3.0.2 or greater) + +ZADD supports a list of options, specified after the name of the key and before +the first score argument. Options are: + +- **XX**: Only update elements that already exist. Never add elements. +- **NX**: Don't update already existing elements. Always add new elements. +- **CH**: Modify the return value from the number of new elements added, to the + total number of elements changed (CH is an abbreviation of _changed_). Changed + elements are **new elements added** and elements already existing for which + **the score was updated**. So elements specified in the command line having + the same score as they had in the past are not counted. Note: normally the + return value of `ZADD` only counts the number of new elements added. +- **INCR**: When this option is specified `ZADD` acts like `ZINCRBY`. Only one + score-element pair can be specified in this mode. + +## Range of integer scores that can be expressed precisely + +Redis sorted sets use a _double 64-bit floating point number_ to represent the +score. In all the architectures we support, this is represented as an **IEEE 754 +floating point number**, that is able to represent precisely integer numbers +between `-(2^53)` and `+(2^53)` included. In more practical terms, all the +integers between -9007199254740992 and 9007199254740992 are perfectly +representable. Larger integers, or fractions, are internally represented in +exponential form, so it is possible that you get only an approximation of the +decimal number, or of the very big integer, that you set as score. + +## Sorted sets 101 + +Sorted sets are sorted by their score in an ascending way. The same element only +exists a single time, no repeated elements are permitted. The score can be +modified both by `ZADD` that will update the element score, and as a side +effect, its position on the sorted set, and by `ZINCRBY` that can be used in +order to update the score relatively to its previous value. + +The current score of an element can be retrieved using the `ZSCORE` command, +that can also be used to verify if an element already exists or not. + +For an introduction to sorted sets, see the data types page on [sorted +sets][tdtss]. + +[tdtss]: /topics/data-types#sorted-sets + +## Elements with the same score + +While the same element can't be repeated in a sorted set since every element is +unique, it is possible to add multiple different elements _having the same +score_. When multiple elements have the same score, they are _ordered +lexicographically_ (they are still ordered by score as a first key, however, +locally, all the elements with the same score are relatively ordered +lexicographically). + +The lexicographic ordering used is binary, it compares strings as array of +bytes. + +If the user inserts all the elements in a sorted set with the same score (for +example 0), all the elements of the sorted set are sorted lexicographically, and +range queries on elements are possible using the command `ZRANGEBYLEX` (Note: it +is also possible to query sorted sets by range of scores using `ZRANGEBYSCORE`). + +@return + +@integer-reply, specifically: + +- The number of elements added to the sorted set, not including elements already + existing for which the score was updated. + +If the `INCR` option is specified, the return value will be @bulk-string-reply: + +- The new score of `member` (a double precision floating point number) + represented as string, or `nil` if the operation was aborted (when called with + either the `XX` or the `NX` option). + +@history + +- `>= 2.4`: Accepts multiple elements. In Redis versions older than 2.4 it was + possible to add or update a single member per call. + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 1 "uno" +ZADD myzset 2 "two" 3 "three" +ZRANGE myzset 0 -1 WITHSCORES +``` diff --git a/iredis/data/commands/zcard.md b/iredis/data/commands/zcard.md new file mode 100644 index 0000000..5ad5043 --- /dev/null +++ b/iredis/data/commands/zcard.md @@ -0,0 +1,15 @@ +Returns the sorted set cardinality (number of elements) of the sorted set stored +at `key`. + +@return + +@integer-reply: the cardinality (number of elements) of the sorted set, or `0` +if `key` does not exist. + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 2 "two" +ZCARD myzset +``` diff --git a/iredis/data/commands/zcount.md b/iredis/data/commands/zcount.md new file mode 100644 index 0000000..49e6dd6 --- /dev/null +++ b/iredis/data/commands/zcount.md @@ -0,0 +1,23 @@ +Returns the number of elements in the sorted set at `key` with a score between +`min` and `max`. + +The `min` and `max` arguments have the same semantic as described for +`ZRANGEBYSCORE`. + +Note: the command has a complexity of just O(log(N)) because it uses elements +ranks (see `ZRANK`) to get an idea of the range. Because of this there is no +need to do a work proportional to the size of the range. + +@return + +@integer-reply: the number of elements in the specified score range. + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 2 "two" +ZADD myzset 3 "three" +ZCOUNT myzset -inf +inf +ZCOUNT myzset (1 3 +``` diff --git a/iredis/data/commands/zincrby.md b/iredis/data/commands/zincrby.md new file mode 100644 index 0000000..0ac8a89 --- /dev/null +++ b/iredis/data/commands/zincrby.md @@ -0,0 +1,25 @@ +Increments the score of `member` in the sorted set stored at `key` by +`increment`. If `member` does not exist in the sorted set, it is added with +`increment` as its score (as if its previous score was `0.0`). If `key` does not +exist, a new sorted set with the specified `member` as its sole member is +created. + +An error is returned when `key` exists but does not hold a sorted set. + +The `score` value should be the string representation of a numeric value, and +accepts double precision floating point numbers. It is possible to provide a +negative value to decrement the score. + +@return + +@bulk-string-reply: the new score of `member` (a double precision floating point +number), represented as string. + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 2 "two" +ZINCRBY myzset 2 "one" +ZRANGE myzset 0 -1 WITHSCORES +``` diff --git a/iredis/data/commands/zinterstore.md b/iredis/data/commands/zinterstore.md new file mode 100644 index 0000000..e7e71f0 --- /dev/null +++ b/iredis/data/commands/zinterstore.md @@ -0,0 +1,30 @@ +Computes the intersection of `numkeys` sorted sets given by the specified keys, +and stores the result in `destination`. It is mandatory to provide the number of +input keys (`numkeys`) before passing the input keys and the other (optional) +arguments. + +By default, the resulting score of an element is the sum of its scores in the +sorted sets where it exists. Because intersection requires an element to be a +member of every given sorted set, this results in the score of every element in +the resulting sorted set to be equal to the number of input sorted sets. + +For a description of the `WEIGHTS` and `AGGREGATE` options, see `ZUNIONSTORE`. + +If `destination` already exists, it is overwritten. + +@return + +@integer-reply: the number of elements in the resulting sorted set at +`destination`. + +@examples + +```cli +ZADD zset1 1 "one" +ZADD zset1 2 "two" +ZADD zset2 1 "one" +ZADD zset2 2 "two" +ZADD zset2 3 "three" +ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3 +ZRANGE out 0 -1 WITHSCORES +``` diff --git a/iredis/data/commands/zlexcount.md b/iredis/data/commands/zlexcount.md new file mode 100644 index 0000000..9aa7092 --- /dev/null +++ b/iredis/data/commands/zlexcount.md @@ -0,0 +1,23 @@ +When all the elements in a sorted set are inserted with the same score, in order +to force lexicographical ordering, this command returns the number of elements +in the sorted set at `key` with a value between `min` and `max`. + +The `min` and `max` arguments have the same meaning as described for +`ZRANGEBYLEX`. + +Note: the command has a complexity of just O(log(N)) because it uses elements +ranks (see `ZRANK`) to get an idea of the range. Because of this there is no +need to do a work proportional to the size of the range. + +@return + +@integer-reply: the number of elements in the specified score range. + +@examples + +```cli +ZADD myzset 0 a 0 b 0 c 0 d 0 e +ZADD myzset 0 f 0 g +ZLEXCOUNT myzset - + +ZLEXCOUNT myzset [b [f +``` diff --git a/iredis/data/commands/zpopmax.md b/iredis/data/commands/zpopmax.md new file mode 100644 index 0000000..dea7a16 --- /dev/null +++ b/iredis/data/commands/zpopmax.md @@ -0,0 +1,20 @@ +Removes and returns up to `count` members with the highest scores in the sorted +set stored at `key`. + +When left unspecified, the default value for `count` is 1. Specifying a `count` +value that is higher than the sorted set's cardinality will not produce an +error. When returning multiple elements, the one with the highest score will be +the first, followed by the elements with lower scores. + +@return + +@array-reply: list of popped elements and scores. + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 2 "two" +ZADD myzset 3 "three" +ZPOPMAX myzset +``` diff --git a/iredis/data/commands/zpopmin.md b/iredis/data/commands/zpopmin.md new file mode 100644 index 0000000..789e30c --- /dev/null +++ b/iredis/data/commands/zpopmin.md @@ -0,0 +1,20 @@ +Removes and returns up to `count` members with the lowest scores in the sorted +set stored at `key`. + +When left unspecified, the default value for `count` is 1. Specifying a `count` +value that is higher than the sorted set's cardinality will not produce an +error. When returning multiple elements, the one with the lowest score will be +the first, followed by the elements with greater scores. + +@return + +@array-reply: list of popped elements and scores. + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 2 "two" +ZADD myzset 3 "three" +ZPOPMIN myzset +``` diff --git a/iredis/data/commands/zrange.md b/iredis/data/commands/zrange.md new file mode 100644 index 0000000..e2b1c5b --- /dev/null +++ b/iredis/data/commands/zrange.md @@ -0,0 +1,49 @@ +Returns the specified range of elements in the sorted set stored at `key`. The +elements are considered to be ordered from the lowest to the highest score. +Lexicographical order is used for elements with equal score. + +See `ZREVRANGE` when you need the elements ordered from highest to lowest score +(and descending lexicographical order for elements with equal score). + +Both `start` and `stop` are zero-based indexes, where `0` is the first element, +`1` is the next element and so on. They can also be negative numbers indicating +offsets from the end of the sorted set, with `-1` being the last element of the +sorted set, `-2` the penultimate element and so on. + +`start` and `stop` are **inclusive ranges**, so for example `ZRANGE myzset 0 1` +will return both the first and the second element of the sorted set. + +Out of range indexes will not produce an error. If `start` is larger than the +largest index in the sorted set, or `start > stop`, an empty list is returned. +If `stop` is larger than the end of the sorted set Redis will treat it like it +is the last element of the sorted set. + +It is possible to pass the `WITHSCORES` option in order to return the scores of +the elements together with the elements. The returned list will contain +`value1,score1,...,valueN,scoreN` instead of `value1,...,valueN`. Client +libraries are free to return a more appropriate data type (suggestion: an array +with (value, score) arrays/tuples). + +@return + +@array-reply: list of elements in the specified range (optionally with their +scores, in case the `WITHSCORES` option is given). + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 2 "two" +ZADD myzset 3 "three" +ZRANGE myzset 0 -1 +ZRANGE myzset 2 3 +ZRANGE myzset -2 -1 +``` + +The following example using `WITHSCORES` shows how the command returns always an +array, but this time, populated with _element_1_, _score_1_, _element_2_, +_score_2_, ..., _element_N_, _score_N_. + +```cli +ZRANGE myzset 0 1 WITHSCORES +``` diff --git a/iredis/data/commands/zrangebylex.md b/iredis/data/commands/zrangebylex.md new file mode 100644 index 0000000..ab387bd --- /dev/null +++ b/iredis/data/commands/zrangebylex.md @@ -0,0 +1,66 @@ +When all the elements in a sorted set are inserted with the same score, in order +to force lexicographical ordering, this command returns all the elements in the +sorted set at `key` with a value between `min` and `max`. + +If the elements in the sorted set have different scores, the returned elements +are unspecified. + +The elements are considered to be ordered from lower to higher strings as +compared byte-by-byte using the `memcmp()` C function. Longer strings are +considered greater than shorter strings if the common part is identical. + +The optional `LIMIT` argument can be used to only get a range of the matching +elements (similar to _SELECT LIMIT offset, count_ in SQL). A negative `count` +returns all elements from the `offset`. Keep in mind that if `offset` is large, +the sorted set needs to be traversed for `offset` elements before getting to the +elements to return, which can add up to O(N) time complexity. + +## How to specify intervals + +Valid _start_ and _stop_ must start with `(` or `[`, in order to specify if the +range item is respectively exclusive or inclusive. The special values of `+` or +`-` for _start_ and _stop_ have the special meaning or positively infinite and +negatively infinite strings, so for instance the command **ZRANGEBYLEX +myzset - +** is guaranteed to return all the elements in the sorted set, if all +the elements have the same score. + +## Details on strings comparison + +Strings are compared as binary array of bytes. Because of how the ASCII +character set is specified, this means that usually this also have the effect of +comparing normal ASCII characters in an obvious dictionary way. However this is +not true if non plain ASCII strings are used (for example utf8 strings). + +However the user can apply a transformation to the encoded string so that the +first part of the element inserted in the sorted set will compare as the user +requires for the specific application. For example if I want to add strings that +will be compared in a case-insensitive way, but I still want to retrieve the +real case when querying, I can add strings in the following way: + + ZADD autocomplete 0 foo:Foo 0 bar:BAR 0 zap:zap + +Because of the first _normalized_ part in every element (before the colon +character), we are forcing a given comparison, however after the range is +queries using `ZRANGEBYLEX` the application can display to the user the second +part of the string, after the colon. + +The binary nature of the comparison allows to use sorted sets as a general +purpose index, for example the first part of the element can be a 64 bit big +endian number: since big endian numbers have the most significant bytes in the +initial positions, the binary comparison will match the numerical comparison of +the numbers. This can be used in order to implement range queries on 64 bit +values. As in the example below, after the first 8 bytes we can store the value +of the element we are actually indexing. + +@return + +@array-reply: list of elements in the specified score range. + +@examples + +```cli +ZADD myzset 0 a 0 b 0 c 0 d 0 e 0 f 0 g +ZRANGEBYLEX myzset - [c +ZRANGEBYLEX myzset - (c +ZRANGEBYLEX myzset [aaa (g +``` diff --git a/iredis/data/commands/zrangebyscore.md b/iredis/data/commands/zrangebyscore.md new file mode 100644 index 0000000..f440e0e --- /dev/null +++ b/iredis/data/commands/zrangebyscore.md @@ -0,0 +1,100 @@ +Returns all the elements in the sorted set at `key` with a score between `min` +and `max` (including elements with score equal to `min` or `max`). The elements +are considered to be ordered from low to high scores. + +The elements having the same score are returned in lexicographical order (this +follows from a property of the sorted set implementation in Redis and does not +involve further computation). + +The optional `LIMIT` argument can be used to only get a range of the matching +elements (similar to _SELECT LIMIT offset, count_ in SQL). A negative `count` +returns all elements from the `offset`. Keep in mind that if `offset` is large, +the sorted set needs to be traversed for `offset` elements before getting to the +elements to return, which can add up to O(N) time complexity. + +The optional `WITHSCORES` argument makes the command return both the element and +its score, instead of the element alone. This option is available since Redis +2.0. + +## Exclusive intervals and infinity + +`min` and `max` can be `-inf` and `+inf`, so that you are not required to know +the highest or lowest score in the sorted set to get all elements from or up to +a certain score. + +By default, the interval specified by `min` and `max` is closed (inclusive). It +is possible to specify an open interval (exclusive) by prefixing the score with +the character `(`. For example: + +``` +ZRANGEBYSCORE zset (1 5 +``` + +Will return all elements with `1 < score <= 5` while: + +``` +ZRANGEBYSCORE zset (5 (10 +``` + +Will return all the elements with `5 < score < 10` (5 and 10 excluded). + +@return + +@array-reply: list of elements in the specified score range (optionally with +their scores). + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 2 "two" +ZADD myzset 3 "three" +ZRANGEBYSCORE myzset -inf +inf +ZRANGEBYSCORE myzset 1 2 +ZRANGEBYSCORE myzset (1 2 +ZRANGEBYSCORE myzset (1 (2 +``` + +## Pattern: weighted random selection of an element + +Normally `ZRANGEBYSCORE` is simply used in order to get range of items where the +score is the indexed integer key, however it is possible to do less obvious +things with the command. + +For example a common problem when implementing Markov chains and other +algorithms is to select an element at random from a set, but different elements +may have different weights that change how likely it is they are picked. + +This is how we use this command in order to mount such an algorithm: + +Imagine you have elements A, B and C with weights 1, 2 and 3. You compute the +sum of the weights, which is 1+2+3 = 6 + +At this point you add all the elements into a sorted set using this algorithm: + +``` +SUM = ELEMENTS.TOTAL_WEIGHT // 6 in this case. +SCORE = 0 +FOREACH ELE in ELEMENTS + SCORE += ELE.weight / SUM + ZADD KEY SCORE ELE +END +``` + +This means that you set: + +``` +A to score 0.16 +B to score .5 +C to score 1 +``` + +Since this involves approximations, in order to avoid C is set to, like, 0.998 +instead of 1, we just modify the above algorithm to make sure the last score is +1 (left as an exercise for the reader...). + +At this point, each time you want to get a weighted random element, just compute +a random number between 0 and 1 (which is like calling `rand()` in most +languages), so you can just do: + + RANDOM_ELE = ZRANGEBYSCORE key RAND() +inf LIMIT 0 1 diff --git a/iredis/data/commands/zrank.md b/iredis/data/commands/zrank.md new file mode 100644 index 0000000..62520d9 --- /dev/null +++ b/iredis/data/commands/zrank.md @@ -0,0 +1,22 @@ +Returns the rank of `member` in the sorted set stored at `key`, with the scores +ordered from low to high. The rank (or index) is 0-based, which means that the +member with the lowest score has rank `0`. + +Use `ZREVRANK` to get the rank of an element with the scores ordered from high +to low. + +@return + +- If `member` exists in the sorted set, @integer-reply: the rank of `member`. +- If `member` does not exist in the sorted set or `key` does not exist, + @bulk-string-reply: `nil`. + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 2 "two" +ZADD myzset 3 "three" +ZRANK myzset "three" +ZRANK myzset "four" +``` diff --git a/iredis/data/commands/zrem.md b/iredis/data/commands/zrem.md new file mode 100644 index 0000000..0ce5061 --- /dev/null +++ b/iredis/data/commands/zrem.md @@ -0,0 +1,26 @@ +Removes the specified members from the sorted set stored at `key`. Non existing +members are ignored. + +An error is returned when `key` exists and does not hold a sorted set. + +@return + +@integer-reply, specifically: + +- The number of members removed from the sorted set, not including non existing + members. + +@history + +- `>= 2.4`: Accepts multiple elements. In Redis versions older than 2.4 it was + possible to remove a single member per call. + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 2 "two" +ZADD myzset 3 "three" +ZREM myzset "two" +ZRANGE myzset 0 -1 WITHSCORES +``` diff --git a/iredis/data/commands/zremrangebylex.md b/iredis/data/commands/zremrangebylex.md new file mode 100644 index 0000000..ceaf69f --- /dev/null +++ b/iredis/data/commands/zremrangebylex.md @@ -0,0 +1,22 @@ +When all the elements in a sorted set are inserted with the same score, in order +to force lexicographical ordering, this command removes all elements in the +sorted set stored at `key` between the lexicographical range specified by `min` +and `max`. + +The meaning of `min` and `max` are the same of the `ZRANGEBYLEX` command. +Similarly, this command actually returns the same elements that `ZRANGEBYLEX` +would return if called with the same `min` and `max` arguments. + +@return + +@integer-reply: the number of elements removed. + +@examples + +```cli +ZADD myzset 0 aaaa 0 b 0 c 0 d 0 e +ZADD myzset 0 foo 0 zap 0 zip 0 ALPHA 0 alpha +ZRANGE myzset 0 -1 +ZREMRANGEBYLEX myzset [alpha [omega +ZRANGE myzset 0 -1 +``` diff --git a/iredis/data/commands/zremrangebyrank.md b/iredis/data/commands/zremrangebyrank.md new file mode 100644 index 0000000..4de25f9 --- /dev/null +++ b/iredis/data/commands/zremrangebyrank.md @@ -0,0 +1,20 @@ +Removes all elements in the sorted set stored at `key` with rank between `start` +and `stop`. Both `start` and `stop` are `0` -based indexes with `0` being the +element with the lowest score. These indexes can be negative numbers, where they +indicate offsets starting at the element with the highest score. For example: +`-1` is the element with the highest score, `-2` the element with the second +highest score and so forth. + +@return + +@integer-reply: the number of elements removed. + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 2 "two" +ZADD myzset 3 "three" +ZREMRANGEBYRANK myzset 0 1 +ZRANGE myzset 0 -1 WITHSCORES +``` diff --git a/iredis/data/commands/zremrangebyscore.md b/iredis/data/commands/zremrangebyscore.md new file mode 100644 index 0000000..3665bd0 --- /dev/null +++ b/iredis/data/commands/zremrangebyscore.md @@ -0,0 +1,19 @@ +Removes all elements in the sorted set stored at `key` with a score between +`min` and `max` (inclusive). + +Since version 2.1.6, `min` and `max` can be exclusive, following the syntax of +`ZRANGEBYSCORE`. + +@return + +@integer-reply: the number of elements removed. + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 2 "two" +ZADD myzset 3 "three" +ZREMRANGEBYSCORE myzset -inf (2 +ZRANGE myzset 0 -1 WITHSCORES +``` diff --git a/iredis/data/commands/zrevrange.md b/iredis/data/commands/zrevrange.md new file mode 100644 index 0000000..c9f6c4d --- /dev/null +++ b/iredis/data/commands/zrevrange.md @@ -0,0 +1,21 @@ +Returns the specified range of elements in the sorted set stored at `key`. The +elements are considered to be ordered from the highest to the lowest score. +Descending lexicographical order is used for elements with equal score. + +Apart from the reversed ordering, `ZREVRANGE` is similar to `ZRANGE`. + +@return + +@array-reply: list of elements in the specified range (optionally with their +scores). + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 2 "two" +ZADD myzset 3 "three" +ZREVRANGE myzset 0 -1 +ZREVRANGE myzset 2 3 +ZREVRANGE myzset -2 -1 +``` diff --git a/iredis/data/commands/zrevrangebylex.md b/iredis/data/commands/zrevrangebylex.md new file mode 100644 index 0000000..831e5cd --- /dev/null +++ b/iredis/data/commands/zrevrangebylex.md @@ -0,0 +1,18 @@ +When all the elements in a sorted set are inserted with the same score, in order +to force lexicographical ordering, this command returns all the elements in the +sorted set at `key` with a value between `max` and `min`. + +Apart from the reversed ordering, `ZREVRANGEBYLEX` is similar to `ZRANGEBYLEX`. + +@return + +@array-reply: list of elements in the specified score range. + +@examples + +```cli +ZADD myzset 0 a 0 b 0 c 0 d 0 e 0 f 0 g +ZREVRANGEBYLEX myzset [c - +ZREVRANGEBYLEX myzset (c - +ZREVRANGEBYLEX myzset (g [aaa +``` diff --git a/iredis/data/commands/zrevrangebyscore.md b/iredis/data/commands/zrevrangebyscore.md new file mode 100644 index 0000000..c16d8b4 --- /dev/null +++ b/iredis/data/commands/zrevrangebyscore.md @@ -0,0 +1,27 @@ +Returns all the elements in the sorted set at `key` with a score between `max` +and `min` (including elements with score equal to `max` or `min`). In contrary +to the default ordering of sorted sets, for this command the elements are +considered to be ordered from high to low scores. + +The elements having the same score are returned in reverse lexicographical +order. + +Apart from the reversed ordering, `ZREVRANGEBYSCORE` is similar to +`ZRANGEBYSCORE`. + +@return + +@array-reply: list of elements in the specified score range (optionally with +their scores). + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 2 "two" +ZADD myzset 3 "three" +ZREVRANGEBYSCORE myzset +inf -inf +ZREVRANGEBYSCORE myzset 2 1 +ZREVRANGEBYSCORE myzset 2 (1 +ZREVRANGEBYSCORE myzset (2 (1 +``` diff --git a/iredis/data/commands/zrevrank.md b/iredis/data/commands/zrevrank.md new file mode 100644 index 0000000..e85c80c --- /dev/null +++ b/iredis/data/commands/zrevrank.md @@ -0,0 +1,22 @@ +Returns the rank of `member` in the sorted set stored at `key`, with the scores +ordered from high to low. The rank (or index) is 0-based, which means that the +member with the highest score has rank `0`. + +Use `ZRANK` to get the rank of an element with the scores ordered from low to +high. + +@return + +- If `member` exists in the sorted set, @integer-reply: the rank of `member`. +- If `member` does not exist in the sorted set or `key` does not exist, + @bulk-string-reply: `nil`. + +@examples + +```cli +ZADD myzset 1 "one" +ZADD myzset 2 "two" +ZADD myzset 3 "three" +ZREVRANK myzset "one" +ZREVRANK myzset "four" +``` diff --git a/iredis/data/commands/zscan.md b/iredis/data/commands/zscan.md new file mode 100644 index 0000000..3926307 --- /dev/null +++ b/iredis/data/commands/zscan.md @@ -0,0 +1 @@ +See `SCAN` for `ZSCAN` documentation. diff --git a/iredis/data/commands/zscore.md b/iredis/data/commands/zscore.md new file mode 100644 index 0000000..f3204d0 --- /dev/null +++ b/iredis/data/commands/zscore.md @@ -0,0 +1,16 @@ +Returns the score of `member` in the sorted set at `key`. + +If `member` does not exist in the sorted set, or `key` does not exist, `nil` is +returned. + +@return + +@bulk-string-reply: the score of `member` (a double precision floating point +number), represented as string. + +@examples + +```cli +ZADD myzset 1 "one" +ZSCORE myzset "one" +``` diff --git a/iredis/data/commands/zunionstore.md b/iredis/data/commands/zunionstore.md new file mode 100644 index 0000000..45b4b3b --- /dev/null +++ b/iredis/data/commands/zunionstore.md @@ -0,0 +1,38 @@ +Computes the union of `numkeys` sorted sets given by the specified keys, and +stores the result in `destination`. It is mandatory to provide the number of +input keys (`numkeys`) before passing the input keys and the other (optional) +arguments. + +By default, the resulting score of an element is the sum of its scores in the +sorted sets where it exists. + +Using the `WEIGHTS` option, it is possible to specify a multiplication factor +for each input sorted set. This means that the score of every element in every +input sorted set is multiplied by this factor before being passed to the +aggregation function. When `WEIGHTS` is not given, the multiplication factors +default to `1`. + +With the `AGGREGATE` option, it is possible to specify how the results of the +union are aggregated. This option defaults to `SUM`, where the score of an +element is summed across the inputs where it exists. When this option is set to +either `MIN` or `MAX`, the resulting set will contain the minimum or maximum +score of an element across the inputs where it exists. + +If `destination` already exists, it is overwritten. + +@return + +@integer-reply: the number of elements in the resulting sorted set at +`destination`. + +@examples + +```cli +ZADD zset1 1 "one" +ZADD zset1 2 "two" +ZADD zset2 1 "one" +ZADD zset2 2 "two" +ZADD zset2 3 "three" +ZUNIONSTORE out 2 zset1 zset2 WEIGHTS 2 3 +ZRANGE out 0 -1 WITHSCORES +``` diff --git a/iredis/data/dangerous_commands.csv b/iredis/data/dangerous_commands.csv new file mode 100644 index 0000000..f42caa5 --- /dev/null +++ b/iredis/data/dangerous_commands.csv @@ -0,0 +1,13 @@ +Command,Reason +FLUSHDB,"FLUSHDB will delete all the keys of the currently selected DB" +FLUSHALL,"FLUSHALL will delete all the keys of all the existing databases" +KEYS,"KEYS will hang redis server, use SCAN instead" +PEXPIRE,"PEXPIRE may delete keys" +DEL,"DEL will delete keys, it may cause high latency when the value is big" +CONFIG SET,"CONFIG SET will change the server's configs" +SHUTDOWN,"SHUTDOWN will shutdown the server" +SAVE,"SAVE performs a synchronous save, it will hang redis server" +SPOP,"SPOP will delete items" +SREM,"SREM will delete items" +RENAME,"RENAME use DELETE command to overwrite exist key, it may cause high latency when the value is big" +DEBUG,"It's a dangerous command" diff --git a/iredis/data/iredisrc b/iredis/data/iredisrc new file mode 100644 index 0000000..5baef86 --- /dev/null +++ b/iredis/data/iredisrc @@ -0,0 +1,67 @@ +# vi: ft=dosini +[main] +# weather display raw redis response +raw = False + +# iredis use a LRU strategy to store the completions, like keys, set members, +# etc, this will set how many completions can iredis keep at most. +completer_max = 300 + +# Completion casing preference, options are: "lower", "upper", "auto" +completion_casing = auto + +# if in newbie_mode, a description of commands and options will showup along +# with completion, encourage to enable it to who is new to redis +newbie_mode = False + +# show prompt in a ranibow color +rainbow = False + +# retry times for connection error and timeout +retry_times = 2 + +socket_keepalive = True + +# IRedis support running shell command to parse the response, like this: +# > get json-str | jq . +# However that will allow any shell command to execute under iredis REPL, +# you can disable this feature by setting this to False. +# Default is True. +shell = True + +# decode redis response, default None +decode = + +# enable pager? defualt to True, you can disable it by changing it to False +enable_pager = True + +# pager setting when line is too tall +# By default 'PAGER' environment variable is used +# pager = less -SRXF + +# iredis will send a `INFO` command to get the server's version, this option can +# disable it +no_info = False + +# iredis will show command hint on bottom bar, this option can disable it +bottom_bar = True + +# Dangerous command warning mode will alert you before executing a dangerous +# command, that may cause harm to the redis-server or hang server, +# such as "KEYS", "DEL" or "SHUTDOWN". +warning = True + +# IRedis log for debugging, leave this blank will disable log. +# You don't need this unless you are debuging iredis. +# Be careful this will log your commands input (include AUTH with password) to +# log file. +# eg. ~/.iredis.log +log_location = + +# History file location +history_location = ~/.iredis_history + +[alias_dsn] +# example_dsn = redis://[[username]:[password]]@localhost:6379/0 +# example_dsn = rediss://[[username]:[password]]@localhost:6379/0 +# example_dsn = unix://[[username]:[password]]@/path/to/socket.sock?db=0 diff --git a/iredis/entry.py b/iredis/entry.py new file mode 100644 index 0000000..8e396a4 --- /dev/null +++ b/iredis/entry.py @@ -0,0 +1,446 @@ +# -*- coding: utf-8 -*- +import os +import logging +import sys +import time +from pathlib import Path +import platform + +import click +from prompt_toolkit import PromptSession +from prompt_toolkit.history import FileHistory +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory +from prompt_toolkit import print_formatted_text +from prompt_toolkit.formatted_text import FormattedText +from prompt_toolkit.key_binding.bindings.named_commands import ( + register as prompt_register, +) + +from .client import Client +from .key_bindings import kb as key_bindings +from .style import STYLE +from .config import config, load_config_files +from .processors import UserInputCommand, UpdateBottomProcessor, PasswordProcessor +from .bottom import BottomToolbar +from .utils import timer, exit, convert_formatted_text_to_bytes, parse_url +from .completers import IRedisCompleter +from .lexer import IRedisLexer +from . import __version__ + +logger = logging.getLogger(__name__) + + +class SkipAuthFileHistory(FileHistory): + """Exactlly like FileHistory, but won't save `AUTH` command into history + file.""" + + def append_string(self, string: str) -> None: + if string.lstrip().upper().startswith("AUTH"): + return + super().append_string(string) + + +def setup_log(): + if config.log_location: + logging.basicConfig( + filename=os.path.expanduser(config.log_location), + filemode="a", + format="%(levelname)5s %(message)s", + level="DEBUG", + ) + else: + logging.disable(logging.CRITICAL) + logger.info("------ iRedis ------") + + +def greetings(): + iredis_version = f"iredis {__version__} (Python {platform.python_version()})" + if config.no_version_reason: + reason = f"({config.no_version_reason})" + else: + reason = "" + + server_version = f"redis-server {config.version} {reason}" + home_page = "Home: https://iredis.io" + issues = "Issues: https://iredis.io/issues" + display = "\n".join([iredis_version, server_version, home_page, issues]) + if config.raw: + display = display.encode() + write_result(display) + + +def print_help_msg(command): + with click.Context(command) as ctx: + click.echo(command.get_help(ctx)) + + +def is_too_tall(text, max_height): + if isinstance(text, FormattedText): + text = convert_formatted_text_to_bytes(text) + lines = len(text.split(b"\n")) + return lines > max_height + + +def write_result(text, max_height=None): + """ + When config.raw set to True, write text(must be bytes in that case) + directly to stdout, same if text is bytes. + + :param text: is_raw: bytes or str, not raw: FormattedText + :is_raw: bool + """ + logger.info(f"Print result {type(text)}: {text}"[:200]) + + # this function only handle bytes or FormattedText + # if it's str, convert to bytes + if isinstance(text, str): + if config.decode: + text = text.encode(config.decode) + else: + text = text.encode() + + # using pager if too tall + if max_height and config.enable_pager and is_too_tall(text, max_height): + if isinstance(text, FormattedText): + text = convert_formatted_text_to_bytes(text) + os.environ["LESS"] = "-SRX" + # click.echo_via_pager only accepts str + if config.decode: + text = text.decode(config.decode) + else: + text = text.decode() + # TODO current pager doesn't support colors + click.echo_via_pager(text) + return + + if isinstance(text, bytes): + sys.stdout.buffer.write(text) + sys.stdout.write("\n") + else: + print_formatted_text(text, end="", style=STYLE) + print_formatted_text() + + +class Rainbow: + color = [ + ("#cc2244"), + ("#bb4444"), + ("#996644"), + ("#cc8844"), + ("#ccaa44"), + ("#bbaa44"), + ("#99aa44"), + ("#778844"), + ("#55aa44"), + ("#33aa44"), + ("#11aa44"), + ("#11aa66"), + ("#11aa88"), + ("#11aaaa"), + ("#11aacc"), + ("#11aaee"), + ] + + def __init__(self): + self.current = -1 + self.forword = 1 + + def __iter__(self): + return self + + def __next__(self): + self.current += self.forword + if 0 <= self.current < len(self.color): + # not to the end + return self.color[self.current] + else: + self.forword = -self.forword + self.current += 2 * self.forword + return self.color[self.current] + + +def prompt_message(client): + # TODO custome prompt + text = "{hostname}> ".format(hostname=str(client)) + if config.rainbow: + return list(zip(Rainbow(), text)) + return text + + +def repl(client, session, start_time): + command_holder = UserInputCommand() + timer(f"First REPL command enter, time cost: {time.time() - start_time}") + + while True: + logger.info("↓↓↓↓" * 10) + logger.info("REPL waiting for command...") + + try: + command = session.prompt( + prompt_message(client), + bottom_toolbar=BottomToolbar(command_holder).render + if config.bottom_bar + else None, + input_processors=[ + UpdateBottomProcessor(command_holder, session), + PasswordProcessor(), + ], + rprompt=lambda: "<transaction>" if config.transaction else None, + key_bindings=key_bindings, + enable_suspend=True, + ) + + except KeyboardInterrupt: + logger.warning("KeyboardInterrupt!") + continue + except EOFError: + exit() + command = command.strip() + logger.info(f"[Command] {command}") + + # blank input + if not command: + continue + + try: + answers = client.send_command(command, session.completer) + for answer in answers: + write_result( + answer, + # -1 is because 127.0.0.1:6379> takes one line + session.output.get_size().rows - session.reserve_space_for_menu - 1, + ) + # Error with previous command or exception + except Exception as e: + logger.exception(e) + # TODO red error color + print("(error)", str(e)) + + +RAW_HELP = """ +Use raw formatting for replies (default when STDOUT is not a tty). \ +However, you can use --no-raw to force formatted output even \ +when STDOUT is not a tty. +""" +DECODE_HELP = """ +decode response, default is No decode, which will output all bytes literals. +""" +RAINBOW = "Display colorful prompt." +DSN_HELP = """ +Use DSN configured into the [alias_dsn] section of iredisrc file. \ +(Can set with env `IREDIS_DSN`) +""" +URL_HELP = """ +Use Redis URL to indicate connection(Can set with env `IREDIS_URL`), Example: + redis://[[username]:[password]]@localhost:6379/0 + rediss://[[username]:[password]]@localhost:6379/0 + unix://[[username]:[password]]@/path/to/socket.sock?db=0 +""" +SHELL = """Allow to run shell commands, default to True.""" +PAGER_HELP = """Using pager when output is too tall for your window, default to True.""" + + +# command line entry here... +@click.command() +@click.pass_context +@click.option("-h", help="Server hostname (default: 127.0.0.1).", default="127.0.0.1") +@click.option("-p", help="Server port (default: 6379).", default="6379") +@click.option( + "-s", "--socket", default=None, help="Server socket (overrides hostname and port)." +) +@click.option( + "-n", help="Database number.(overwrites dsn/url's db number)", default=None +) +@click.option("-a", "--password", help="Password to use when connecting to the server.") +@click.option("--url", default=None, envvar="IREDIS_URL", help=URL_HELP) +@click.option("-d", "--dsn", default=None, envvar="IREDIS_DSN", help=DSN_HELP) +@click.option( + "--newbie/--no-newbie", + default=None, + is_flag=True, + help="Show command hints and useful helps.", +) +@click.option( + "--iredisrc", + default="~/.iredisrc", + help="Config file for iredis, default is ~/.iredisrc.", +) +@click.option("--decode", default=None, help=DECODE_HELP) +@click.option("--raw/--no-raw", default=None, is_flag=True, help=RAW_HELP) +@click.option("--rainbow/--no-rainbow", default=None, is_flag=True, help=RAINBOW) +@click.option("--shell/--no-shell", default=None, is_flag=True, help=SHELL) +@click.option("--pager/--no-pager", default=None, is_flag=True, help=PAGER_HELP) +@click.version_option() +@click.argument("cmd", nargs=-1) +def gather_args( + ctx, + h, + p, + n, + password, + newbie, + iredisrc, + decode, + raw, + rainbow, + cmd, + dsn, + url, + socket, + shell, + pager, +): + """ + IRedis: Interactive Redis + + When no command is given, IRedis starts in interactive mode. + + \b + Examples: + - iredis + - iredis -d dsn + - iredis -h 127.0.0.1 -p 6379 + - iredis -h 127.0.0.1 -p 6379 -a <password> + - iredis --url redis://localhost:7890/3 + + Type "help" in interactive mode for information on available commands + and settings. + """ + load_config_files(iredisrc) + setup_log() + logger.info( + f"[commandline args] host={h}, port={p}, db={n}, newbie={newbie}, " + f"iredisrc={iredisrc}, decode={decode}, raw={raw}, " + f"cmd={cmd}, rainbow={rainbow}." + ) + # raw config + if raw is not None: + config.raw = raw + if not sys.stdout.isatty(): + config.raw = True + + config.newbie_mode = newbie + + if decode is not None: + config.decode = decode + if rainbow is not None: + config.rainbow = rainbow + if shell is not None: + config.shell = shell + if pager is not None: + config.enable_pager = pager + + return ctx + + +@prompt_register("edit-and-execute-command") +def edit_and_execute(event): + """Different from the prompt-toolkit default, we want to have a choice not + to execute a query after editing, hence validate_and_handle=False.""" + buff = event.current_buffer + # this will prevent running command immediately when exit editor. + buff.open_in_editor(validate_and_handle=False) + + +def resolve_dsn(dsn): + try: + dsn_uri = config.alias_dsn[dsn] + except KeyError: + click.secho( + "Could not find the specified DSN in the config file. " + 'Please check the "[alias_dsn]" section in your ' + "iredisrc.", + err=True, + fg="red", + ) + sys.exit(1) + return dsn_uri + + +def create_client(params): + """ + Create a Client. + :param params: commandline params. + """ + host = params["h"] + port = params["p"] + db = params["n"] + password = params["password"] + + dsn_from_url = None + dsn = params["dsn"] + if config.alias_dsn and dsn: + dsn_uri = resolve_dsn(dsn) + dsn_from_url = parse_url(dsn_uri) + if params["url"]: + dsn_from_url = parse_url(params["url"]) + if dsn_from_url: + # db from command lint options should be high priority + db = db if db else dsn_from_url.db + return Client( + host=dsn_from_url.host, + port=dsn_from_url.port, + db=db, + password=dsn_from_url.password, + path=dsn_from_url.path, + scheme=dsn_from_url.scheme, + username=dsn_from_url.username, + ) + if params["socket"]: + return Client(scheme="unix", path=params["socket"], db=db, password=password) + return Client(host=host, port=port, db=db, password=password) + + +def main(): + enter_main_time = time.time() # just for logs + + # invoke in non-standalone mode to gather args + ctx = None + try: + ctx = gather_args.main(standalone_mode=False) + except click.exceptions.NoSuchOption as nosuchoption: + nosuchoption.show() + except click.exceptions.BadOptionUsage as badoption: + if badoption.option_name == "-h": + # -h without host, is short command for --help + # like redis-cli + print_help_msg(gather_args) + return + if not ctx: # called help + return + + # redis client + client = create_client(ctx.params) + + if not sys.stdin.isatty(): + for line in sys.stdin.readlines(): + logger.debug(f"[Command stdin] {line}") + for answer in client.send_command(line, None): + write_result(answer) + return + + # no interactive mode, directly run a command + if ctx.params["cmd"]: + answers = client.send_command(" ".join(ctx.params["cmd"]), None) + for answer in answers: + write_result(answer) + logger.warning("[OVER] command executed, exit...") + return + + # prompt session + session = PromptSession( + history=SkipAuthFileHistory(Path(os.path.expanduser(config.history_location))), + style=STYLE, + auto_suggest=AutoSuggestFromHistory(), + complete_while_typing=True, + lexer=IRedisLexer(), + completer=IRedisCompleter( + hint=config.newbie_mode, completion_casing=config.completion_casing + ), + enable_open_in_editor=True, + tempfile_suffix=".redis", + ) + + # print hello message + greetings() + repl(client, session, enter_main_time) diff --git a/iredis/exceptions.py b/iredis/exceptions.py new file mode 100644 index 0000000..6aa45a1 --- /dev/null +++ b/iredis/exceptions.py @@ -0,0 +1,22 @@ +class IRedisException(Exception): + pass + + +class UsageError(IRedisException): + pass + + +class InvalidArguments(IRedisException): + """Invalid argument(s)""" + + +class NotRedisCommand(IRedisException): + """Not a Redis command""" + + +class AmbiguousCommand(IRedisException): + """Command is not finished, don't it's command's name""" + + +class NotSupport(IRedisException): + """IRedis currently not support this.""" diff --git a/iredis/key_bindings.py b/iredis/key_bindings.py new file mode 100644 index 0000000..fad4127 --- /dev/null +++ b/iredis/key_bindings.py @@ -0,0 +1,22 @@ +import logging + +from prompt_toolkit.filters import completion_is_selected +from prompt_toolkit.key_binding import KeyBindings + +logger = logging.getLogger(__name__) + +kb = KeyBindings() + + +@kb.add("enter", filter=completion_is_selected) +def _(event): + """Makes the enter key work as the tab key only when showing the menu. + In other words, don't execute query when enter is pressed in + the completion dropdown menu, instead close the dropdown menu + (accept current selection). + """ + logger.debug("Detected enter key.") + + event.current_buffer.complete_state = None + b = event.app.current_buffer + b.complete_state = None diff --git a/iredis/lexer.py b/iredis/lexer.py new file mode 100644 index 0000000..d1cfd57 --- /dev/null +++ b/iredis/lexer.py @@ -0,0 +1,101 @@ +from typing import Callable, Hashable + +from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer +from prompt_toolkit.document import Document +from prompt_toolkit.formatted_text.base import StyleAndTextTuples +from prompt_toolkit.lexers import Lexer, PygmentsLexer, SimpleLexer +from pygments.lexers.scripting import LuaLexer + +from .commands import split_command_args +from .exceptions import InvalidArguments, AmbiguousCommand +from .redis_grammar import CONST, get_command_grammar + + +def get_lexer_mapping(): + """ + Input command render color with lexer mapping below + + This converts token to styles in style.py + """ + # pygments token + # http://pygments.org/docs/tokens/ + lexers_dict = { + "key": SimpleLexer("class:key"), + "keys": SimpleLexer("class:key"), + "newkey": SimpleLexer("class:important-key"), + "destination": SimpleLexer("class:important-key"), + "member": SimpleLexer("class:member"), + "members": SimpleLexer("class:member"), + "value": SimpleLexer("class:string"), + "element": SimpleLexer("class:string"), + "svalue": SimpleLexer("class:string"), + "values": SimpleLexer("class:string"), + "lexmin": SimpleLexer("class:string"), + "lexmax": SimpleLexer("class:string"), + "bit": SimpleLexer("class:bit"), + "expiration": SimpleLexer("class:integer"), + "second": SimpleLexer("class:integer"), + "millisecond": SimpleLexer("class:integer"), + "start": SimpleLexer("class:integer"), + "float": SimpleLexer("class:integer"), + "end": SimpleLexer("class:integer"), + # stream id + "stream_id": SimpleLexer("class:integer"), + "group": SimpleLexer("class:group"), + "delta": SimpleLexer("class:integer"), + "offset": SimpleLexer("class:integer"), + "count": SimpleLexer("class:integer"), + "rank": SimpleLexer("class:integer"), + "index": SimpleLexer("class:index"), + "clientid": SimpleLexer("class:integer"), + "password": SimpleLexer("class:password"), + "min": SimpleLexer("class:integer"), + "max": SimpleLexer("class:integer"), + "score": SimpleLexer("class:integer"), + "timeout": SimpleLexer("class:integer"), + "position": SimpleLexer("class:integer"), + "cursor": SimpleLexer("class:integer"), + "pattern": SimpleLexer("class:pattern"), + "type": SimpleLexer("class:string"), + "fields": SimpleLexer("class:field"), + "field": SimpleLexer("class:field"), + "sfield": SimpleLexer("class:field"), + "parameter": SimpleLexer("class:field"), + "channel": SimpleLexer("class:channel"), + "double_lua": PygmentsLexer(LuaLexer), + "single_lua": PygmentsLexer(LuaLexer), + "command": SimpleLexer("class:command"), + "approximately": SimpleLexer("class:const"), + "username": SimpleLexer("class:username"), + } + + lexers_dict.update({key: SimpleLexer("class:const") for key in CONST}) + return lexers_dict + + +class IRedisLexer(Lexer): + """ + Lexer class that can dynamically returns any Lexer. + + :param get_lexer: Callable that returns a :class:`.Lexer` instance. + """ + + def __init__(self) -> None: + self._current_lexer = self._dummy = SimpleLexer() + + def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: + input_text = document.text + + try: + command, _ = split_command_args(input_text) + # compile grammar for this command + grammar = get_command_grammar(command) + self._current_lexer = GrammarLexer(grammar, lexers=get_lexer_mapping()) + except (InvalidArguments, AmbiguousCommand): + self._current_lexer = self._dummy + + return self._current_lexer.lex_document(document) + + def invalidation_hash(self) -> Hashable: + lexer = self.get_lexer() or self._dummy + return id(lexer) diff --git a/iredis/markdown.py b/iredis/markdown.py new file mode 100644 index 0000000..ff2b726 --- /dev/null +++ b/iredis/markdown.py @@ -0,0 +1,69 @@ +""" +Markdown render. + +use https://github.com/lepture/mistune render to html, then print with my style. +""" + +import logging +import mistune +import re +from prompt_toolkit.formatted_text import to_formatted_text, HTML + + +logger = logging.getLogger(__name__) + + +class TerminalRender(mistune.Renderer): + def _to_title(self, text): + return f"{text}\n{'='*len(text)}\n" + + def paragraph(self, text): + return text + "\n\n" + + def block_code(self, code, language=None): + code = "\n".join([" " + line for line in code.splitlines()]) + return super().block_code(code) + + def header(self, text, level, raw=None): + if level == 2: + header_text = self._to_title(text) + return super().header(header_text, 2) + return super().header(self._to_title(text), level) + + def list(self, body, ordered=True): + """Rendering list tags like ``<ul>`` and ``<ol>``. + + :param body: body contents of the list. + :param ordered: whether this list is ordered or not. + """ + tag = "ul" + if ordered: + tag = "ol" + return "<%s>%s</%s>\n" % (tag, body, tag) + + def list_item(self, text): + """Rendering list item snippet. Like ``<li>``.""" + return "<li> * %s</li>\n" % text + + +class RedisDocLexer(mistune.BlockLexer): + def enable_at_title(self): + self.rules.at_title = re.compile(r"^@(\w+) *(?:\n+|$)") # @example + self.default_rules.insert(0, "at_title") + + def parse_at_title(self, m): + text = m.group(1) + self.tokens.append({"type": "heading", "level": 2, "text": text}) + + +renderer = TerminalRender() +block_lexer = RedisDocLexer() +block_lexer.enable_at_title() +markdown_render = mistune.Markdown(renderer, block=block_lexer) + + +def render(text): + html_text = markdown_render(text) + logger.debug("[Document] {} ...".format(html_text)[:20]) + + return to_formatted_text(HTML(html_text)) diff --git a/iredis/processors.py b/iredis/processors.py new file mode 100644 index 0000000..029f80d --- /dev/null +++ b/iredis/processors.py @@ -0,0 +1,79 @@ +import logging + +from prompt_toolkit.layout.processors import ( + Processor, + Transformation, + TransformationInput, +) + +from .exceptions import InvalidArguments, AmbiguousCommand +from .commands import split_command_args + +logger = logging.getLogger(__name__) + + +class UserInputCommand: + """ + User inputted command in real time. + + ``UpdateBottomProcessor`` update it, and ``BottomToolbar`` read it + """ + + def __init__(self): + # command will always be upper case + self.command = None + + +class UpdateBottomProcessor(Processor): + """ + Update Footer display text while user input. + """ + + def __init__(self, command_holder, session): + # processor will call for internal_refresh, when input_text didn't + # change, don't run + self.session = session + self.command_holder = command_holder + + def apply_transformation( + self, transformation_input: TransformationInput + ) -> Transformation: + input_text = transformation_input.document.text + try: + command, _ = split_command_args(input_text) + except (InvalidArguments, AmbiguousCommand): + self.command_holder.command = None + else: + self.command_holder.command = command.upper() + + return Transformation(transformation_input.fragments) + + +class PasswordProcessor(Processor): + """ + Processor that turns masks the input. (For passwords.) + + :param char: (string) Character to be used. "*" by default. + """ + + def __init__(self, char: str = "*") -> None: + self.char = char + + def apply_transformation(self, ti: TransformationInput) -> Transformation: + input_text = ti.document.text + default_transformation = Transformation(ti.fragments) + try: + command, _ = split_command_args(input_text) + except (InvalidArguments, AmbiguousCommand): + return default_transformation + + if command.upper() != "AUTH": + return default_transformation + + fragments = [] + for style, text, *handler in ti.fragments: + if style == "class:password": + fragments.append((style, self.char * len(text), *handler)) + else: + fragments.append((style, text, *handler)) + return Transformation(fragments) diff --git a/iredis/redis_grammar.py b/iredis/redis_grammar.py new file mode 100644 index 0000000..f03459f --- /dev/null +++ b/iredis/redis_grammar.py @@ -0,0 +1,637 @@ +# noqa: F541 +""" +This module describes how to match a redis command to grammar token based on +regex. + +command_nodex: x means node? +command_keys: ends with s means there can be multiple <key> +""" +import logging +from functools import lru_cache + +from prompt_toolkit.contrib.regular_languages.compiler import compile +from .commands import command2syntax + +logger = logging.getLogger(__name__) +CONST = { + "failoverchoice": "TAKEOVER FORCE", + "withscores": "WITHSCORES", + "limit": "LIMIT", + "expiration": "EX PX", + "condition": "NX XX", + "keepttl": "KEEPTTL", + "operation": "AND OR XOR NOT", + "changed": "CH", + "incr": "INCR", + "resetchoice": "HARD SOFT", + "match": "MATCH", + "count_const": "COUNT", + "const_store": "STORE", + "const_storedist": "STOREDIST", + "type_const": "TYPE", + "type": "string list set zset hash stream", + "position_choice": "BEFORE AFTER", + "error": "TIMEOUT ERROR", + "async": "ASYNC", + "conntype": "NORMAL MASTER REPLICA PUBSUB", + "samples": "SAMPLES", + "slotsubcmd": "IMPORTING MIGRATING NODE STABLE", + "weights_const": "WEIGHTS", + "aggregate_const": "AGGREGATE", + "aggregate": "SUM MIN MAX", + "slowlogsub": "LEN RESET GET", + "shutdown": "SAVE NOSAVE", + "switch": "ON OFF SKIP", + "on_off": "ON OFF", + "const_id": "ID", + "addr": "ADDR", + "skipme": "SKIPME", + "yes": "YES NO", + "migratechoice": "COPY REPLACE", + "auth": "AUTH", + "const_keys": "KEYS", + "object": "REFCOUNT ENCODING IDLETIME FREQ HELP", + "subrestore": "REPLACE ABSTTL IDLETIME FREQ", + "distunit": "m km ft mi", + "geochoice": "WITHCOORD WITHDIST WITHHASH", + "order": "ASC DESC", + "pubsubcmd": "CHANNELS NUMSUB NUMPAT", + "scriptdebug": "YES NO SYNC", + "help": "HELP", + "stream": "STREAM", + "streams": "STREAMS", + "stream_create": "CREATE", + "stream_setid": "SETID", + "stream_destroy": "DESTROY", + "stream_delconsumer": "DELCONSUMER", + "stream_consumers": "CONSUMERS", + "stream_groups": "GROUPS", + "stream_group": "GROUP", + "maxlen": "MAXLEN", + "idel": "IDEL", + "time": "TIME", + "retrycount": "RETRYCOUNT", + "force": "FORCE", + "justid": "JUSTID", + "block": "BLOCK", + "noack": "NOACK", + "get": "GET", + "set": "SET", + "incrby": "INCRBY", + "overflow": "OVERFLOW", + "overflow_option": "WRAP SAT FAIL", + "version": "VERSION", + "schedule": "SCHEDULE", + "graphevent": ( + "ACTIVE-DEFRAG-CYCLE " + "AOF-FSYNC-ALWAYS " + "AOF-STAT " + "AOF-REWRITE-DIFF-WRITE " + "AOF-RENAME " + "AOF-WRITE " + "AOF-WRITE-ACTIVE-CHILD " + "AOF-WRITE-ALONE " + "AOF-WRITE-PENDING-FSYNC " + "COMMAND " + "EXPIRE-CYCLE " + "EVICTION-CYCLE " + "EVICTION-DEL " + "FAST-COMMAND " + "FORK " + "RDB-UNLINK-TEMP-FILE" + ), + "section": ( + "SERVER " + "CLIENTS " + "MEMORY " + "PERSISTENCE " + "STATS " + "REPLICATION " + "CPU " + "COMMANDSTATS " + "CLUSTER " + "KEYSPACE " + "ALL " + "DEFAULT " + ), + "redirect_const": "REDIRECT", + "prefix_const": "PREFIX", + "bcast_const": "BCAST", + "optin_const": "OPTIN", + "optout_const": "OPTOUT", + "noloop_const": "NOLOOP", + "reset_const": "RESET", + "const_user": "USER", + "full_const": "FULL", + "str_algo": "LCS", + "len_const": "LEN", + "idx_const": "IDX", + "minmatchlen_const": "MINMATCHLEN", + "withmatchlen_const": "WITHMATCHLEN", + "strings_const": "STRINGS", + "rank_const": "RANK", +} + + +def c(const_name): + const_values = CONST[const_name].split() + uppers = [x.lower() for x in const_values] + const_values.extend(uppers) + return "|".join(const_values) + + +VALID_TOKEN = r"""( +("([^"]|\\")*?") |# with double quotes +('([^']|\\')*?') |# with single quotes +([^\s"]+) # without quotes +)""" +PATTERN = fr"(?P<pattern>{VALID_TOKEN})" +VALID_SLOT = r"\d+" # TODO add range? max value:16384 +VALID_NODE = r"\d+" +NUM = r"\d+" +NNUM = r"-?\+?\(?\[?(\d+|inf)" # number cloud be negative +_FLOAT = r"-?(\d|\.|e)+" +LEXNUM = r"(\[\w+)|(\(\w+)|(\+)|(-)" + +SLOT = fr"(?P<slot>{VALID_SLOT})" +SLOTS = fr"(?P<slots>{VALID_SLOT}(\s+{VALID_SLOT})*)" +NODE = fr"(?P<node>{VALID_NODE})" +KEY = fr"(?P<key>{VALID_TOKEN})" +PREFIX = fr"(?P<prefix>{VALID_TOKEN})" +KEYS = fr"(?P<keys>{VALID_TOKEN}(\s+{VALID_TOKEN})*)" +DESTINATION = fr"(?P<destination>{VALID_TOKEN})" +NEWKEY = fr"(?P<newkey>{VALID_TOKEN})" +VALUE = fr"(?P<value>{VALID_TOKEN})" +VALUES = fr"(?P<values>{VALID_TOKEN}(\s+{VALID_TOKEN})*)" +ELEMENT = fr"(?P<element>{VALID_TOKEN})" # element for list +FIELDS = fr"(?P<fields>{VALID_TOKEN}(\s+{VALID_TOKEN})*)" +FIELD = fr"(?P<field>{VALID_TOKEN})" +SFIELD = fr"(?P<sfield>{VALID_TOKEN})" +SVALUE = fr"(?P<svalue>{VALID_TOKEN})" +MEMBER = fr"(?P<member>{VALID_TOKEN})" +MEMBERS = fr"(?P<members>{VALID_TOKEN}(\s+{VALID_TOKEN})*)" +COUNT = fr"(?P<count>{NNUM})" +LEN = fr"(?P<len>{NNUM})" +RANK = fr"(?P<rank>{NNUM})" +VERSION_NUM = fr"(?P<version_num>{NUM})" +MESSAGE = fr"(?P<message>{VALID_TOKEN})" +CHANNEL = fr"(?P<channel>{VALID_TOKEN})" +GROUP = fr"(?P<group>{VALID_TOKEN})" +CONSUMER = fr"(?P<consumer>{VALID_TOKEN})" +CATEGORYNAME = fr"(?P<categoryname>{VALID_TOKEN})" +USERNAME = fr"(?P<username>{VALID_TOKEN})" +RULE = fr"(?P<rule>{VALID_TOKEN})" +BIT = r"(?P<bit>0|1)" +FLOAT = fr"(?P<float>{_FLOAT})" +LONGITUDE = fr"(?P<longitude>{_FLOAT})" +LATITUDE = fr"(?P<latitude>{_FLOAT})" +CURSOR = fr"(?P<cursor>{NUM})" +PARAMETER = fr"(?P<parameter>{VALID_TOKEN})" +DOUBLE_LUA = r'(?P<double_lua>[^"]*)' +SINGLE_LUA = r"(?P<single_lua>[^']*)" +INTTYPE = r"(?P<inttype>(i|u)\d+)" +# IP re copied from: +# https://www.regular-expressions.info/ip.html +IP = r"""(?P<ip>(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\. + (25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\. + (25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\. + (25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]))""" +# Port re copied from: +# https://stackoverflow.com/questions/12968093/regex-to-validate-port-number +# pompt_toolkit limit: Exception: {4}-style repetition not yet supported +PORT = r"(?P<port>[1-9]|[1-5]?\d\d\d?\d?|6[1-4][0-9]\d\d\d|65[1-4]\d\d|655[1-2][0-9]|6553[1-5])" +EPOCH = fr"(?P<epoch>{NUM})" +PASSWORD = fr"(?P<password>{VALID_TOKEN})" +REPLICATIONID = fr"(?P<replicationid>{VALID_TOKEN})" +INDEX = r"(?P<index>(1[0-5]|\d))" +CLIENTID = fr"(?P<clientid>{NUM})" +SECOND = fr"(?P<second>{NUM})" +TIMESTAMP = fr"(?P<timestamp>{NUM})" +# TODO test lexer & completer for multi spaces in command +# For now, redis command can have one space at most +COMMAND = "(\s* (?P<command>[\w -]+))" +MILLISECOND = fr"(?P<millisecond>{NUM})" +TIMESTAMPMS = fr"(?P<timestampms>{NUM})" +ANY = r"(?P<any>.*)" # TODO deleted +START = fr"(?P<start>{NNUM})" +END = fr"(?P<end>{NNUM})" + +# for stream ids, special ids include: -, +, $, > and * +# please see: +# https://redis.io/topics/streams-intro#special-ids-in-the-streams-api +# stream id, DO NOT use r"" here, or the \+ will be two string +# NOTE: if miss the outer (), multi IDS won't work. +STREAM_ID = "(?P<stream_id>[T\d:>+*\-\$]+)" + +DELTA = fr"(?P<delta>{NNUM})" +OFFSET = fr"(?P<offset>{NUM})" # string offset, can't be negative +SHARP_OFFSET = f"(?P<offset>\#?{NUM})" # for bitfield command +MIN = fr"(?P<min>{NNUM})" +MAX = fr"(?P<max>{NNUM})" +POSITION = fr"(?P<position>{NNUM})" +TIMEOUT = fr"(?P<timeout>{NUM})" +SCORE = fr"(?P<score>{_FLOAT})" +LEXMIN = fr"(?P<lexmin>{LEXNUM})" +LEXMAX = fr"(?P<lexmax>{LEXNUM})" +WEIGHTS = fr"(?P<weights>{_FLOAT}(\s+{_FLOAT})*)" +IP_PORT = fr"(?P<ip_port>{IP}:{PORT})" +HOST = fr"(?P<host>{VALID_TOKEN})" + +# const choices +FAILOVERCHOICE = fr"(?P<failoverchoice>{c('failoverchoice')})" +WITHSCORES = fr"(?P<withscores>{c('withscores')})" +LIMIT = fr"(?P<limit>{c('limit')})" +EXPIRATION = fr"(?P<expiration>{c('expiration')})" +CONDITION = fr"(?P<condition>{c('condition')})" +OPERATION = fr"(?P<operation>{c('operation')})" +CHANGED = fr"(?P<changed>{c('changed')})" +INCR = fr"(?P<incr>{c('incr')})" +RESETCHOICE = fr"(?P<resetchoice>{c('resetchoice')})" +MATCH = fr"(?P<match>{c('match')})" +COUNT_CONST = fr"(?P<count_const>{c('count_const')})" +TYPE_CONST = fr"(?P<type_const>{c('type_const')})" +TYPE = fr"(?P<type>{c('type')})" +POSITION_CHOICE = fr"(?P<position_choice>{c('position_choice')})" +ERROR = fr"(?P<error>{c('error')})" +ASYNC = fr"(?P<async>{c('async')})" +CONNTYPE = fr"(?P<conntype>{c('conntype')})" +SAMPLES = fr"(?P<samples>{c('samples')})" +SLOTSUBCMD = fr"(?P<slotsubcmd>{c('slotsubcmd')})" +WEIGHTS_CONST = fr"(?P<weights_const>{c('weights_const')})" +AGGREGATE_CONST = fr"(?P<aggregate_const>{c('aggregate_const')})" +AGGREGATE = fr"(?P<aggregate>{c('aggregate')})" +SLOWLOGSUB = fr"(?P<slowlogsub>{c('slowlogsub')})" +SHUTDOWN = fr"(?P<shutdown>{c('shutdown')})" +SWITCH = fr"(?P<switch>{c('switch')})" +ON_OFF = fr"(?P<on_off>{c('on_off')})" +CONST_ID = fr"(?P<const_id>{c('const_id')})" +CONST_USER = fr"(?P<const_user>{c('const_user')})" +ADDR = fr"(?P<addr>{c('addr')})" +SKIPME = fr"(?P<skipme>{c('skipme')})" +YES = fr"(?P<yes>{c('yes')})" +MIGRATECHOICE = fr"(?P<migratechoice>{c('migratechoice')})" +AUTH = fr"(?P<auth>{c('auth')})" +CONST_KEYS = fr"(?P<const_keys>{c('const_keys')})" +OBJECT = fr"(?P<object>{c('object')})" +SUBRESTORE = fr"(?P<subrestore>{c('subrestore')})" +DISTUNIT = fr"(?P<distunit>{c('distunit')})" +GEOCHOICE = fr"(?P<geochoice>{c('geochoice')})" +ORDER = fr"(?P<order>{c('order')})" +CONST_STORE = fr"(?P<const_store>{c('const_store')})" +CONST_STOREDIST = fr"(?P<const_storedist>{c('const_storedist')})" +PUBSUBCMD = fr"(?P<pubsubcmd>{c('pubsubcmd')})" +SCRIPTDEBUG = fr"(?P<scriptdebug>{c('scriptdebug')})" +HELP = fr"(?P<help>{c('help')})" +STREAM = fr"(?P<stream>{c('stream')})" +STREAM_GROUPS = fr"(?P<stream_groups>{c('stream_groups')})" +STREAM_GROUP = fr"(?P<stream_group>{c('stream_group')})" +STREAM_CONSUMERS = fr"(?P<stream_consumers>{c('stream_consumers')})" +STREAM_CREATE = fr"(?P<stream_create>{c('stream_create')})" +STREAM_SETID = fr"(?P<stream_setid>{c('stream_setid')})" +STREAM_DESTROY = fr"(?P<stream_destroy>{c('stream_destroy')})" +STREAM_DELCONSUMER = fr"(?P<stream_delconsumer>{c('stream_delconsumer')})" +MAXLEN = fr"(?P<maxlen>{c('maxlen')})" +APPROXIMATELY = r"(?P<approximately>~)" +IDEL = fr"(?P<idel>{c('idel')})" +TIME = fr"(?P<time>{c('time')})" +RETRYCOUNT = fr"(?P<retrycount>{c('retrycount')})" +FORCE = fr"(?P<force>{c('force')})" +JUSTID = fr"(?P<justid>{c('justid')})" +BLOCK = fr"(?P<block>{c('block')})" +STREAMS = fr"(?P<streams>{c('streams')})" +NOACK = fr"(?P<noack>{c('noack')})" +GET = fr"(?P<get>{c('get')})" +SET = fr"(?P<set>{c('set')})" +INCRBY = fr"(?P<incrby>{c('incrby')})" +OVERFLOW = fr"(?P<overflow>{c('overflow')})" +OVERFLOW_OPTION = fr"(?P<overflow_option>{c('overflow_option')})" +KEEPTTL = fr"(?P<keepttl>{c('keepttl')})" +GRAPHEVENT = fr"(?P<graphevent>{c('graphevent')})" +VERSION = fr"(?P<version>{c('version')})" +SECTION = fr"(?P<section>{c('section')})" +SCHEDULE = fr"(?P<schedule>{c('schedule')})" + +REDIRECT_CONST = fr"(?P<redirect_const>{c('redirect_const')})" +PREFIX_CONST = fr"(?P<prefix_const>{c('prefix_const')})" +BCAST_CONST = fr"(?P<bcast_const>{c('bcast_const')})" +OPTIN_CONST = fr"(?P<optin_const>{c('optin_const')})" +OPTOUT_CONST = fr"(?P<optout_const>{c('optout_const')})" +NOLOOP_CONST = fr"(?P<noloop_const>{c('noloop_const')})" + +RESET_CONST = fr"(?P<reset_const>{c('reset_const')})" +FULL_CONST = fr"(?P<full_const>{c('full_const')})" + +STR_ALGO = fr"(?P<str_algo>{c('str_algo')})" +LEN_CONST = fr"(?P<len_const>{c('len_const')})" +IDX_CONST = fr"(?P<idx_const>{c('idx_const')})" +MINMATCHLEN_CONST = fr"(?P<minmatchlen_const>{c('minmatchlen_const')})" +WITHMATCHLEN_CONST = fr"(?P<withmatchlen_const>{c('withmatchlen_const')})" +STRINGS_CONST = fr"(?P<strings_const>{c('strings_const')})" +RANK_CONST = fr"(?P<rank_const>{c('rank_const')})" + +command_grammar = compile(COMMAND) + +# Here are the core grammars, those are tokens after ``command``. +# E.g. SET command's syntax is "SET key value" +# Then it's grammar here is r"\s+ key \s+ value \s*", we needn't add `command` +# here because every syntaxes starts with `command` so we will prepend `command` +# in get_command_grammar function. +GRAMMAR = { + "command_key": fr"\s+ {KEY} \s*", + "command_pattern": fr"\s+ {PATTERN} \s*", + "command_georadiusbymember": fr""" + \s+ {KEY} \s+ {MEMBER} + \s+ {FLOAT} \s+ {DISTUNIT} + (\s+ {GEOCHOICE})* + (\s+ {COUNT_CONST} \s+ {COUNT})? + (\s+ {ORDER})? + (\s+ {CONST_STORE} \s+ {KEY})? + (\s+ {CONST_STOREDIST} \s+ {KEY})? \s*""", + "command_command": fr"\s+ {COMMAND} \s*", + "command_slots": fr"\s+ {SLOTS} \s*", + "command_node": fr"\s+ {NODE} \s*", + "command_slot": fr"\s+ {SLOT} \s*", + "command_failoverchoice": fr"\s+ {FAILOVERCHOICE} \s*", + "command_resetchoice": fr"\s+ {RESETCHOICE} \s*", + "command_slot_count": fr"\s+ {SLOT} \s+ {COUNT} \s*", + "command_key_samples_count": fr""" + \s+ {KEY} \s+ {SAMPLES} \s+ {COUNT} \s*""", + "command": r"\s*", + "command_ip_port": fr"\s+ {IP} \s+ {PORT} \s*", + "command_epoch": fr"\s+ {EPOCH} \s*", + "command_yes": fr"\s+ {YES} \s*", + "command_sectionx": fr"(\s+ {SECTION})? \s*", + "command_asyncx": fr"(\s+ {ASYNC})? \s*", + "command_slot_slotsubcmd_nodex": fr""" + \s+ {SLOT} \s+ {SLOTSUBCMD} (\s+ {NODE})? \s*""", + "command_password": fr"\s+ {PASSWORD} \s*", + "command_usernamex_password": fr"(\s+ {USERNAME})? \s+ {PASSWORD} \s*", + "command_message": fr"\s+ {MESSAGE} \s*", + "command_messagex": fr"(\s+{MESSAGE})? \s*", + "command_index": fr"\s+ {INDEX} \s*", + "command_index_index": fr"\s+ {INDEX} \s+ {INDEX} \s*", + "command_type_conntype_x": fr""" + (\s+ {TYPE_CONST} \s+ {CONNTYPE})? \s*""", + "command_clientid_errorx": fr"\s+ {CLIENTID} (\s+ {ERROR})? \s*", + "command_keys": fr"\s+ {KEYS} \s*", + "command_key_value": fr"\s+ {KEY} \s+ {VALUE} \s*", + "command_parameter_value": fr"\s+ {PARAMETER} \s+ {VALUE} \s*", + "command_parameter": fr"\s+ {PARAMETER} \s+ {VALUE} \s*", + "command_value": fr"\s+ {VALUE} \s*", + "command_key_second": fr"\s+ {KEY} \s+ {SECOND} \s*", + "command_key_timestamp": fr"\s+ {KEY} \s+ {TIMESTAMP} \s*", + "command_key_index": fr"\s+ {KEY} \s+ {INDEX} \s*", + "command_key_millisecond": fr"\s+ {KEY} \s+ {MILLISECOND} \s*", + "command_key_timestampms": fr"\s+ {KEY} \s+ {TIMESTAMPMS} \s*", + "command_key_newkey": fr"\s+ {KEY} \s+ {NEWKEY} \s*", + "command_newkey_keys": fr"\s+ {NEWKEY} \s+ {KEYS} \s*", + "command_key_newkey_timeout": fr"\s+ {KEY} \s+ {NEWKEY} \s+ {TIMEOUT} \s*", + "command_keys_timeout": fr"\s+ {KEYS} \s+ {TIMEOUT} \s*", + "command_count_timeout": fr"\s+ {COUNT} \s+ {TIMEOUT} \s*", + "command_timeout": fr"\s+ {TIMEOUT} \s*", + "command_key_positionchoice_pivot_value": fr""" + \s+ {KEY} \s+ {POSITION_CHOICE} \s+ {VALUE} \s+ {VALUE} \s*""", + "command_pass": fr"\s+ {ANY} \s*", + "command_any": fr"\s+ {ANY} \s*", + "command_set": fr""" + \s+ {KEY} \s+ {VALUE} + ( + (\s+ {EXPIRATION} \s+ {MILLISECOND})| + (\s+ {CONDITION})| + (\s+ {KEEPTTL}) + )* + \s*""", + "command_key_start_end_x": fr"\s+ {KEY} (\s+ {START} \s+ {END})? \s*", + "command_key_start_end": fr"\s+ {KEY} \s+ {START} \s+ {END} \s*", + "command_key_delta": fr"\s+ {KEY} \s+ {DELTA} \s*", + "command_key_offset_value": fr"\s+ {KEY} \s+ {OFFSET} \s+ {VALUE} \s*", + "command_key_field_value": fr"\s+ {KEY} (\s+ {FIELD} \s+ {VALUE})+ \s*", + "command_key_offset_bit": fr"\s+ {KEY} \s+ {OFFSET} \s+ {BIT} \s*", + "command_key_offset": fr"\s+ {KEY} \s+ {OFFSET} \s*", + "command_key_position": fr"\s+ {KEY} \s+ {POSITION} \s*", + "command_key_position_value": fr"\s+ {KEY} \s+ {POSITION} \s+ {VALUE} \s*", + "command_key_second_value": fr"\s+ {KEY} \s+ {SECOND} \s+ {VALUE} \s*", + "command_key_float": fr"\s+ {KEY} \s+ {FLOAT} \s*", + "command_key_valuess": fr"(\s+ {KEY} \s+ {VALUE})+ \s*", + "command_key_values": fr"\s+ {KEY} \s+ {VALUES} \s*", + "command_key_millisecond_value": fr"\s+ {KEY} \s+ {MILLISECOND} \s+ {VALUE} \s*", + "command_operation_key_keys": fr"\s+ {OPERATION} \s+ {KEY} \s+ {KEYS} \s*", + "command_key_bit_start_end": fr"\s+ {KEY} \s+ {BIT} (\s+ {START})? (\s+ {END})? \s*", + "command_key_members": fr"\s+ {KEY} \s+ {MEMBERS} \s*", + "command_geodist": fr"\s+ {KEY} \s+ {MEMBER} \s+ {MEMBER} (\s+ {DISTUNIT})? \s*", + "command_key_longitude_latitude_members": fr""" + \s+ {KEY} (\s+ {LONGITUDE} \s+ {LATITUDE} \s {MEMBER})+ \s*""", + "command_destination_keys": fr"\s+ {DESTINATION} \s+ {KEYS} \s*", + "command_object_key": fr"\s+ {OBJECT} \s+ {KEY} \s*", + "command_key_member": fr"\s+ {KEY} \s+ {MEMBER} \s*", + "command_key_newkey_member": fr"\s+ {KEY} \s+ {NEWKEY} \s+ {MEMBER} \s*", + "command_key_count_x": fr"\s+ {KEY} (\s+ {COUNT})? \s*", + "command_key_min_max": fr"\s+ {KEY} \s+ {MIN} \s+ {MAX} \s*", + "command_key_condition_changed_incr_score_members": fr""" + \s+ {KEY} (\s+ {CONDITION})? + (\s+ {CHANGED})? + (\s+ {INCR})? + (\s+ {SCORE} \s+ {MEMBER})+ \s*""", + "command_key_float_member": fr"\s+ {KEY} \s+ {FLOAT} \s+ {MEMBER} \s*", + "command_key_lexmin_lexmax": fr"\s+ {KEY} \s+ {LEXMIN} \s+ {LEXMAX} \s*", + "command_key_start_end_withscores_x": fr""" + \s+ {KEY} \s+ {START} \s+ {END} (\s+ {WITHSCORES})? \s*""", + "command_key_lexmin_lexmax_limit_offset_count": fr""" + \s+ {KEY} \s+ {LEXMIN} \s+ {LEXMAX} + (\s+ {LIMIT} \s+ {OFFSET} \s+ {COUNT})? \s*""", + "command_key_min_max_withscore_x_limit_offset_count_x": fr""" + \s+ {KEY} \s+ {MIN} \s+ {MAX} (\s+ {WITHSCORES})? + (\s+ {LIMIT} \s+ {OFFSET} \s+ {COUNT})? \s*""", + "command_cursor_match_pattern_count_type": fr""" + \s+ {CURSOR} (\s+ {MATCH} \s+ {PATTERN})? + (\s+ {COUNT_CONST} \s+ {COUNT})? (\s+ {TYPE_CONST} \s+ {TYPE})? \s*""", + "command_key_cursor_match_pattern_count": fr"""\s+ {KEY} + \s+ {CURSOR} (\s+ {MATCH} \s+ {PATTERN})? (\s+ {COUNT_CONST} \s+ {COUNT})? \s*""", + "command_key_fields": fr"\s+ {KEY} \s+ {FIELDS} \s*", + "command_key_field": fr"\s+ {KEY} \s+ {FIELD} \s*", + "command_key_field_delta": fr"\s+ {KEY} \s+ {FIELD} \s+ {DELTA} \s*", + "command_key_field_float": fr"\s+ {KEY} \s+ {FIELD} \s+ {FLOAT} \s*", + "command_key_fieldvalues": fr"\s+ {KEY} (\s+ {FIELD} \s+ {VALUE})+ \s*", + "command_slowlog": fr"\s+ {SLOWLOGSUB} \s+ {NUM} \s*", + "command_switch": fr"\s+ {SWITCH} \s*", + "command_schedulex": fr"(\s+ {SCHEDULE})? \s*", + "command_clientkill": fr""" + ( + (\s+ {IP_PORT})| + (\s+ {ADDR} \s+ {IP_PORT})| + (\s+ {CONST_ID} \s+ {CLIENTID})| + (\s+ {TYPE_CONST} \s+ {CONNTYPE})| + (\s+ {CONST_USER} \s+ {USERNAME})| + (\s+ {SKIPME} \s+ {YES}) + )+ \s*""", + "command_migrate": fr""" + \s+ {HOST} \s+ {PORT} + \s+ {KEY} \s+ {INDEX} \s+ {TIMEOUT} + (\s+ {MIGRATECHOICE})? + ( + (\s+ {AUTH} \s+ {PASSWORD})| + (\s+ {AUTH} \s+ {USERNAME} \s+ {PASSWORD}) + )? + (\s+ {CONST_KEYS} \s+ {KEYS})? + \s*""", + "command_radius": fr"""\s+ {KEY} + \s+ {LONGITUDE} \s+ {LATITUDE} \s+ {FLOAT} \s+ {DISTUNIT} + (\s+ {GEOCHOICE})* (\s+ {COUNT_CONST} \s+ {COUNT})? + (\s+ {ORDER})? + (\s+ {CONST_STORE} \s+ {KEY})? + (\s+ {CONST_STOREDIST} \s+ {KEY})? \s*""", + "command_restore": fr"""\s+ {KEY} + \s+ {TIMEOUT} \s+ {VALUE} (\s+ {SUBRESTORE} \s+ {SECOND})? \s*""", + "command_pubsubcmd_channels": fr"\s+ {PUBSUBCMD} (\s+ {CHANNEL})+ \s*", + "command_channel_message": fr"\s+ {CHANNEL} \s+ {MESSAGE} \s*", + "command_channels": fr"(\s+ {CHANNEL})+ \s*", + "command_lua_any": fr"""(\s+"{DOUBLE_LUA}")? (\s+'{SINGLE_LUA}')? \s+ {ANY} \s*""", + "command_scriptdebug": fr"\s+ {SCRIPTDEBUG} \s*", + "command_shutdown": fr"\s+ {SHUTDOWN} \s*", + "command_key_start_end_countx": fr"""\s+ {KEY} + \s+ {STREAM_ID} + \s+ {STREAM_ID} + (\s+ {COUNT_CONST} \s+ {COUNT})? + \s*""", + "command_xgroup": fr""" + ( + (\s+ {STREAM_CREATE} \s+ {KEY} \s+ {GROUP} \s+ {STREAM_ID})| + (\s+ {STREAM_SETID} \s+ {KEY} \s+ {GROUP} \s+ {STREAM_ID})| + (\s+ {STREAM_DESTROY} \s+ {KEY} \s+ {GROUP})| + (\s+ {STREAM_DELCONSUMER} \s+ {KEY} \s+ {GROUP} \s+ {CONSUMER}) + ) + \s*""", + "command_key_group_ids": fr""" + \s+ {KEY} \s+ {GROUP} (\s+ {STREAM_ID})+ \s*""", + "command_key_ids": fr""" + \s+ {KEY} (\s+ {STREAM_ID})+ \s*""", + "command_xinfo": fr""" + ( + (\s+ {STREAM_CONSUMERS} \s+ {KEY} \s+ {GROUP})| + (\s+ {STREAM_GROUPS} \s+ {KEY})| + (\s+ {STREAM} \s+ {KEY} + (\s+ {FULL_CONST})? + (\s+ {COUNT_CONST} \s+ {COUNT})? + )| + (\s+ {HELP}) + ) + \s*""", + "command_xpending": fr""" + \s+ {KEY} \s+ {GROUP} + (\s+ {STREAM_ID} \s+ {STREAM_ID} \s+ {COUNT})? + (\s+ {CONSUMER})? + \s*""", + "command_xadd": fr""" + \s+ {KEY} + (\s+ {MAXLEN} (\s+ {APPROXIMATELY})? \s+ {COUNT})? + \s+ {STREAM_ID} + (\s+ {SFIELD} \s+ {SVALUE})+ \s*""", + "command_key_maxlen": fr""" + \s+ {KEY} \s+ {MAXLEN} (\s+ {APPROXIMATELY})? \s+ {COUNT} + \s*""", + "command_xclaim": fr""" + \s+ {KEY} \s+ {GROUP} \s+ {CONSUMER} \s+ {MILLISECOND} + (\s+ {STREAM_ID})+ + (\s+ {IDEL} \s+ {MILLISECOND})? + (\s+ {TIME} \s+ {TIMESTAMP})? + (\s+ {RETRYCOUNT} \s+ {COUNT})? + (\s+ {FORCE})? + (\s+ {JUSTID})? + \s*""", + "command_xread": fr""" + (\s+ {COUNT_CONST} \s+ {COUNT})? + (\s+ {BLOCK} \s+ {MILLISECOND})? + \s+ {STREAMS} + \s+ {KEYS} + (\s+ {STREAM_ID})+ + \s*""", + "command_xreadgroup": fr""" + \s+ {STREAM_GROUP} \s+ {GROUP} \s+ {CONSUMER} + (\s+ {COUNT_CONST} \s+ {COUNT})? + (\s+ {BLOCK} \s+ {MILLISECOND})? + (\s+ {NOACK})? + \s+ {STREAMS} + \s+ {KEYS} + (\s+ {STREAM_ID})+ + \s*""", + "command_bitfield": fr""" + \s+ {KEY} + ( + (\s+ {GET} \s+ {INTTYPE} \s+ {SHARP_OFFSET})| + (\s+ {SET} \s+ {INTTYPE} \s+ {SHARP_OFFSET} \s+ {VALUE})| + (\s+ {INCRBY} \s+ {INTTYPE} \s+ {SHARP_OFFSET} \s+ {VALUE})| + (\s+ {OVERFLOW} \s+ {OVERFLOW_OPTION}) + )+ + \s*""", + "command_replicationid_offset": fr"\s+ {REPLICATIONID} \s+ {OFFSET} \s*", + "command_graphevent": fr"\s+ {GRAPHEVENT} \s*", + "command_graphevents": fr"(\s+ {GRAPHEVENT})* \s*", + # before redis 5: lolwut 5 1 + # start from redis 6: lolwut VERSION 5 1 + "command_version": fr"(\s+ {VERSION} \s+ {VERSION_NUM})? (\s+ {ANY})? \s*", + "command_client_tracking": fr""" + \s+ {ON_OFF} + ( + (\s+ {REDIRECT_CONST} \s+ {CLIENTID})| + (\s+ {PREFIX_CONST} \s+ {PREFIX})| + (\s+ {BCAST_CONST})| + (\s+ {OPTIN_CONST})| + (\s+ {OPTOUT_CONST})| + (\s+ {NOLOOP_CONST}) + )* + \s*""", + "command_categorynamex": fr"(\s+ {CATEGORYNAME})? \s*", + "command_usernames": fr"(\s+ {USERNAME})+ \s*", + "command_username": fr"\s+ {USERNAME} \s*", + "command_count_or_resetx": fr"( (\s+ {COUNT}) | (\s+ {RESET_CONST}) )? \s*", + "command_username_rules": fr"\s+ {USERNAME} (\s+ {RULE})* \s*", + "command_count": fr"(\s+ {COUNT})? \s*", + "command_stralgo": fr""" + ( + \s+ {STR_ALGO} + ( + (\s+ {CONST_KEYS} \s+ {KEYS})| + (\s+ {STRINGS_CONST} \s+ {VALUES}) + ) + (\s+ {IDX_CONST})? + (\s+ {LEN_CONST})? + (\s+ {MINMATCHLEN_CONST} \s+ {LEN})? + (\s+ {WITHMATCHLEN_CONST})? + ) + \s*""", + "command_lpos": fr""" + \s+ {KEY} \s+ {ELEMENT} + ( + (\s+ {RANK_CONST} \s+ {RANK})| + (\s+ {COUNT_CONST} \s+ {COUNT})| + (\s+ {MAXLEN} \s+ {LEN}) + )* + \s*""", +} + +pipeline = r"(?P<shellcommand>\|.*)?" + + +@lru_cache(maxsize=256) +def get_command_grammar(command): + """ + :param command: command name in upper case. This command must be raw user + input, otherwise can't match in lexer, cause this command to be invalid; + """ + syntax_name = command2syntax[" ".join(command.split()).upper()] + syntax = GRAMMAR.get(syntax_name) + + # If a command is not supported yet, (e.g. command from latest version added + # by Redis recently, or command from third Redis module.) return a defualt + # grammar, so the lexer and completion won't be activated. + if syntax is None: + return command_grammar + # prepend command token for this syntax + command_allow_multi_spaces = command.replace(r" ", r"\s+") + syntax = fr"\s* (?P<command>{command_allow_multi_spaces}) " + syntax + # allow user input pipeline to redirect to shell, like `get json | jq .` + syntax += pipeline + + logger.info(f"syxtax: {syntax}") + + return compile(syntax) diff --git a/iredis/renders.py b/iredis/renders.py new file mode 100644 index 0000000..02470f0 --- /dev/null +++ b/iredis/renders.py @@ -0,0 +1,415 @@ +""" +Render redis-server responses. +This module will be auto loaded to callbacks. + +func(redis-response) -> formatted result(str) +""" +import logging +import time +from distutils.version import StrictVersion + +from prompt_toolkit.formatted_text import FormattedText + +from .commands import command2callback +from .config import config +from .utils import double_quotes, ensure_str, nativestr + +logger = logging.getLogger(__name__) +NEWLINE_TUPLE = ("", "\n") +NIL_TUPLE = ("class:type", "(nil)") +NIL = FormattedText([NIL_TUPLE]) +EMPTY_LIST = FormattedText([("class:type", "(empty list or set)")]) + + +class OutputRender: + """Render redis output""" + + @staticmethod + def get_render(command_name): + """Dynamic render output due to command name.""" + command_upper = " ".join(command_name.split()).upper() + callback_name = command2callback.get(command_upper) + + # using `render_list_or_string` as default render. + if callback_name is None: + callback = OutputRender.render_list_or_string + else: + callback = getattr( + OutputRender, callback_name, OutputRender.render_list_or_string + ) + + logger.info( + f"[render] Find callback {callback_name}, for command: {command_name}" + ) + return callback + + @staticmethod + def render_raw(value): + """ + Render for all kinds, list, string, bulkstring, int + + :return : bytes + """ + if value is None: + return b"" + if isinstance(value, bytes): + return value + if isinstance(value, int): + return str(value).encode() + if isinstance(value, list): + return _render_raw_list(value) + + @staticmethod + def render_bulk_string(value): + if value is None: + return NIL + return double_quotes(ensure_str(value)) + + @staticmethod + def render_bulk_string_decode(value): + """ + Only for server group commands, no double quoted, always displayed as + utf-8 decoded. + """ + decoded = nativestr(value) + splitted = "\n".join(decoded.splitlines()) # get rid of last newline + return splitted + + @staticmethod + def render_nested_pair(value): + """ + For redis internel responses. + Always decode with utf-8 + Render nested list. + Items come as pairs. + """ + return FormattedText(_render_pair(value, 0)) + + @staticmethod + def render_int(value): + if value is None: + return NIL + return FormattedText([("class:type", "(integer) "), ("", str(value))]) + + @staticmethod + def render_unixtime(value): + rendered_int = OutputRender.render_int(value) + explained_date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(value))) + rendered_int.extend( + [ + NEWLINE_TUPLE, + ("class:type", "(local time)"), + ("", " "), + ("", explained_date), + ] + ) + return rendered_int + + @staticmethod + def render_time(value): + unix_timestamp, millisecond = value[0].decode(), value[1].decode() + explained_date = time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(int(unix_timestamp)) + ) + rendered = [ + ("class:type", "(unix timestamp) "), + ("", unix_timestamp), + NEWLINE_TUPLE, + ("class:type", "(millisecond) "), + ("", millisecond), + NEWLINE_TUPLE, + ("class:type", "(convert to local timezone) "), + ("", f"{explained_date}.{millisecond}"), + ] + return FormattedText(rendered) + + @staticmethod + def render_list(text, style="class:string"): + """ + Render callback for redis Array Reply + Note: Cloud be null in it. + """ + str_items = [] + for item in text: + if item is None: + str_items.append(None) + else: + str_item = ensure_str(item) + double_quoted = double_quotes(str_item) + str_items.append(double_quoted) + rendered = _render_list(text, str_items, style) + return FormattedText(rendered) + + @staticmethod + def render_list_or_string(text): + if isinstance(text, list): + return OutputRender.render_list(text) + return OutputRender.render_bulk_string(text) + + @staticmethod + def render_string_or_int(text): + if isinstance(text, int): + return OutputRender.render_int(text) + return OutputRender.render_bulk_string(text) + + @staticmethod + def render_error(error_msg): + text = ensure_str(error_msg) + return FormattedText([("class:type", "(error) "), ("class:error", text)]) + + @staticmethod + def render_simple_string(text): + """ + If response is b'OK', render simple string always with success color. + If Error happend, error will be rendered by ``render_error`` + """ + if text is None: + return NIL + text = ensure_str(text) + return FormattedText([("class:success", text)]) + + @staticmethod + def render_transaction_queue(text): + """ + Used when client session is in a transaction. + + Response message should be "QUEUE" or Error. + """ + text = ensure_str(text) + return FormattedText([("class:queued", text)]) + + @staticmethod + def render_members(items): + if not config.withscores: + return OutputRender.render_list(items, "class:member") + + if not items: + return EMPTY_LIST + str_items = ensure_str(items) + + members = [item for item in str_items[::2]] + scores = [item for item in str_items[1::2]] + logger.debug(f"[MEMBERS] {members}") + logger.debug(f"[SCORES] {scores}") + # render display + double_quoted = double_quotes(members) + index_width = len(str(len(double_quoted))) + score_width = max(len(score) for score in scores) + rendered = [] + for index, item in enumerate(double_quoted): + index_const_width = f"{index+1:{index_width}})" + rendered.append(("", index_const_width)) + # add a space between index and member + rendered.append(("", " ")) + # add score + rendered.append(("class:integer", f"{scores[index]:{score_width}} ")) + # add member + if item is None: + rendered.append(NIL_TUPLE) + else: + rendered.append(("class:member", item)) + + # add a newline for eachline + if index + 1 < len(double_quoted): + rendered.append(NEWLINE_TUPLE) + return FormattedText(rendered) + + @staticmethod + def render_hash_pairs(response): + # render hash pairs + if not response: + return EMPTY_LIST + str_items = ensure_str(response) + fields = str_items[0::2] + values = str_items[1::2] + # render display + index_width = len(str(len(fields))) + values_quoted = double_quotes(values) + fields_quoted = double_quotes(fields) + rendered = [] + for index, item in enumerate(fields_quoted): + index_const_width = f"{index+1:{index_width}})" + rendered.append(("", index_const_width)) + rendered.append(("", " ")) + rendered.append(("class:field", item)) + rendered.append(NEWLINE_TUPLE) + rendered.append(("", " " * (len(index_const_width) + 1))) + value = values_quoted[index] + if value is None: + rendered.append(NIL_TUPLE) + else: + rendered.append(("class:string", value)) + + # add a newline for eachline + if index + 1 < len(fields): + rendered.append(NEWLINE_TUPLE) + return FormattedText(rendered) + + @staticmethod + def render_slowlog(raw): + fields = ["Slow log id", "Start at", "Running time(ms)", "Command"] + if StrictVersion(config.version) > StrictVersion("4.0"): + fields.extend(["Client IP and port", "Client name"]) + + rendered = [] + text = ensure_str(raw) + index_width = len(str(len(text))) + for index, slowlog in enumerate(text): + index_str = f"{index+1:{index_width}}) " + rendered.append(("", index_str)) + for field, value in zip(fields, slowlog): + if field == "Command": + value = " ".join(value) + if field != "Slow log id": + display_field = " " * len(index_str) + field + else: + display_field = field + logger.debug(f"field: {field}, value: {value}") + rendered.extend( + [ + ("class:field", f"{display_field}: "), + ("class:string", value), + NEWLINE_TUPLE, + ] + ) + + return FormattedText(rendered[:-1]) + + @staticmethod + def render_subscribe(raw): + """ + message type; + channel; + message; + see: https://redis.io/topics/pubsub#format-of-pushed-messages + """ + logger.info(raw) + if raw[1] is None: + raw[1] = "all" + mtype, *channel, message = ensure_str(raw) + # PUNSUBSCRIBE, 4 args + channel = ":".join(channel) + return FormattedText( + [ + ("", f"{mtype:<9} from "), # 9 is len("subscribe") + ("class:channel", channel), + ("", ": "), # 9 is len("subscribe") + ("class:string", f"{message}"), + ] + ) + + @staticmethod + def command_keys(items): + return OutputRender.render_list(items, "class:key") + + @staticmethod + def command_scan(response): + """ + Render Scan command result. + see: https://redis.io/commands/scan + """ + return _render_scan(OutputRender.command_keys, response) + + @staticmethod + def command_sscan(response): + return _render_scan(OutputRender.render_members, response) + + @staticmethod + def command_zscan(response): + return _render_scan(OutputRender.render_members, response) + + @staticmethod + def command_hscan(response): + return _render_scan(OutputRender.render_hash_pairs, response) + + @staticmethod + def command_hkeys(response): + return OutputRender.render_list(response, "class:field") + + @staticmethod + def render_bytes(response): + return response.rstrip(b"\n") # there is a new line in `write_result` + + @staticmethod + def default_render(text): + pass + + +def _render_raw_list(bytes_items): + flatten_items = [] + for item in bytes_items: + if item is None: + flatten_items.append(b"") + elif isinstance(item, bytes): + flatten_items.append(item) + elif isinstance(item, int): + flatten_items.append(str(item).encode()) + elif isinstance(item, list): + flatten_items.append(_render_raw_list(item)) + return b"\n".join(flatten_items) + + +def _render_list(byte_items, str_items, style=None, pre_space=0): + """Complute the newline/number-width/lineno, + render list to FormattedText + """ + if not str_items: + return EMPTY_LIST + + index_width = len(str(len(str_items))) + rendered = [] + for index, item in enumerate(str_items): + indent_spaces = (index + 1 != 1) * pre_space * " " + if indent_spaces: + rendered.append(("", indent_spaces)) # add a space before item + + index_const_width = f"{index+1:{index_width}})" + rendered.append(("", index_const_width)) + # list item + rendered.append(("", " ")) # add a space before item + if item is None: + rendered.append(NIL_TUPLE) + elif isinstance(item, str): + rendered.append((style, item)) + else: # it's a nested list + # if config.raw == True, never will get there + sublist = _render_list(None, item, style, pre_space + index_width + 2) + rendered.extend(sublist) + + # add a newline for eachline + if index + 1 < len(str_items): + rendered.append(NEWLINE_TUPLE) + return rendered + + +def _render_scan(render_response, response): + cursor, responses = response + + rendered = [ + ("class:type", "(cursor) "), + ("class:integer", cursor.decode()), + ("", "\n"), + ] + rendered_keys = render_response(responses) + return FormattedText(rendered + rendered_keys) + + +def _render_pair(pairs, indent): + keys = [item for item in pairs[::2]] + values = [item for item in pairs[1::2]] + rendered = [] + for key, value in zip(keys, values): + key = ensure_str(key, decode="utf-8") + value = ensure_str(value, decode="utf-8") + rendered.append(("class:string", f"{' '*4*indent}{key}: ")) + if isinstance(value, list): + rendered.append(NEWLINE_TUPLE) + rendered.extend(_render_pair(value, indent + 1)) + else: + rendered.append(("class:value", value)) + rendered.append(NEWLINE_TUPLE) + return rendered[:-1] # remove last newline + + +# TODO +# special list render, bzpopmax, key-value pair diff --git a/iredis/style.py b/iredis/style.py new file mode 100644 index 0000000..9fd6eb6 --- /dev/null +++ b/iredis/style.py @@ -0,0 +1,100 @@ +from prompt_toolkit.styles import Style, merge_styles + +override_style = Style([("bottom-toolbar", "noreverse")]) + +style = REDIS_TOKEN = { + "key": "#33aa33", + "important-key": "#058B06", + "pattern": "bold #33aa33", + "string": "#FD971F", + "member": "#FD971F", + "command": "bold #008000", + "integer": "#AE81FF", + "const": "bold #AE81FF", + "time": "#aa22ff", + "double": "#bb6688", + "nil": "#808080", + "bit": "#8541FF", + "field": "cyan", + "group": "ansiblue", + "username": "blue", +} + +DOC = { + "doccommand": "bold", + "dockey": "#E6DB74", + "code": "#aaaaaa", + "h2": "bold #33aa33", +} + +GROUP = { + "group.cluster": "#E6DB74", + "group.connection": "#E6DB74", + "group.generic": "#E6DB74", + "group.geo": "#E6DB74", + "group.hash": "#E6DB74", + "group.hyperloglog": "#E6DB74", + "group.list": "#E6DB74", + "group.pubsub": "#E6DB74", + "group.server": "#E6DB74", + "group.set": "#E6DB74", + "group.sortedset": "#E6DB74", + "group.stram": "#E6DB74", + "group.string": "#E6DB74", + "group.transections": "#E6DB74", +} + + +STYLE_DICT = { + # User input (default text). + "": "", + # Prompt. + "rprompt": "bg:#ff0066 #ffffff", + "hostname": "", + "index": "#ff0000", + "trailing-input": "bg:#ff0000 #000000", + "password": "hidden", + "success": "#00ff5f bold", + "queued": "#32CD32 bold", + "error": "#ff005f bold", + "type": "#888", + "channel": "#888", # FIXME + # colors below copied from mycli project, ~~love~~ + # bottom-toolbar + "bottom-toolbar": "bg:#222222 #aaaaaa", + "bottom-toolbar.on": "bg:#222222 #ffffff", + "bottom-toolbar.off": "bg:#222222 #888888", + "bottom-toolbar.loaded": "bg:#222222 #44aa44", + "bottom-toolbar.since": "bg:#222222 #bc7a00", + "bottom-toolbar.complexity": "bg:#222222 #666666", + "bottom-toolbar.group": "bg:#222222 #d2413a bold", + # completion + "completion-menu.completion.current": "bg:#ffffff #000000", + "completion-menu.completion": "bg:#008888 #ffffff", + "completion-menu.meta.completion.current": "bg:#44aaaa #000000", + "completion-menu.meta.completion": "bg:#448888 #ffffff", + "completion-menu.multi-column-meta": "bg:#aaffff #000000", + "scrollbar.arrow": "bg:#003333", + "scrollbar": "bg:#00aaaa", + "selected": "#ffffff bg:#6666aa", + "search": "#ffffff bg:#4444aa", + "search.current": "#ffffff bg:#44aa44", + "search-toolbar": "noinherit bold", + "search-toolbar.text": "nobold", + "system-toolbar": "noinherit bold", + "arg-toolbar": "noinherit bold", + "arg-toolbar.text": "nobold", +} + +BOTTOM_TOOLBAR_TOKEN = { + # redis token + f"bottom-toolbar.{token}": f"bg:#222222 {token_style}" + for token, token_style in REDIS_TOKEN.items() +} + +style.update(STYLE_DICT) +style.update(BOTTOM_TOOLBAR_TOKEN) +style.update(DOC) + + +STYLE = merge_styles([override_style, Style.from_dict(style)]) diff --git a/iredis/utils.py b/iredis/utils.py new file mode 100644 index 0000000..b11097d --- /dev/null +++ b/iredis/utils.py @@ -0,0 +1,323 @@ +import re +import sys +import time +import logging +from collections import namedtuple +from urllib.parse import parse_qs, unquote, urlparse + +from prompt_toolkit.formatted_text import FormattedText + +from iredis.exceptions import InvalidArguments + + +logger = logging.getLogger(__name__) + +_last_timer = time.time() +_timer_counter = 0 +sperator = re.compile(r"\s") +logger.debug(f"[timer] start on {_last_timer}") + + +def timer(title): + global _last_timer + global _timer_counter + + now = time.time() + tick = now - _last_timer + logger.debug(f"[timer{_timer_counter:2}] {tick:.8f} -> {title}") + + _last_timer = now + _timer_counter += 1 + + +def nativestr(x): + return x if isinstance(x, str) else x.decode("utf-8", "replace") + + +def literal_bytes(b): + if isinstance(b, bytes): + return str(b)[2:-1] + return b + + +def _valide_token(words): + token = "".join(words).strip() + if token: + yield token + + +def strip_quote_args(s): + """ + Given string s, split it into args.(Like bash paring) + Handle with all quote cases. + + Raise ``InvalidArguments`` if quotes not match + + :return: args list. + """ + word = [] + in_quote = None + pre_back_slash = False + for char in s: + if in_quote: + # close quote + if char == in_quote: + if not pre_back_slash: + yield "".join(word) + word = [] + in_quote = None + else: + # previous char is \ , merge with current " + word[-1] = char + else: + word.append(char) + # not in quote + else: + # sperator + if sperator.match(char): + if word: + yield "".join(word) + word = [] + # open quotes + elif char in ["'", '"']: + in_quote = char + else: + word.append(char) + if char == "\\" and not pre_back_slash: + pre_back_slash = True + else: + pre_back_slash = False + + if word: + yield "".join(word) + # quote not close + if in_quote: + raise InvalidArguments("Invalid argument(s)") + + +type_convert = {"posix time": "time"} + + +def parse_argument_to_formatted_text( + name, _type, is_option, style_class="bottom-toolbar" +): + result = [] + if isinstance(name, str): + _type = type_convert.get(_type, _type) + result.append((f"class:{style_class}.{_type}", " " + name)) + elif isinstance(name, list): + for inner_name, inner_type in zip(name, _type): + inner_type = type_convert.get(inner_type, inner_type) + if is_option: + result.append((f"class:{style_class}.{inner_type}", f" [{inner_name}]")) + else: + result.append((f"class:{style_class}.{inner_type}", f" {inner_name}")) + else: + raise Exception() + return result + + +def compose_command_syntax(command_info, style_class="bottom-toolbar"): + command_style = f"class:{style_class}.command" + const_style = f"class:{style_class}.const" + args = [] + if command_info.get("arguments"): + for argument in command_info["arguments"]: + if argument.get("command"): + # command [ + args.append((command_style, " [" + argument["command"])) + if argument.get("enum"): + enums = "|".join(argument["enum"]) + args.append((const_style, f" [{enums}]")) + elif argument.get("name"): + args.extend( + parse_argument_to_formatted_text( + argument["name"], + argument["type"], + argument.get("optional"), + style_class=style_class, + ) + ) + # ] + args.append((command_style, "]")) + elif argument.get("enum"): + enums = "|".join(argument["enum"]) + args.append((const_style, f" [{enums}]")) + + else: + args.extend( + parse_argument_to_formatted_text( + argument["name"], + argument["type"], + argument.get("optional"), + style_class=style_class, + ) + ) + return args + + +def command_syntax(command, command_info): + """ + Get command syntax based on redis-doc/commands.json + + :param command: Command name in uppercase + :param command_info: dict loaded from commands.json, only for + this command. + """ + comamnd_group = command_info["group"] + bottoms = [ + ("class:bottom-toolbar.group", f"({comamnd_group}) "), + ("class:bottom-toolbar.command", f"{command}"), + ] # final display FormattedText + + bottoms += compose_command_syntax(command_info) + + if "since" in command_info: + since = command_info["since"] + bottoms.append(("class:bottom-toolbar.since", f" since: {since}")) + if "complexity" in command_info: + complexity = command_info["complexity"] + bottoms.append(("class:bottom-toolbar.complexity", f" complexity:{complexity}")) + + return FormattedText(bottoms) + + +def _literal_bytes(b): + """ + convert bytes to printable text. + + backslash and double-quotes will be escaped by + backslash. + "hello\" -> \"hello\\\" + + we don't add outter double quotes here, since + completer also need this function's return value + to patch completers. + + b'hello' -> "hello" + b'double"quotes"' -> "double\"quotes\"" + """ + s = str(b) + s = s[2:-1] # remove b' ' + # unescape single quote + s = s.replace(r"\'", "'") + return s + + +def ensure_str(origin, decode=None): + """ + Ensure is string, for display and completion. + + Then add double quotes + + Note: this method do not handle nil, make sure check (nil) + out of this method. + """ + if origin is None: + return None + if isinstance(origin, str): + return origin + if isinstance(origin, int): + return str(origin) + elif isinstance(origin, list): + return [ensure_str(b) for b in origin] + elif isinstance(origin, bytes): + if decode: + return origin.decode(decode) + return _literal_bytes(origin) + else: + raise Exception(f"Unkown type: {type(origin)}, origin: {origin}") + + +def double_quotes(unquoted): + """ + Display String like redis-cli. + escape inner double quotes. + add outter double quotes. + + :param unquoted: list, or str + """ + if isinstance(unquoted, str): + # escape double quote + escaped = unquoted.replace('"', '\\"') + return f'"{escaped}"' # add outter double quotes + elif isinstance(unquoted, list): + return [double_quotes(item) for item in unquoted] + + +def exit(): + """ + Exit IRedis REPL + """ + print("Goodbye!") + sys.exit() + + +def convert_formatted_text_to_bytes(formatted_text): + to_render = [text for style, text in formatted_text] + return "".join(to_render).encode() + + +DSN = namedtuple("DSN", "scheme host port path db username password") + + +def parse_url(url, db=0): + """ + Return a Redis client object configured from the given URL + + For example:: + + redis://[[username]:[password]]@localhost:6379/0 + rediss://[[username]:[password]]@localhost:6379/0 + unix://[[username]:[password]]@/path/to/socket.sock?db=0 + + Three URL schemes are supported: + + - ```redis://`` + <http://www.iana.org/assignments/uri-schemes/prov/redis>`_ creates a + normal TCP socket connection + - ```rediss://`` + <http://www.iana.org/assignments/uri-schemes/prov/rediss>`_ creates a + SSL wrapped TCP socket connection + - ``unix://`` creates a Unix Domain Socket connection + + There are several ways to specify a database number. The parse function + will return the first specified option: + 1. A ``db`` querystring option, e.g. redis://localhost?db=0 + 2. If using the redis:// scheme, the path argument of the url, e.g. + redis://localhost/0 + 3. The ``db`` argument to this function. + + If none of these options are specified, db=0 is used. + """ + url = urlparse(url) + + scheme = url.scheme + path = unquote(url.path) if url.path else None + # We only support redis://, rediss:// and unix:// schemes. + # if scheme is ``unix``, read ``db`` from query string + # otherwise read ``db`` from path + if url.scheme == "unix": + qs = parse_qs(url.query) + if "db" in qs: + db = int(qs["db"][0] or db) + elif url.scheme in ("redis", "rediss"): + scheme = url.scheme + if path: + try: + db = int(path.replace("/", "")) + path = None + except (AttributeError, ValueError): + pass + else: + valid_schemes = ", ".join(("redis://", "rediss://", "unix://")) + raise ValueError( + "Redis URL must specify one of the following" "schemes (%s)" % valid_schemes + ) + + username = unquote(url.username) if url.username else None + password = unquote(url.password) if url.password else None + hostname = unquote(url.hostname) if url.hostname else None + port = url.port + + return DSN(scheme, hostname, port, path, db, username, password) diff --git a/iredis/warning.py b/iredis/warning.py new file mode 100644 index 0000000..bb8231e --- /dev/null +++ b/iredis/warning.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +import sys +import click +from .commands import dangerous_commands + + +class ConfirmBoolParamType(click.ParamType): + name = "confirmation" + + def convert(self, value, param, ctx): + if isinstance(value, bool): + return bool(value) + value = value.lower() + if value in ("yes", "y"): + return True + elif value in ("no", "n"): + return False + self.fail("%s is not a valid boolean" % value, param, ctx) + + def __repr__(self): + return "BOOL" + + +BOOLEAN_TYPE = ConfirmBoolParamType() + + +def is_dangerous(command): + """ + :return : return True, reason str if command is dangerous; + return False, None otherwise. + """ + reason = dangerous_commands.get(command) + return reason is not None, reason + + +def prompt(*args, **kwargs): + """Prompt the user for input and handle any abort exceptions.""" + try: + return click.prompt(*args, **kwargs) + except click.Abort: + return False + + +def confirm_dangerous_command(upper_command): + """Check if the query is destructive and prompts the user to confirm. + + Returns: + * None if the query is non-destructive or we can't prompt the user. + * True if the query is destructive and the user wants to proceed. + * False if the query is destructive and the user doesn't want to proceed. + + """ + dangerous, reason = is_dangerous(upper_command) + prompt_text = f"{reason}.\n" "Do you want to proceed? (y/n)" + if dangerous and sys.stdin.isatty(): + return prompt(prompt_text, type=BOOLEAN_TYPE) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..62763af --- /dev/null +++ b/poetry.lock @@ -0,0 +1,391 @@ +[[package]] +category = "dev" +description = "Atomic file writes." +marker = "sys_platform == \"win32\"" +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "main" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" + +[[package]] +category = "dev" +description = "Cross-platform colored terminal text." +marker = "sys_platform == \"win32\"" +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" + +[[package]] +category = "main" +description = "Config file reading, writing and validation." +name = "configobj" +optional = false +python-versions = "*" +version = "5.0.6" + +[package.dependencies] +six = "*" + +[[package]] +category = "dev" +description = "Read metadata from Python packages" +marker = "python_version < \"3.8\"" +name = "importlib-metadata" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.6.1" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] + +[[package]] +category = "main" +description = "Read resources from Python packages" +name = "importlib-resources" +optional = false +python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" +version = "1.0.2" + +[[package]] +category = "main" +description = "The fastest markdown parser in pure Python" +name = "mistune" +optional = false +python-versions = "*" +version = "0.8.4" + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.5" +version = "8.4.0" + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.4" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "main" +description = "Python datetimes made easy" +name = "pendulum" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.1.0" + +[package.dependencies] +python-dateutil = ">=2.6,<3.0" +pytzdata = ">=2018.3" + +[[package]] +category = "dev" +description = "Pexpect allows easy control of interactive console applications." +name = "pexpect" +optional = false +python-versions = "*" +version = "4.8.0" + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.1" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "main" +description = "Library for building powerful interactive command lines in Python" +name = "prompt-toolkit" +optional = false +python-versions = ">=3.6" +version = "3.0.3" + +[package.dependencies] +wcwidth = "*" + +[[package]] +category = "dev" +description = "Run a subprocess in a pseudo terminal" +name = "ptyprocess" +optional = false +python-versions = "*" +version = "0.6.0" + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.2" + +[[package]] +category = "main" +description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +optional = false +python-versions = ">=3.5" +version = "2.6.1" + +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.7" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=3.5" +version = "5.4.3" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +checkqa-mypy = ["mypy (v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +category = "main" +description = "Extensions to the standard Python datetime module" +name = "python-dateutil" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +version = "2.8.1" + +[package.dependencies] +six = ">=1.5" + +[[package]] +category = "main" +description = "The Olson timezone database for Python." +name = "pytzdata" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2019.3" + +[[package]] +category = "main" +description = "Python client for Redis key-value store" +name = "redis" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "3.5.3" + +[package.extras] +hiredis = ["hiredis (>=0.1.3)"] + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" + +[[package]] +category = "main" +description = "Measures number of Terminal column cells of wide-character codes" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.1.9" + +[[package]] +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" +name = "zipp" +optional = false +python-versions = ">=3.6" +version = "3.1.0" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["jaraco.itertools", "func-timeout"] + +[metadata] +content-hash = "50616ff2ae7105090d8555ffaae4bceb8cfc92d7fddb70635202a5eb8b4cf701" +python-versions = "^3.6" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +colorama = [ + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +configobj = [ + {file = "configobj-5.0.6.tar.gz", hash = "sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902"}, +] +importlib-metadata = [ + {file = "importlib_metadata-1.6.1-py2.py3-none-any.whl", hash = "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"}, + {file = "importlib_metadata-1.6.1.tar.gz", hash = "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545"}, +] +importlib-resources = [ + {file = "importlib_resources-1.0.2-py2.py3-none-any.whl", hash = "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b"}, + {file = "importlib_resources-1.0.2.tar.gz", hash = "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078"}, +] +mistune = [ + {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, + {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, +] +more-itertools = [ + {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, + {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, +] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, +] +pendulum = [ + {file = "pendulum-2.1.0-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:9eda38ff65b1f297d860d3f562480e048673fb4b81fdd5c8c55decb519b97ed2"}, + {file = "pendulum-2.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:70007aebc4494163f8705909a1996ce21ab853801b57fba4c2dd53c3df5c38f0"}, + {file = "pendulum-2.1.0-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:575934b65b298eeb99c5a5b1673c945fc5c99e2b56caff772a91bc4b1eba7b82"}, + {file = "pendulum-2.1.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:d42d1e870541eeaf3fe0500aac0c76a85bd4bd53ebed74f9a7daf8f01ac77374"}, + {file = "pendulum-2.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ff7f3420de0c0cf21c1fc813d581fcfa4a1fb6d87f09485880b3e1204eb9cdd7"}, + {file = "pendulum-2.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ac3c6a992beeb4c9bd90c317a1bb2a6cba159b49a49b6dd3c86b5bacb86f3d50"}, + {file = "pendulum-2.1.0-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:75a62e3f98499283fafe8ef4b44f81052e84825b00a0b64609dd8a06985382b9"}, + {file = "pendulum-2.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a79a72a7fd1092a7c69ddd8580a0be5365ded40c9f9c865623c7665742e3b888"}, + {file = "pendulum-2.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:701127e1f0ff7c253cc0c07f29becc5f9210547914e0bbe59ffd9fa064d7c3c8"}, + {file = "pendulum-2.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:816e01dcb0ba4ffcf2ceaafe4d644174fea680361e909f6f8ba0a4fdb2ccae24"}, + {file = "pendulum-2.1.0-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:edd00e6b43698762e10bfda508cc9c06bad88c0703a9b37e412aec1189e06e23"}, + {file = "pendulum-2.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4420e058110740a8193fb0709350dfc6ac790a99c345fc4e92e24df0f834ddcb"}, + {file = "pendulum-2.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:aa560bd39d94f3889646422f1e65b8dfd025bf6288d43e5c2e31d4f972aaf2e4"}, + {file = "pendulum-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2788945a0111d5325fd27ae3e3b18b741e440d20bdb7d4ea22fce7c9a4fbbf40"}, + {file = "pendulum-2.1.0-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:eb7e349bb2d1b2b418d094e2179d6768561e8242fd8cb640b5aaba735f3e91d1"}, + {file = "pendulum-2.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6cf0f876cd088ee1578266f4231121376747aa90c3ed3b8e212a8344a9920061"}, + {file = "pendulum-2.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:aa13ddea12fd871d3191f633f08090b91ea2e80fb0ed50a7a149add7f680b12d"}, + {file = "pendulum-2.1.0-cp38-cp38m-win_amd64.whl", hash = "sha256:0cbbd4f30c69a283690d9ed8e58e44a990e067e59ee05b5ef55d022b38659aeb"}, + {file = "pendulum-2.1.0.tar.gz", hash = "sha256:093cab342e10516660e64b935a6da1a043e0286de36cc229fb48471415981ffe"}, +] +pexpect = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.3-py3-none-any.whl", hash = "sha256:c93e53af97f630f12f5f62a3274e79527936ed466f038953dfa379d4941f651a"}, + {file = "prompt_toolkit-3.0.3.tar.gz", hash = "sha256:a402e9bf468b63314e37460b68ba68243d55b2f8c4d0192f85a019af3945050e"}, +] +ptyprocess = [ + {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, + {file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"}, +] +py = [ + {file = "py-1.8.2-py2.py3-none-any.whl", hash = "sha256:a673fa23d7000440cc885c17dbd34fafcb7d7a6e230b29f6766400de36a33c44"}, + {file = "py-1.8.2.tar.gz", hash = "sha256:f3b3a4c36512a4c4f024041ab51866f11761cc169670204b235f6b20523d4e6b"}, +] +pygments = [ + {file = "Pygments-2.6.1-py2.py3-none-any.whl", hash = "sha256:aa931c0bd5daa25c475afadb2147115134cfe501f0656828cbe7cb566c7123bc"}, + {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, + {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, + {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, +] +pytzdata = [ + {file = "pytzdata-2019.3-py2.py3-none-any.whl", hash = "sha256:84c52b9a47d097fcd483f047a544979de6c3a86e94c845e3569e9f8acd0fa071"}, + {file = "pytzdata-2019.3.tar.gz", hash = "sha256:fac06f7cdfa903188dc4848c655e4adaee67ee0f2fe08e7daf815cf2a761ee5e"}, +] +redis = [ + {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, + {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +wcwidth = [ + {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"}, + {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"}, +] +zipp = [ + {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, + {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, +] diff --git a/pyoxidizer.template.bzl b/pyoxidizer.template.bzl new file mode 100644 index 0000000..612ecde --- /dev/null +++ b/pyoxidizer.template.bzl @@ -0,0 +1,102 @@ +# This file defines how PyOxidizer application building and packaging is +# performed. See the pyoxidizer crate's documentation for extensive +# documentation on this file format. + +# Obtain the default PythonDistribution for our build target. We link +# this distribution into our produced executable and extract the Python +# standard library from it. +def make_dist(): + return default_python_distribution() + +# Configuration files consist of functions which define build "targets." +# This function creates a Python executable and installs it in a destination +# directory. +def make_exe(dist): + # This variable defines the configuration of the + # embedded Python interpreter. + python_config = PythonInterpreterConfig( + run_eval="from iredis.entry import main; main()", + # Allows the executable to load deps from this folder + sys_paths=["$ORIGIN/lib"] + ) + + # Produce a PythonExecutable from a Python distribution, embedded + # resources, and other options. The returned object represents the + # standalone executable that will be built. + exe = dist.to_python_executable( + name="iredis", + config=python_config, + # Embed all extension modules, making this a fully-featured Python. + extension_module_filter='all', + + # Only package the minimal set of extension modules needed to initialize + # a Python interpreter. Many common packages in Python's standard + # library won't work with this setting. + #extension_module_filter='minimal', + + # Only package extension modules that don't require linking against + # non-Python libraries. e.g. will exclude support for OpenSSL, SQLite3, + # other features that require external libraries. + #extension_module_filter='no-libraries', + + # Only package extension modules that don't link against GPL licensed + # libraries. + #extension_module_filter='no-gpl', + + # Include Python module sources. This isn't strictly required and it does + # make binary sizes larger. But having the sources can be useful for + # activities such as debugging. + include_sources=True, + + # Whether to include non-module resource data/files. + include_resources=False, + + # Do not include functionality for testing Python itself. + include_test=False, + ) + + # Discover Python files from a virtualenv and add them to our embedded + # context. + #exe.add_python_resources(dist.read_virtualenv(path="/path/to/venv")) + + # Filter all resources collected so far through a filter of names + # in a file. + #exe.filter_from_files(files=["/path/to/filter-file"])) + + # Return our `PythonExecutable` instance so it can be built and + # referenced by other consumers of this target. + return exe + +def make_embedded_data(exe): + return exe.to_embedded_data() + +def make_install(dist, exe): + # Create an object that represents our installed application file layout. + files = FileManifest() + + # Add the generated executable to our install layout in the root directory. + files.add_python_resource(".", exe) + + # Include pip dependencies alongside the executable + # WHEEL_PATH will be replaced with envsubst because pyoxidizer doesn't support env vars + files.add_python_resources("lib", dist.pip_install(["$WHEEL_PATH"])) + + return files + +# Tell PyOxidizer about the build targets defined above. +register_target("dist", make_dist) +register_target("exe", make_exe, depends=["dist"], default=True) +register_target("embedded", make_embedded_data, depends=["exe"], default_build_script=True) +register_target("install", make_install, depends=["dist", "exe"]) + +# Resolve whatever targets the invoker of this configuration file is requesting +# be resolved. +resolve_targets() + +# END OF COMMON USER-ADJUSTED SETTINGS. +# +# Everything below this is typically managed by PyOxidizer and doesn't need +# to be updated by people. + +PYOXIDIZER_VERSION = "0.6.0" +PYOXIDIZER_COMMIT = "" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..76a0b7b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[tool.poetry] +name = "iredis" +version = "1.9.1" +description = "Terminal client for Redis with auto-completion and syntax highlighting." +authors = ["laixintao <laixintao1995@163.com>"] +readme = 'README.md' +license = "BSD-3-Clause" +repository = 'https://github.com/laixintao/iredis' +homepage = "https://github.com/laixintao/iredis" +keywords=["Redis", "key-value store", "Commandline tools", "Redis Client"] +classifiers = [ +# see https://pypi.org/pypi?%3Aaction=list_classifiers + "Development Status :: 4 - Beta", + "Environment :: Console", + "Environment :: Console :: Curses", + "Environment :: MacOS X", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Topic :: Database", + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers", +] + +packages = [ + { include = "iredis" }, + { include = "tests", format = "sdist" }, +] + +[tool.poetry.dependencies] +python = "^3.6" +redis = "^3" +prompt_toolkit = "^3" +Pygments = "^2" +mistune = "^0.8" +configobj = "^5.0" +click = "^7.0" +pendulum = "^2.0" +importlib-resources = "1.0.2" +# wcwidth 0.2.x uses pkg_resources which is not supported by PyOxidizer +wcwidth = "0.1.9" + +[tool.poetry.dev-dependencies] +pytest = "^5" +pexpect = "^4.7" + +[tool.poetry.scripts] +iredis = 'iredis.entry:main' + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/scripts/add_fooset_commands.txt b/scripts/add_fooset_commands.txt new file mode 100644 index 0000000..92b0312 --- /dev/null +++ b/scripts/add_fooset_commands.txt @@ -0,0 +1 @@ +SADD fooset alligator ant bear bee bird camel cat cheetah chicken chimpanzee cow crocodile deer dog dolphin duck eagle elephant fish fly fox frog giraffe goat goldfish hamster hippopotamus horse kangaroo kitten lion lobster monkey octopus owl panda pig puppy rabbit rat scorpion seal shark sheep snail snake spider squirrel tiger turtle wolf zebra diff --git a/scripts/add_hash.txt b/scripts/add_hash.txt new file mode 100644 index 0000000..fea3c7a --- /dev/null +++ b/scripts/add_hash.txt @@ -0,0 +1,3 @@ +HMSET hash1 Tolerant intolerant Decent indecent Discreet indiscreet Excusable inexcusable +HMSET hash2 Behave misbehave Interpret misinterpret Lead mislead Trust mistrust Likely unlikely Able unable Fortunate unfortunate Forgiving unforgiving +HMSET hash3 Entity nonentity Conformist nonconformist Payment nonpayment Sense nonsense diff --git a/scripts/add_lists.txt b/scripts/add_lists.txt new file mode 100644 index 0000000..f8b7542 --- /dev/null +++ b/scripts/add_lists.txt @@ -0,0 +1,3 @@ +LPUSH list:animals alligator ant bear bee bird camel cat cheetah chicken chimpanzee cow crocodile deer dog dolphin duck eagle elephant fish fly fox frog giraffe goat goldfish hamster hippopotamus horse kangaroo kitten lion lobster monkey octopus owl panda pig puppy rabbit rat scorpion seal shark sheep snail snake spider squirrel tiger turtle wolf zebra +LPUSH list:buildings "airport" "apartment building" "bank" "barber shop" "book store" "bowling alley" "bus stop" "church" "convenience store" "department store" "fire department" "gas station" "hospital" "house" "library" "movie theater" "museum" "office building" "post office" "restaurant" "school" "mall" "supermarket" "train station" +LPUSH list:restaurant bill breakfast check cup dessert dinner dressing drink fork hamburger knife lunch menu napkin order salt spoon water coffee tea diff --git a/scripts/add_myzset_commands.txt b/scripts/add_myzset_commands.txt new file mode 100644 index 0000000..4532d60 --- /dev/null +++ b/scripts/add_myzset_commands.txt @@ -0,0 +1,52 @@ +ZADD myzset 28693 alligator +ZADD myzset 29596 ant +ZADD myzset 11320 bear +ZADD myzset 28872 bee +ZADD myzset 26656 bird +ZADD myzset 708 camel +ZADD myzset 31829 cat +ZADD myzset 7424 cheetah +ZADD myzset 30668 chicken +ZADD myzset 154 chimpanzee +ZADD myzset 24709 cow +ZADD myzset 25916 crocodile +ZADD myzset 6888 deer +ZADD myzset 32034 dog +ZADD myzset 15528 dolphin +ZADD myzset 667 duck +ZADD myzset 202 eagle +ZADD myzset 19551 elephant +ZADD myzset 32231 fish +ZADD myzset 4002 fly +ZADD myzset 18679 fox +ZADD myzset 10147 frog +ZADD myzset 28405 giraffe +ZADD myzset 15557 goat +ZADD myzset 2062 goldfish +ZADD myzset 25018 hamster +ZADD myzset 19888 hippopotamus +ZADD myzset 24984 horse +ZADD myzset 16088 kangaroo +ZADD myzset 7907 kitten +ZADD myzset 9814 lion +ZADD myzset 32194 lobster +ZADD myzset 8036 monkey +ZADD myzset 19483 octopus +ZADD myzset 9398 owl +ZADD myzset 24987 panda +ZADD myzset 28153 pig +ZADD myzset 28829 puppy +ZADD myzset 9709 rabbit +ZADD myzset 14184 rat +ZADD myzset 11208 scorpion +ZADD myzset 11385 seal +ZADD myzset 1900 shark +ZADD myzset 6890 sheep +ZADD myzset 6897 snail +ZADD myzset 7659 snake +ZADD myzset 22338 spider +ZADD myzset 3676 squirrel +ZADD myzset 1490 tiger +ZADD myzset 31275 turtle +ZADD myzset 8113 wolf +ZADD myzset 1601 zebra diff --git a/scripts/check_renders.sh b/scripts/check_renders.sh new file mode 100755 index 0000000..f84602e --- /dev/null +++ b/scripts/check_renders.sh @@ -0,0 +1 @@ +rg -vw render_int iredis/data/command_syntax.csv | rg -vw render_simple_string | rg -wv render_bulk_string | rg -vw render_list | rg -vw render_members | rg -vw render_bulk_string_decode | rg -vw render_string_or_int | rg -wv render_list_or_string diff --git a/scripts/conf/slave/slave.conf b/scripts/conf/slave/slave.conf new file mode 100644 index 0000000..f8749d9 --- /dev/null +++ b/scripts/conf/slave/slave.conf @@ -0,0 +1,2 @@ +slaveof localhost 6379 +port 6479 diff --git a/scripts/display_all_command_hints.py b/scripts/display_all_command_hints.py new file mode 100644 index 0000000..9f13a4c --- /dev/null +++ b/scripts/display_all_command_hints.py @@ -0,0 +1,7 @@ +from iredis.utils import command_syntax +from iredis.style import STYLE +from iredis.commands import commands_summary +from prompt_toolkit import print_formatted_text + +for command, info in commands_summary.items(): + print_formatted_text(command_syntax(command, info), style=STYLE) diff --git a/scripts/download_redis_commands.py b/scripts/download_redis_commands.py new file mode 100755 index 0000000..4dda6b8 --- /dev/null +++ b/scripts/download_redis_commands.py @@ -0,0 +1,50 @@ +#!python3 + +""" +Download all Reids commands from https://redis.io/commands. +Output to csv format. +""" +import sys +import csv + +from lxml import etree +import requests + +stdout_writer = csv.writer(sys.stdout) + + +def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + + +eprint("Download https://redis.io/commands page...") +page = requests.get("https://redis.io/commands").text +eprint("Download finished!") + +eprint("Start prase page...") +html = etree.HTML(page) +commands = html.xpath("//div[@class='container']/ul/li") +stdout_writer.writerow(["Group", "Command", "Args", "Summary", "Redis.io link"]) +command_rows = [] +# parse page +for command in commands: + group = command.attrib["data-group"] + command_name = command.xpath("./a/span[@class='command']/text()")[0].strip() + command_args = command.xpath( + "./a/span[@class='command']/span[@class='args']/text()" + )[0].strip() + command_summary = command.xpath("./a/span[@class='summary']/text()")[0].strip() + command_link = "https://redis.io" + command.xpath("./a/@href")[0].strip() + command_rows.append( + [ + group, + command_name, + " ".join(command_args.split()), + command_summary, + command_link, + ] + ) +# write to stdout +for row in sorted(command_rows): + stdout_writer.writerow(row) +eprint("Down.") diff --git a/scripts/multi_del.txt b/scripts/multi_del.txt new file mode 100644 index 0000000..685cb03 --- /dev/null +++ b/scripts/multi_del.txt @@ -0,0 +1,4 @@ +DEL key-001 +DEL key-002 +DEL key-003 +DEL key-004 diff --git a/scripts/redis.conf b/scripts/redis.conf new file mode 100644 index 0000000..368a34c --- /dev/null +++ b/scripts/redis.conf @@ -0,0 +1,4 @@ +port 7379 +unixsocket /tmp/redis.sock +unixsocketperm 775 +aclfile /tmp/redis.acl diff --git a/scripts/set_999_keys_in_redis.py b/scripts/set_999_keys_in_redis.py new file mode 100644 index 0000000..9a65f04 --- /dev/null +++ b/scripts/set_999_keys_in_redis.py @@ -0,0 +1,6 @@ +import redis + +client = redis.StrictRedis(db=3) + +for i in range(100000): + client.set(f"key-{i}", "hello world") diff --git a/scripts/tcp-proxy.sh b/scripts/tcp-proxy.sh new file mode 100755 index 0000000..ff0ce53 --- /dev/null +++ b/scripts/tcp-proxy.sh @@ -0,0 +1,20 @@ +#!/bin/sh -e +# Display TCP packets +# code from: +# https://notes.tweakblogs.net/blog/7955/using-netcat-to-build-a-simple-tcp-proxy-in-linux.html + +if [ $# != 3 ] +then + echo "usage: $0 <src-port> <dst-host> <dst-port>" + exit 0 +fi + +TMP=`mktemp -d` +BACK=$TMP/pipe.back +SENT=$TMP/pipe.sent +RCVD=$TMP/pipe.rcvd +trap 'rm -rf "$TMP"' EXIT +mkfifo -m 0600 "$BACK" "$SENT" "$RCVD" +sed 's/^/ => /' <"$SENT" & +sed 's/^/<= /' <"$RCVD" & +nc -l 127.0.0.1 "$1" <"$BACK" | tee "$SENT" | nc "$2" "$3" | tee "$RCVD" >"$BACK" diff --git a/scripts/update_command_from_redis_doc.sh b/scripts/update_command_from_redis_doc.sh new file mode 100755 index 0000000..7cc161f --- /dev/null +++ b/scripts/update_command_from_redis_doc.sh @@ -0,0 +1 @@ +wget https://raw.githubusercontent.com/antirez/redis-doc/master/commands.json -O commands.json diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/__init__.py diff --git a/tests/cli_tests/test_cli_pubsub.py b/tests/cli_tests/test_cli_pubsub.py new file mode 100644 index 0000000..956e105 --- /dev/null +++ b/tests/cli_tests/test_cli_pubsub.py @@ -0,0 +1,31 @@ +def test_subscribe(cli, clean_redis): + cli.sendline("subscribe foo") + cli.expect("subscribe from") + cli.expect("foo") + cli.expect("1") + + clean_redis.publish("foo", "test message") + cli.expect("from") + cli.expect("foo") + cli.expect("test message") + + # unsubscribe, send ctrl-c + cli.send(chr(3)) + cli.expect("unsubscribe from") + cli.expect("0") + + +def test_subscribe_in_raw_mode(raw_cli, clean_redis): + raw_cli.sendline("subscribe foo") + raw_cli.expect("subscribe\r") + raw_cli.expect("foo\r") + raw_cli.expect("1\r") + + clean_redis.publish("foo", "test message") + raw_cli.expect("message\r") + raw_cli.expect("foo\r") + raw_cli.expect("test message") + + # unsubscribe, send ctrl-c + raw_cli.send(chr(3)) + raw_cli.expect("0\r") diff --git a/tests/cli_tests/test_cli_start.py b/tests/cli_tests/test_cli_start.py new file mode 100644 index 0000000..d33185a --- /dev/null +++ b/tests/cli_tests/test_cli_start.py @@ -0,0 +1,75 @@ +import pexpect +import pytest +from textwrap import dedent + + +def test_start_on_connection_error(): + cli = pexpect.spawn("iredis -p 12345", timeout=1) + cli.logfile_read = open("cli_test.log", "ab") + cli.expect(r"Error \d+ connecting to 127.0.0.1:12345. Connection refused.") + cli.close() + + +def test_short_help_option(config): + c = pexpect.spawn("iredis -h", timeout=2) + + c.expect("Show this message and exit.") + + c = pexpect.spawn("iredis -h 127.0.0.1") + c.expect("127.0.0.1:6379>") + + c.close() + + +@pytest.mark.skipif("int(os.environ['REDIS_VERSION']) != 5") +def test_server_version_in_starting_on5(): + c = pexpect.spawn("iredis", timeout=2) + c.expect("redis-server 5") + c.close() + + +@pytest.mark.skipif("int(os.environ['REDIS_VERSION']) != 6") +def test_server_version_in_starting_on6(): + c = pexpect.spawn("iredis", timeout=2) + c.expect("redis-server 6") + c.close() + + +def test_connection_using_url(clean_redis): + c = pexpect.spawn("iredis --url redis://localhost:6379/7", timeout=2) + c.logfile_read = open("cli_test.log", "ab") + c.expect(["iredis", "127.0.0.1:6379[7]>"]) + c.sendline("set current-db 7") + c.expect("OK") + c.close() + + +def test_connection_using_url_from_env(clean_redis, monkeypatch): + monkeypatch.setenv("IREDIS_URL", "redis://localhost:6379/7") + c = pexpect.spawn("iredis", timeout=2) + c.logfile_read = open("cli_test.log", "ab") + c.expect(["iredis", "localhost:6379[7]>"]) + c.sendline("set current-db 7") + c.expect("OK") + c.close() + + +@pytest.mark.xfail(reason="current test in github action, socket not supported.") +# https://github.community/t5/GitHub-Actions/Job-service-command/td-p/33901# +# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idservices +def test_connect_via_socket(fake_redis_socket): + config_content = dedent( + """ + [main] + log_location = /tmp/iredis1.log + no_info=True + """ + ) + with open("/tmp/iredisrc", "w+") as etc_config: + etc_config.write(config_content) + + c = pexpect.spawn("iredis --iredisrc /tmp/iredisrc -s /tmp/test.sock", timeout=2) + c.logfile_read = open("cli_test.log", "ab") + c.expect("redis /tmp/test.sock") + + c.close() diff --git a/tests/cli_tests/test_command_input.py b/tests/cli_tests/test_command_input.py new file mode 100644 index 0000000..4f970f5 --- /dev/null +++ b/tests/cli_tests/test_command_input.py @@ -0,0 +1,55 @@ +import pytest + + +def test_wrong_select_db_index(cli): + cli.sendline("select 1") + cli.expect(["OK", "127.0.0.1"]) + + cli.sendline("select 128") + cli.expect(["DB index is out of range", "127.0.0.1:6379[1]>"]) + + cli.sendline("select abc") + cli.expect(["invalid DB index", "127.0.0.1:6379[1]>"]) + + cli.sendline("select 15") + cli.expect("OK") + + +def test_set_command_with_shash(clean_redis, cli): + cli.sendline("set a \\hello\\") # legal redis command + cli.expect("OK") + + cli.sendline("get a") + cli.expect(r"hello") + + +def test_enter_key_binding(clean_redis, cli): + cli.send("set") + cli.expect("set") + cli.send("\033[B") # down + cli.sendline() # enter + + cli.sendline(" a 'hello'") + cli.expect("OK") + + cli.sendline("get a") + cli.expect(r"hello") + + +@pytest.mark.skipif("int(os.environ['REDIS_VERSION']) < 6") +def test_auth_hidden_password_with_username(clean_redis, cli): + cli.send("auth default hello-world") + cli.expect("default") + cli.expect(r"\*{11}") + + +@pytest.mark.skipif("int(os.environ['REDIS_VERSION']) > 5") +def test_auth_hidden_password(clean_redis, cli): + cli.send("auth hello-world") + cli.expect("auth") + cli.expect(r"\*{11}") + + +def test_hello_command_is_not_supported(cli): + cli.sendline("hello 3") + cli.expect("IRedis currently not support RESP3") diff --git a/tests/cli_tests/test_completer.py b/tests/cli_tests/test_completer.py new file mode 100644 index 0000000..4ffd058 --- /dev/null +++ b/tests/cli_tests/test_completer.py @@ -0,0 +1,51 @@ +import pytest + + +def test_integer_type_completer(cli): + cli.expect("127.0.0.1") + cli.send("BITFIELD meykey GET ") + cli.expect(["i64", "u63", "u62"]) + cli.sendline("u4 #0") + cli.expect("127.0.0.1") + + cli.send("BITFIELD meykey GET ") + cli.expect(["u4", "i64", "u63", "u62"]) + + +def test_command_completion_when_a_command_is_another_command_substring( + cli, clean_redis +): + cli.expect("127.0.0.1") + cli.send("set") + cli.expect(["set", "setnx", "setex", "setbit", "setrange"]) + + cli.send("n") + cli.expect("setnx") + cli.send("x") + cli.expect("setnx") + cli.sendline("foo bar") + cli.expect(["1", "127.0.0.1"]) + + cli.send("setnx") + cli.expect("foo") + + +def test_command_completion_when_space_command(cli, clean_redis): + cli.expect("127.0.0.1") + + cli.send("command in") + cli.expect("command info") + + +@pytest.mark.skipif("int(os.environ['REDIS_VERSION']) < 6") +def test_username_completer(cli, iredis_client): + iredis_client.execute("acl setuser", "foo1") + iredis_client.execute("acl setuser", "bar2") + + cli.expect("127.0.0.1") + cli.sendline("acl users") + cli.expect("foo1") + + cli.send("acl deluser ") + cli.expect("foo1") + cli.expect("bar2") diff --git a/tests/cli_tests/test_config.py b/tests/cli_tests/test_config.py new file mode 100644 index 0000000..7d6839d --- /dev/null +++ b/tests/cli_tests/test_config.py @@ -0,0 +1,25 @@ +import pexpect +from textwrap import dedent +from pathlib import Path + + +def test_log_location_config(): + config_content = dedent( + """ + [main] + log_location = /tmp/iredis1.log + """ + ) + with open("/tmp/iredisrc", "w+") as etc_config: + etc_config.write(config_content) + + cli = pexpect.spawn("iredis -n 15 --iredisrc /tmp/iredisrc", timeout=1) + cli.expect("127.0.0.1") + cli.close() + + log = Path("/tmp/iredis1.log") + assert log.exists() + with open(log, "r") as logfile: + content = logfile.read() + + assert len(content) > 100 diff --git a/tests/cli_tests/test_dsn.py b/tests/cli_tests/test_dsn.py new file mode 100644 index 0000000..8c9528d --- /dev/null +++ b/tests/cli_tests/test_dsn.py @@ -0,0 +1,53 @@ +import os + +import pexpect +from textwrap import dedent +import pytest + + +def test_using_dsn(): + config_content = dedent( + """ + [alias_dsn] + local = redis://localhost:6379/15 + """ + ) + with open("/tmp/iredisrc", "w+") as etc_config: + etc_config.write(config_content) + + cli = pexpect.spawn("iredis --iredisrc /tmp/iredisrc --dsn local", timeout=1) + cli.logfile_read = open("cli_test.log", "ab") + cli.expect(["iredis", "localhost:6379[15]>"]) + cli.close() + + # overwrite with -n + cli = pexpect.spawn("iredis --iredisrc /tmp/iredisrc --dsn local -n 3", timeout=1) + cli.logfile_read = open("cli_test.log", "ab") + cli.expect(["iredis", "localhost:6379[3]>"]) + cli.close() + + # dsn not exists + cli = pexpect.spawn("iredis --iredisrc /tmp/iredisrc --dsn ghost-dsn", timeout=1) + cli.expect(["Could not find the specified DSN in the config file."]) + cli.close() + assert cli.status == 1 + + +@pytest.mark.skipif( + not os.path.exists("/tmp/redis/redis.sock"), reason="unix socket is not found" +) +def test_using_dsn_unix(): + config_content = dedent( + """ + [alias_dsn] + unix = unix:///tmp/redis/redis.sock?db=3 + """ + ) + with open("/tmp/iredisrc", "w+") as etc_config: + etc_config.write(config_content) + + cli = pexpect.spawn("iredis --iredisrc /tmp/iredisrc --dsn unix", timeout=2) + cli.logfile_read = open("cli_test.log", "ab") + cli.expect(["iredis", "redis /tmp/redis/redis.sock[3]>"]) + + cli.close() diff --git a/tests/cli_tests/test_history.py b/tests/cli_tests/test_history.py new file mode 100644 index 0000000..09e6c82 --- /dev/null +++ b/tests/cli_tests/test_history.py @@ -0,0 +1,42 @@ +import os +import pexpect +from pathlib import Path +from textwrap import dedent + + +def test_history_not_log_auth(cli): + cli.sendline("AUTH 123") + cli.expect(["Client sent AUTH, but no password is set", "127.0.0.1"]) + cli.sendline("set foo bar") + cli.expect("OK") + + with open(os.path.expanduser("~/.iredis_history"), "r") as history_file: + content = history_file.read() + + assert "set foo bar" in content + assert "AUTH" not in content + + +def test_history_create_and_writing_with_config(): + config_content = dedent( + """ + [main] + history_location = /tmp/iredis_history.txt + """ + ) + with open("/tmp/iredisrc", "w+") as etc_config: + etc_config.write(config_content) + + cli = pexpect.spawn("iredis -n 15 --iredisrc /tmp/iredisrc", timeout=2) + cli.expect("127.0.0.1") + cli.sendline("set hello world") + cli.expect("OK") + cli.close() + + log = Path("/tmp/iredis_history.txt") + assert log.exists() + + with open(log, "r") as logfile: + content = logfile.read() + + assert "set hello world" in content diff --git a/tests/cli_tests/test_on_error.py b/tests/cli_tests/test_on_error.py new file mode 100644 index 0000000..9ab8927 --- /dev/null +++ b/tests/cli_tests/test_on_error.py @@ -0,0 +1,12 @@ +def test_wrong_stream_type(clean_redis, cli): + clean_redis.lpush("mylist", "foo") + cli.sendline("xrange mylist 0 -1") + cli.expect("error") + cli.expect("Invalid stream ID specified as stream command argument") + + +def test_wrong_stream_type_in_raw_mode(clean_redis, raw_cli): + clean_redis.lpush("mylist", "foo") + raw_cli.sendline("xrange mylist 0 -1") + raw_cli.expect("ERROR") + raw_cli.expect("Invalid stream ID specified as stream command argument") diff --git a/tests/cli_tests/test_pager.py b/tests/cli_tests/test_pager.py new file mode 100644 index 0000000..38ced31 --- /dev/null +++ b/tests/cli_tests/test_pager.py @@ -0,0 +1,117 @@ +# noqa: F541 +import os +import sys +import pexpect +import pathlib +from contextlib import contextmanager +from textwrap import dedent + + +TEST_IREDISRC = "/tmp/.iredisrc.test" +TEST_PAGER_BOUNDARY = "---boundary---" +TEST_PAGER_BOUNDARY_NUMBER = "---88938347271---" + +env_pager = "{0} {1} {2}".format( + sys.executable, + os.path.join(pathlib.Path(__file__).parent, "wrappager.py"), + TEST_PAGER_BOUNDARY, +) +env_pager_numbers = "{0} {1} {2}".format( + sys.executable, + os.path.join(pathlib.Path(__file__).parent, "wrappager.py"), + TEST_PAGER_BOUNDARY_NUMBER, +) + + +@contextmanager +def pager_enabled_cli(): + env = os.environ + env["PAGER"] = env_pager + child = pexpect.spawn("iredis -n 15", timeout=3, env=env) + child.logfile_read = open("cli_test.log", "ab") + child.expect("127.0.0.1") + try: + yield child + finally: + child.close() + + +def test_using_pager_when_rows_too_high(clean_redis): + for index in range(100): + clean_redis.lpush("long-list", f"value-{index}") + with pager_enabled_cli() as child: + child.sendline("lrange long-list 0 -1") + child.expect(TEST_PAGER_BOUNDARY) + child.expect("value-1") + child.expect(TEST_PAGER_BOUNDARY) + + +def test_using_pager_works_for_help(): + with pager_enabled_cli() as child: + child.sendline("help set") + child.expect(TEST_PAGER_BOUNDARY) + child.expect("Set the string value of a key") + child.expect(TEST_PAGER_BOUNDARY) + + +def test_pager_works_for_peek(clean_redis): + for index in range(100): + clean_redis.lpush("long-list", f"value-{index}") + with pager_enabled_cli() as child: + child.sendline("peek long-list") + child.expect(TEST_PAGER_BOUNDARY) + child.expect("(quicklist)") + child.expect("value-1") + child.expect(TEST_PAGER_BOUNDARY) + + +def test_using_pager_from_config(clean_redis): + config_content = dedent( + f""" + [main] + log_location = /tmp/iredis1.log + pager = {env_pager_numbers} + """ + ) + + with open(TEST_IREDISRC, "w+") as test_iredisrc: + test_iredisrc.write(config_content) + + child = pexpect.spawn(f"iredis -n 15 --iredisrc {TEST_IREDISRC}", timeout=3) + child.logfile_read = open("cli_test.log", "ab") + child.expect("127.0.0.1") + for index in range(100): + clean_redis.lpush("long-list", f"value-{index}") + child.sendline("lrange long-list 0 -1") + child.expect(TEST_PAGER_BOUNDARY_NUMBER) + child.expect("value-1") + child.expect(TEST_PAGER_BOUNDARY_NUMBER) + child.close() + + +def test_using_pager_from_config_when_env_config_both_set(clean_redis): + config_content = dedent( + f""" + [main] + log_location = /tmp/iredis1.log + pager = {env_pager_numbers} + """ + ) + + with open(TEST_IREDISRC, "w+") as test_iredisrc: + test_iredisrc.write(config_content) + + env = os.environ + env["PAGER"] = env_pager + child = pexpect.spawn( + f"iredis -n 15 --iredisrc {TEST_IREDISRC}", timeout=3, env=env + ) + child.logfile_read = open("cli_test.log", "ab") + child.expect("127.0.0.1") + for index in range(100): + clean_redis.lpush("long-list", f"value-{index}") + child.sendline("lrange long-list 0 -1") + child.expect(TEST_PAGER_BOUNDARY_NUMBER) + child.expect("value-1") + child.expect(TEST_PAGER_BOUNDARY_NUMBER) + child.close() diff --git a/tests/cli_tests/test_self_implemented_command.py b/tests/cli_tests/test_self_implemented_command.py new file mode 100644 index 0000000..328d56b --- /dev/null +++ b/tests/cli_tests/test_self_implemented_command.py @@ -0,0 +1,14 @@ +""" +check ascii table: +http://ascii-table.com/ansi-escape-sequences.php +""" + + +def test_clear(cli): + cli.sendline("clear") + cli.expect("\\[2J") # clear screen + + +def test_exirt(cli): + cli.sendline("EXIT") + cli.expect("Goodbye!") diff --git a/tests/cli_tests/test_shell_pipeline.py b/tests/cli_tests/test_shell_pipeline.py new file mode 100644 index 0000000..4bacf8d --- /dev/null +++ b/tests/cli_tests/test_shell_pipeline.py @@ -0,0 +1,11 @@ +import pexpect + + +def test_running_disable_shell_pipeline(): + cli = pexpect.spawn("iredis -n 15 --no-shell", timeout=2) + cli.expect("127.0.0.1") + cli.sendline("set foo hello") + cli.expect("OK") + cli.sendline("get foo | grep w") + cli.expect(r"hello") + cli.close() diff --git a/tests/cli_tests/test_string_execute.py b/tests/cli_tests/test_string_execute.py new file mode 100644 index 0000000..310adab --- /dev/null +++ b/tests/cli_tests/test_string_execute.py @@ -0,0 +1,39 @@ +def test_set(cli): + cli.sendline("set foo bar") + cli.expect(["OK", "127.0.0.1"]) + + cli.sendline("set foo bar nx") + cli.expect(["(nil)", "127.0.0.1"]) + + cli.sendline("set foo bar xx") + cli.expect(["OK", "127.0.0.1"]) + + cli.sendline("set foo1 bar xx") + cli.expect(["(nil)", "127.0.0.1"]) + + +def test_get(cli): + cli.sendline("set foo bar") + cli.expect("OK") + + cli.sendline("get foo") + cli.expect('"bar"') + + +def test_delete_string(clean_redis, cli, config): + config.warning = True + cli.sendline("set foo bar") + cli.expect("OK") + cli.sendline("del foo") + cli.expect("Do you want to proceed") + cli.sendline("yes") + cli.expect("1") + + cli.sendline("get foo") + cli.expect("(nil)") + + +def test_on_dangerous_commands(cli, config): + config.warning = True + cli.sendline("keys *") + cli.expect("KEYS will hang redis server, use SCAN instead") diff --git a/tests/cli_tests/test_transaction.py b/tests/cli_tests/test_transaction.py new file mode 100644 index 0000000..b5c6b4e --- /dev/null +++ b/tests/cli_tests/test_transaction.py @@ -0,0 +1,70 @@ +import pytest +import pexpect + + +def test_trasaction_rprompt(clean_redis, cli): + cli.sendline("multi") + cli.expect(["OK", "transaction", "127.0.0.1"]) + + cli.sendline("get foo") + cli.expect(["QUEUED", "127.0.0.1", "transaction"]) + + cli.sendline("set hello world") + cli.expect(["QUEUED", "127.0.0.1", "transaction"]) + + cli.sendline("ping") + cli.expect(["QUEUED", "127.0.0.1", "transaction"]) + + cli.sendline("EXEC") + cli.expect("(nil)") + cli.expect("OK") + cli.expect("PONG") + cli.expect("127.0.0.1") + + with pytest.raises(pexpect.exceptions.TIMEOUT): + cli.expect("transaction") + + +def test_trasaction_syntax_error(cli): + cli.sendline("multi") + cli.sendline("get foo 1") + cli.expect(["wrong number of arguments for 'get' command", "transaction"]) + + cli.sendline("EXEC") + cli.expect("Transaction discarded because of previous errors.") + with pytest.raises(pexpect.exceptions.TIMEOUT): + cli.expect("transaction") + + +def test_trasaction_in_raw_mode(clean_redis, raw_cli): + clean_redis.set("foo", "bar") + + raw_cli.sendline("multi") + raw_cli.expect(["OK", "transaction", "127.0.0.1"]) + + raw_cli.sendline("get foo") + raw_cli.expect(["QUEUED", "127.0.0.1", "transaction"]) + + raw_cli.sendline("EXEC") + raw_cli.expect("bar") + raw_cli.expect("127.0.0.1") + + with pytest.raises(pexpect.exceptions.TIMEOUT): + raw_cli.expect("transaction") + + +def test_exec_return_nil_when_using_watch(clean_redis, cli): + cli.sendline("watch foo") + cli.expect("OK") + + cli.sendline("multi") + cli.expect("OK") + + cli.sendline("get bar") + cli.expect("QUEUED") + + # transaction will fail, return nil + clean_redis.set("foo", "hello!") + + cli.sendline("exec") + cli.expect("(nil)") diff --git a/tests/cli_tests/test_warning.py b/tests/cli_tests/test_warning.py new file mode 100644 index 0000000..402c51b --- /dev/null +++ b/tests/cli_tests/test_warning.py @@ -0,0 +1,51 @@ +from iredis.warning import is_dangerous + + +def test_is_dangerous(): + assert is_dangerous("KEYS") == ( + True, + "KEYS will hang redis server, use SCAN instead", + ) + + +def test_warning_for_dangerous_command(cli): + cli.sendline("config set save '900 1'") + cli.expect("Do you want to proceed?") + cli.sendline("yes") + + cli.sendline("config get save") + cli.expect("900 1") + + +def test_warnings_in_raw_mode(clean_redis, raw_cli): + clean_redis.set("foo", "bar") + raw_cli.sendline("keys *") + raw_cli.expect("Do you want to proceed?") + raw_cli.sendline("y") + raw_cli.expect("foo") + + +def test_warnings_in_raw_mode_canceled(clean_redis, raw_cli): + clean_redis.set("foo", "bar") + raw_cli.sendline("keys *") + raw_cli.expect("Do you want to proceed?") + raw_cli.sendline("n") + # the f should never appeared + raw_cli.expect("Canceled![^f]+127.0.0.1") + + +def test_warnings_confirmed(clean_redis, cli): + clean_redis.set("foo", "bar") + cli.sendline("keys *") + cli.expect("Do you want to proceed?") + cli.sendline("y") + cli.expect("foo") + + +def test_warnings_canceled(clean_redis, cli): + clean_redis.set("foo", "bar") + cli.sendline("keys *") + cli.expect("Do you want to proceed?") + cli.sendline("n") + # the f should never appeared + cli.expect("Canceled![^f]+127.0.0.1") diff --git a/tests/cli_tests/wrappager.py b/tests/cli_tests/wrappager.py new file mode 100644 index 0000000..dce86ce --- /dev/null +++ b/tests/cli_tests/wrappager.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +import sys +import fileinput + + +def wrappager(boundary): + print(boundary) + for line in fileinput.input(files="-"): + sys.stdout.write(line) + print(boundary) + + +if __name__ == "__main__": + wrappager(sys.argv[1]) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4d7d643 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,137 @@ +import os +import tempfile +from textwrap import dedent + +import pexpect +import pytest +import redis + +from iredis.client import Client +from iredis.commands import split_command_args +from iredis.redis_grammar import get_command_grammar +from iredis.exceptions import InvalidArguments +from iredis.config import Config, config as global_config + + +TIMEOUT = 2 +HISTORY_FILE = ".iredis_history" + + +@pytest.fixture +def judge_command(): + def judge_command_func(input_text, expect): + if expect == "invalid": + with pytest.raises(InvalidArguments): + split_command_args(input_text) + return + + command, _ = split_command_args(input_text) + grammar = get_command_grammar(command) + + m = grammar.match(input_text) + + # test on not match + if not expect: + assert m is None + return + + variables = m.variables() + print("Found variables: {}".format(variables)) + for expect_token, expect_value in expect.items(): + all_variables = variables.getall(expect_token) + if len(all_variables) > 1: + assert sorted(all_variables) == sorted(expect_value) + else: + assert variables.get(expect_token) == expect_value + + return judge_command_func + + +@pytest.fixture(scope="function") +def clean_redis(): + """ + Return a empty redis db. (redis-py client) + """ + client = redis.StrictRedis(db=15) + client.flushdb() + return client + + +@pytest.fixture +def iredis_client(): + return Client("127.0.0.1", "6379", db=15) + + +@pytest.fixture +def config(): + newconfig = Config() + global_config.__dict__ = newconfig.__dict__ + config.raw = False + return global_config + + +@pytest.fixture(scope="function") +def cli(): + """Open iredis subprocess to test""" + f = tempfile.TemporaryFile("w") + config_content = dedent( + """ + [main] + log_location = + warning = True + """ + ) + f.write(config_content) + f.close() + env = os.environ + env["PROMPT_TOOLKIT_NO_CPR"] = "1" + + child = pexpect.spawn(f"iredis -n 15 --iredisrc {f.name}", timeout=TIMEOUT, env=env) + child.logfile_read = open("cli_test.log", "ab") + child.expect(["https://iredis.io/issues", "127.0.0.1"]) + yield child + child.close() + + +@pytest.fixture(scope="function") +def raw_cli(): + """Open iredis subprocess to test""" + TEST_IREDISRC = "/tmp/.iredisrc.test" + config_content = dedent( + """ + [main] + log_location = + warning = True + """ + ) + + with open(TEST_IREDISRC, "w+") as test_iredisrc: + test_iredisrc.write(config_content) + + child = pexpect.spawn( + f"iredis --raw -n 15 --iredisrc {TEST_IREDISRC}", timeout=TIMEOUT + ) + child.logfile_read = open("cli_test.log", "ab") + child.expect(["https://iredis.io/issues", "127.0.0.1"]) + yield child + child.close() + + +@pytest.fixture(scope="function") +def cli_without_warning(): + f = tempfile.TemporaryFile("w") + config_content = dedent( + """ + [main] + log_location = /tmp/iredis1.log + warning = False + """ + ) + f.write(config_content) + f.close() + + cli = pexpect.spawn(f"iredis -n 15 --iredisrc {f.name}", timeout=1) + cli.logfile_read = open("cli_test.log", "ab") + yield cli + cli.close() + os.remove("/tmp/iredisrc") diff --git a/tests/unittests/command_parse/test_base_token.py b/tests/unittests/command_parse/test_base_token.py new file mode 100644 index 0000000..b04722e --- /dev/null +++ b/tests/unittests/command_parse/test_base_token.py @@ -0,0 +1,56 @@ +def test_ip_match(judge_command): + def ip_valid(ip, valid): + if valid: + judge_command( + f"cluster meet {ip} 6379", + {"command": "cluster meet", "ip": ip, "port": "6379"}, + ) + else: + judge_command(f"cluster meet {ip} 6379", None) + + ip_valid("192.168.0.1", True) + ip_valid("255.255.255.255", True) + ip_valid("192.168.0.256", False) + ip_valid("192.256.0.26", False) + ip_valid("192.255.256.26", False) + ip_valid("0.0.0.0", True) + ip_valid("99.999.100.1", False) + ip_valid("300.168.0.1", False) + + +def test_port_match(judge_command): + def port_valid(port, valid): + if valid: + judge_command( + f"cluster meet 192.168.0.1 {port}", + {"command": "cluster meet", "ip": "192.168.0.1", "port": port}, + ) + else: + judge_command(f"cluster meet 192.168.0.1 {port}", None) + + port_valid("65535", True) + port_valid("0", False) + port_valid("1", True) + port_valid("192.168.0.256", False) + port_valid("65536", False) + port_valid("65545", False) + port_valid("65635", False) + port_valid("66535", False) + port_valid("75535", False) + port_valid("1024", True) + port_valid("6553", True) + port_valid("99999", False) + port_valid("99999999", False) + + +def test_command_with_key_in_quotes(judge_command): + judge_command( + 'cluster keyslot "mykey"', {"command": "cluster keyslot", "key": '"mykey"'} + ) + judge_command( + 'cluster keyslot "\\"mykey"', + {"command": "cluster keyslot", "key": '"\\"mykey"'}, + ) + judge_command( + 'cluster keyslot "mykey "', {"command": "cluster keyslot", "key": '"mykey "'} + ) diff --git a/tests/unittests/command_parse/test_cluster.py b/tests/unittests/command_parse/test_cluster.py new file mode 100644 index 0000000..e60fe92 --- /dev/null +++ b/tests/unittests/command_parse/test_cluster.py @@ -0,0 +1,227 @@ +""" +redis command in `cluster` group parse test. +""" + + +def test_command_cluster_addslots(judge_command): + judge_command("cluster addslots 1", {"command": "cluster addslots", "slots": "1"}) + judge_command("CLUSTER ADDSLOTS 1", {"command": "CLUSTER ADDSLOTS", "slots": "1"}) + judge_command( + "cluster addslots 1 2 3 4", {"command": "cluster addslots", "slots": "1 2 3 4"} + ) + judge_command("cluster addslots 1 a", None) + judge_command("cluster addslots a", None) + judge_command("cluster addslots a 4", None) + judge_command("cluster addslots abc", None) + + +def test_command_cluster_count_failure_reports(judge_command): + judge_command( + "cluster count-failure-reports 1", + {"command": "cluster count-failure-reports", "node": "1"}, + ) + judge_command( + "CLUSTER COUNT-FAILURE-REPORTS 1", + {"command": "CLUSTER COUNT-FAILURE-REPORTS", "node": "1"}, + ) + judge_command("cluster count-failure-reports 1 2 3 4", None) + judge_command("cluster count-failure-reports 1 a", None) + judge_command("cluster count-failure-reports a", None) + judge_command("cluster count-failure-reports a 2", None) + judge_command("cluster count-failure-reports abc", None) + + +def test_command_cluster_countkeysinslot(judge_command): + judge_command( + "cluster countkeysinslot 1", {"command": "cluster countkeysinslot", "slot": "1"} + ) + judge_command( + "CLUSTER COUNTKEYSINSLOT 1", {"command": "CLUSTER COUNTKEYSINSLOT", "slot": "1"} + ) + judge_command("cluster countkeysinslot 1 2 3 4", None) + judge_command("cluster countkeysinslot 1 a", None) + judge_command("cluster countkeysinslot a", None) + judge_command("cluster countkeysinslot a 4", None) + judge_command("cluster countkeysinslot abc", None) + + +def test_command_cluster_delslots(judge_command): + judge_command("cluster delslots 1", {"command": "cluster delslots", "slots": "1"}) + judge_command("CLUSTER DELSLOTS 1", {"command": "CLUSTER DELSLOTS", "slots": "1"}) + judge_command( + "cluster delslots 1 2 3 4", {"command": "cluster delslots", "slots": "1 2 3 4"} + ) + judge_command("cluster delslots 1 a", None) + judge_command("cluster delslots a", None) + judge_command("cluster delslots a 4", None) + judge_command("cluster delslots abc", None) + + +def test_command_cluster_failover(judge_command): + judge_command( + "cluster failover force", + {"command": "cluster failover", "failoverchoice": "force"}, + ) + judge_command( + "cluster failover takeover", + {"command": "cluster failover", "failoverchoice": "takeover"}, + ) + judge_command( + "CLUSTER FAILOVER FORCE", + {"command": "CLUSTER FAILOVER", "failoverchoice": "FORCE"}, + ) + judge_command( + "CLUSTER FAILOVER takeover", + {"command": "CLUSTER FAILOVER", "failoverchoice": "takeover"}, + ) + judge_command( + "CLUSTER FAILOVER TAKEOVER", + {"command": "CLUSTER FAILOVER", "failoverchoice": "TAKEOVER"}, + ) + + +def test_command_cluster_forget(judge_command): + judge_command("cluster forget 1", {"command": "cluster forget", "node": "1"}) + judge_command( + "CLUSTER COUNT-FAILURE-REPORTS 1", + {"command": "CLUSTER COUNT-FAILURE-REPORTS", "node": "1"}, + ) + judge_command("cluster forget 1 2 3 4", None) + judge_command("cluster forget 1 a", None) + judge_command("cluster forget a", None) + judge_command("cluster forget a 2", None) + judge_command("cluster forget abc", None) + + +def test_command_cluster_getkeysinslot(judge_command): + judge_command( + "cluster getkeysinslot 1 1", + {"command": "cluster getkeysinslot", "slot": "1", "count": "1"}, + ) + judge_command( + "CLUSTER GETKEYSINSLOT 1 1", + {"command": "CLUSTER GETKEYSINSLOT", "slot": "1", "count": "1"}, + ) + judge_command( + "cluster getkeysinslot 1123 1121", + {"command": "cluster getkeysinslot", "slot": "1123", "count": "1121"}, + ) + judge_command("cluster getkeysinslot 1 2 3 4", None) + judge_command("cluster getkeysinslot 1 a", None) + judge_command("cluster getkeysinslot a", None) + judge_command("cluster getkeysinslot a 4", None) + judge_command("cluster getkeysinslot abc", None) + + +def test_command_cluster_info(judge_command): + judge_command("cluster info", {"command": "cluster info"}) + judge_command("CLUSTER INFO", {"command": "CLUSTER INFO"}) + judge_command("CLUSTER INFO 1", None) + judge_command("Acluster info", "invalid") + + +def test_command_cluster_keyslot(judge_command): + judge_command( + "cluster keyslot mykey", {"command": "cluster keyslot", "key": "mykey"} + ) + judge_command( + "cluster keyslot MYKEY", {"command": "cluster keyslot", "key": "MYKEY"} + ) + judge_command( + "CLUSTER KEYSLOT MYKEY", {"command": "CLUSTER KEYSLOT", "key": "MYKEY"} + ) + + +def test_command_cluster_meet(judge_command): + judge_command( + "cluster meet 192.168.0.1 12200", + {"command": "cluster meet", "ip": "192.168.0.1", "port": "12200"}, + ) + judge_command( + "CLUSTER MEET 192.168.0.1 12200", + {"command": "CLUSTER MEET", "ip": "192.168.0.1", "port": "12200"}, + ) + + +def test_command_cluster_nodes(judge_command): + judge_command("cluster nodes", {"command": "cluster nodes"}) + judge_command("CLUSTER NODES", {"command": "CLUSTER NODES"}) + + +def test_command_cluster_reset(judge_command): + judge_command( + "cluster reset hard", {"command": "cluster reset", "resetchoice": "hard"} + ) + judge_command( + "cluster reset soft", {"command": "cluster reset", "resetchoice": "soft"} + ) + judge_command( + "CLUSTER RESET HARD", {"command": "CLUSTER RESET", "resetchoice": "HARD"} + ) + judge_command( + "CLUSTER RESET soft", {"command": "CLUSTER RESET", "resetchoice": "soft"} + ) + judge_command( + "CLUSTER RESET SOFT", {"command": "CLUSTER RESET", "resetchoice": "SOFT"} + ) + judge_command("CLUSTER RESET SOFT1", None) + judge_command("CLUSTER RESET SAOFT", None) + + +def test_command_cluster_set_config_epoch(judge_command): + judge_command("cluster set-config-epoch 123123 ad", None) + judge_command( + "cluster set-config-epoch 0 ", + {"command": "cluster set-config-epoch", "epoch": "0"}, + ) + judge_command( + "cluster set-config-epoch 123123 ", + {"command": "cluster set-config-epoch", "epoch": "123123"}, + ) + + +def test_command_cluster_set_slot(judge_command): + judge_command( + "cluster setslot 123 importing 123123", + { + "command": "cluster setslot", + "slot": "123", + "slotsubcmd": "importing", + "node": "123123", + }, + ) + judge_command( + "cluster setslot 123 migrating 123123", + { + "command": "cluster setslot", + "slot": "123", + "slotsubcmd": "migrating", + "node": "123123", + }, + ) + judge_command( + "cluster setslot 123 node 123123", + { + "command": "cluster setslot", + "slot": "123", + "slotsubcmd": "node", + "node": "123123", + }, + ) + judge_command( + "cluster setslot 123 MIGRATING 123123", + { + "command": "cluster setslot", + "slot": "123", + "slotsubcmd": "MIGRATING", + "node": "123123", + }, + ) + judge_command( + "cluster setslot 123 stable", + {"command": "cluster setslot", "slot": "123", "slotsubcmd": "stable"}, + ) + judge_command( + "cluster setslot 123 STABLE", + {"command": "cluster setslot", "slot": "123", "slotsubcmd": "STABLE"}, + ) diff --git a/tests/unittests/command_parse/test_connection.py b/tests/unittests/command_parse/test_connection.py new file mode 100644 index 0000000..8952e53 --- /dev/null +++ b/tests/unittests/command_parse/test_connection.py @@ -0,0 +1,97 @@ +def test_auth(judge_command): + judge_command("auth 123", {"command": "auth", "password": "123"}) + + +def test_auth_redis6(judge_command): + from iredis.commands import command2syntax + from iredis.redis_grammar import get_command_grammar + + get_command_grammar.cache_clear() + + old = command2syntax["AUTH"] + command2syntax["AUTH"] = "command_usernamex_password" + judge_command( + "auth default 123", + {"command": "auth", "password": "123", "username": "default"}, + ) + judge_command("AUTH 123", {"command": "AUTH", "password": "123"}) + command2syntax["AUTH"] = old + + +def test_echo(judge_command): + judge_command("echo hello", {"command": "echo", "message": "hello"}) + + +def test_ping(judge_command): + judge_command("ping hello", {"command": "ping", "message": "hello"}) + judge_command("ping", {"command": "ping", "message": None}) + judge_command("ping hello world", None) + + +def test_select(judge_command): + for index in range(16): + judge_command(f"select {index}", {"command": "select", "index": str(index)}) + for index in range(16, 100): + judge_command(f"select {index}", None) + judge_command("select acb", None) + + +def test_swapdb(judge_command): + for index1 in range(16): + for index2 in range(16): + judge_command( + f"swapdb {index1} {index2}", + {"command": "swapdb", "index": [str(index1), str(index2)]}, + ) + judge_command("swapdb abc 1", None) + judge_command("swapdb 1", None) + + +def test_client_caching(judge_command): + judge_command("CLIENT CACHING YES", {"command": "CLIENT CACHING", "yes": "YES"}) + judge_command("CLIENT CACHING NO", {"command": "CLIENT CACHING", "yes": "NO"}) + judge_command("CLIENT CACHING", None) + judge_command("CLIENT CACHING abc", None) + + +def test_client_tracking(judge_command): + judge_command("CLIENT TRACKING on", {"command": "CLIENT TRACKING", "on_off": "on"}) + judge_command( + "CLIENT TRACKING ON REDIRECT 123", + { + "command": "CLIENT TRACKING", + "on_off": "ON", + "redirect_const": "REDIRECT", + "clientid": "123", + }, + ) + judge_command( + "CLIENT TRACKING ON PREFIX foo", + { + "command": "CLIENT TRACKING", + "on_off": "ON", + "prefix_const": "PREFIX", + "prefix": "foo", + }, + ) + judge_command( + "CLIENT TRACKING ON PREFIX foo", + { + "command": "CLIENT TRACKING", + "on_off": "ON", + "prefix_const": "PREFIX", + "prefix": "foo", + }, + ) + judge_command( + "CLIENT TRACKING ON PREFIX foo BCAST NOLOOP OPTIN", + { + "command": "CLIENT TRACKING", + "on_off": "ON", + "prefix_const": "PREFIX", + "prefix": "foo", + "bcast_const": "BCAST", + "noloop_const": "NOLOOP", + "optin_const": "OPTIN", + }, + ) diff --git a/tests/unittests/command_parse/test_generic_parse.py b/tests/unittests/command_parse/test_generic_parse.py new file mode 100644 index 0000000..f41d887 --- /dev/null +++ b/tests/unittests/command_parse/test_generic_parse.py @@ -0,0 +1,179 @@ +def test_del(judge_command): + judge_command("DEL abc", {"command": "DEL", "keys": "abc"}) + judge_command("DEL bc1", {"command": "DEL", "keys": "bc1"}) + judge_command("DEL 1", {"command": "DEL", "keys": "1"}) + judge_command('DEL "hello world"', {"command": "DEL", "keys": '"hello world"'}) + judge_command( + r'DEL "hell\"o world"', {"command": "DEL", "keys": r'"hell\"o world"'} + ) + judge_command("DEL abc def", {"command": "DEL", "keys": "abc def"}) + judge_command("DEL 1 2", {"command": "DEL", "keys": "1 2"}) + judge_command("DEL 'he \"llo'", {"command": "DEL", "keys": "'he \"llo'"}) + judge_command( + """DEL 'he "llo' "abc" 'def' """, + {"command": "DEL", "keys": "'he \"llo' \"abc\" 'def'"}, + ) + + +def test_exists(judge_command): + judge_command("EXISTS foo bar", {"command": "EXISTS", "keys": "foo bar"}) + judge_command("EXISTS foo", {"command": "EXISTS", "keys": "foo"}) + judge_command("EXISTS 1", {"command": "EXISTS", "keys": "1"}) + judge_command('EXISTS "foo bar"', {"command": "EXISTS", "keys": '"foo bar"'}) + judge_command(r'EXISTS "\""', {"command": "EXISTS", "keys": r'"\""'}) + + +def test_expire(judge_command): + judge_command("EXPIRE key 12", {"command": "EXPIRE", "key": "key", "second": "12"}) + judge_command("EXPIRE key a12", None) + judge_command("EXPIRE 12 12", {"command": "EXPIRE", "key": "12", "second": "12"}) + judge_command("EXPIRE 12", None) + + +def test_expireat(judge_command): + judge_command( + "EXPIRE key 1565787643", + {"command": "EXPIRE", "key": "key", "second": "1565787643"}, + ) + judge_command("EXPIRE key a12", None) + + +def test_keys(judge_command): + judge_command("KEYS *", {"command": "KEYS", "pattern": "*"}) + judge_command("KEYS *abc", {"command": "KEYS", "pattern": "*abc"}) + judge_command("keys abc*", {"command": "keys", "pattern": "abc*"}) + + +def test_move(judge_command): + judge_command("MOVE key 14", {"command": "MOVE", "key": "key", "index": "14"}) + + +def test_pexpire(judge_command): + judge_command( + "PEXPIRE key 12", {"command": "PEXPIRE", "key": "key", "millisecond": "12"} + ) + judge_command("PEXPIRE key a12", None) + judge_command( + "PEXPIRE 12 12", {"command": "PEXPIRE", "key": "12", "millisecond": "12"} + ) + judge_command("PEXPIRE 12", None) + + +def test_pexpireat(judge_command): + judge_command( + "PEXPIREAT key 1565787643", + {"command": "PEXPIREAT", "key": "key", "timestampms": "1565787643"}, + ) + judge_command("PEXPIREAT key a12", None) + + +def test_rename(judge_command): + judge_command( + "rename key newkey", {"command": "rename", "key": "key", "newkey": "newkey"} + ) + judge_command( + "rename 123 newkey", {"command": "rename", "key": "123", "newkey": "newkey"} + ) + judge_command("rename 123 ", None) + + +def test_scan(judge_command): + judge_command( + "SCAN 0 MATCH task* COUNT 15 TYPE string", + { + "command": "SCAN", + "cursor": "0", + "match": "MATCH", + "pattern": "task*", + "count_const": "COUNT", + "count": "15", + "type_const": "TYPE", + "type": "string", + }, + ) + judge_command("SCAN 0", {"command": "SCAN", "cursor": "0"}) + judge_command( + "SCAN 0 MATCH task*", + {"command": "SCAN", "cursor": "0", "match": "MATCH", "pattern": "task*"}, + ) + judge_command( + "SCAN 0 COUNT 15 TYPE string", + { + "command": "SCAN", + "cursor": "0", + "count_const": "COUNT", + "count": "15", + "type_const": "TYPE", + "type": "string", + }, + ) + + +def test_migrate(judge_command): + judge_command( + 'MIGRATE 192.168.1.34 6379 " " 0 5000 KEYS key1 key2 key3', + { + "command": "MIGRATE", + "host": "192.168.1.34", + "port": "6379", + "key": '" "', + "index": "0", + "timeout": "5000", + "const_keys": "KEYS", + "keys": "key1 key2 key3", + }, + ) + judge_command( + "MIGRATE 192.168.1.34 6379 foo 0 5000 auth password1 KEYS key1 key2 key3", + { + "command": "MIGRATE", + "host": "192.168.1.34", + "port": "6379", + "key": "foo", + "index": "0", + "timeout": "5000", + "const_keys": "KEYS", + "keys": "key1 key2 key3", + "auth": "auth", + "password": "password1", + }, + ) + judge_command( + "MIGRATE 192.168.1.34 6379 foo 0 5000 auth username1 password1 KEYS key1 key2 key3", + { + "command": "MIGRATE", + "host": "192.168.1.34", + "port": "6379", + "key": "foo", + "index": "0", + "timeout": "5000", + "const_keys": "KEYS", + "keys": "key1 key2 key3", + "auth": "auth", + "password": "password1", + "username": "username1", + }, + ) + + +def test_object(judge_command): + judge_command( + "object refcount mylist", + {"command": "object", "object": "refcount", "key": "mylist"}, + ) + + +def test_wait(judge_command): + judge_command("WAIT 3 100", {"command": "WAIT", "count": "3", "timeout": "100"}) + + +def test_restore(judge_command): + judge_command( + 'RESTORE mykey 0 "\n\x17\x17\x00\x00\x00\x12\x00\x00\x00\x03\x00\x00\xc0\x01\x00\x04\xc0\x02\x00\x04\xc0\x03\x00\xff\x04\x00u#<\xc0;.\xe9\xdd"', # noqa + { + "command": "RESTORE", + "key": "mykey", + "timeout": "0", + "value": '"\n\x17\x17\x00\x00\x00\x12\x00\x00\x00\x03\x00\x00\xc0\x01\x00\x04\xc0\x02\x00\x04\xc0\x03\x00\xff\x04\x00u#<\xc0;.\xe9\xdd"', # noqa + }, + ) diff --git a/tests/unittests/command_parse/test_geo.py b/tests/unittests/command_parse/test_geo.py new file mode 100644 index 0000000..8a8d4c7 --- /dev/null +++ b/tests/unittests/command_parse/test_geo.py @@ -0,0 +1,39 @@ +def test_geoadd(judge_command): + judge_command( + 'GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"', + { + "command": "GEOADD", + "key": "Sicily", + "longitude": "15.087269", + "latitude": "37.502669", + "member": '"Catania"', + }, + ) + + +def test_georadiusbymember(judge_command): + judge_command( + "GEORADIUSBYMEMBER Sicily Agrigento 100 km", + { + "command": "GEORADIUSBYMEMBER", + "key": "Sicily", + "member": "Agrigento", + "float": "100", + "distunit": "km", + }, + ) + + +def test_georadius(judge_command): + judge_command( + "GEORADIUS Sicily 15 37 200 km WITHDIST WITHCOORD ", + { + "command": "GEORADIUS", + "key": "Sicily", + "longitude": "15", + "latitude": "37", + "float": "200", + "distunit": "km", + "geochoice": "WITHCOORD", + }, + ) diff --git a/tests/unittests/command_parse/test_hash_parse.py b/tests/unittests/command_parse/test_hash_parse.py new file mode 100644 index 0000000..0c3aa04 --- /dev/null +++ b/tests/unittests/command_parse/test_hash_parse.py @@ -0,0 +1,45 @@ +def test_hdel(judge_command): + judge_command("HDEL foo bar", {"command": "HDEL", "key": "foo", "fields": "bar"}) + judge_command( + "HDEL foo bar hello world", + {"command": "HDEL", "key": "foo", "fields": "bar hello world"}, + ) + + +def test_hmset(judge_command): + judge_command( + "HMSET foo bar hello-world", + {"command": "HMSET", "key": "foo", "field": "bar", "value": "hello-world"}, + ) + judge_command( + "HMSET foo bar hello-world key2 value2", + {"command": "HMSET", "key": "foo", "field": "key2", "value": "value2"}, + ) + + +def test_hexists(judge_command): + judge_command( + "HEXISTS foo bar", {"command": "HEXISTS", "key": "foo", "field": "bar"} + ) + judge_command("HEXISTS foo bar hello-world", None) + + +def test_hincrby(judge_command): + judge_command( + "HINCRBY foo bar 12", + {"command": "HINCRBY", "key": "foo", "field": "bar", "delta": "12"}, + ) + + +def test_hincrbyfloat(judge_command): + judge_command( + "HINCRBYFLOAT foo bar 12.1", + {"command": "HINCRBYFLOAT", "key": "foo", "field": "bar", "float": "12.1"}, + ) + + +def test_hset(judge_command): + judge_command( + "HSET foo bar hello", + {"command": "HSET", "key": "foo", "field": "bar", "value": "hello"}, + ) diff --git a/tests/unittests/command_parse/test_hll_parse.py b/tests/unittests/command_parse/test_hll_parse.py new file mode 100644 index 0000000..b139eed --- /dev/null +++ b/tests/unittests/command_parse/test_hll_parse.py @@ -0,0 +1,5 @@ +def test_pfmerge(judge_command): + judge_command( + "PFMERGE hll3 hll1 hll2", + {"command": "PFMERGE", "newkey": "hll3", "keys": "hll1 hll2"}, + ) diff --git a/tests/unittests/command_parse/test_list_parse.py b/tests/unittests/command_parse/test_list_parse.py new file mode 100644 index 0000000..e60d7a2 --- /dev/null +++ b/tests/unittests/command_parse/test_list_parse.py @@ -0,0 +1,108 @@ +def test_rpush(judge_command): + judge_command( + "RPUSH list1 foo bar hello world", + {"command": "RPUSH", "key": "list1", "values": "foo bar hello world"}, + ) + judge_command( + "LPUSH list1 foo", {"command": "LPUSH", "key": "list1", "values": "foo"} + ) + + +def test_lindex(judge_command): + judge_command( + "LINDEX list1 10", {"command": "LINDEX", "key": "list1", "position": "10"} + ) + judge_command( + "LINDEX list1 -10", {"command": "LINDEX", "key": "list1", "position": "-10"} + ) + judge_command("LINDEX list1 1.1", None) + + +def test_lset(judge_command): + judge_command( + "LSET list1 10 newbie", + {"command": "LSET", "key": "list1", "position": "10", "value": "newbie"}, + ) + judge_command( + "LSET list1 -1 newbie", + {"command": "LSET", "key": "list1", "position": "-1", "value": "newbie"}, + ) + + +def test_brpoplpush(judge_command): + judge_command( + "BRPOPLPUSH list1 list2 10", + {"command": "BRPOPLPUSH", "key": "list1", "newkey": "list2", "timeout": "10"}, + ) + judge_command( + "BRPOPLPUSH list1 list2 0", + {"command": "BRPOPLPUSH", "key": "list1", "newkey": "list2", "timeout": "0"}, + ) + judge_command("BRPOPLPUSH list1 list2 -1", None) + + +def test_linsert(judge_command): + judge_command( + 'LINSERT mylist BEFORE "World" "There"', + { + "command": "LINSERT", + "key": "mylist", + "position_choice": "BEFORE", + "value": ['"World"', '"There"'], + }, + ) + judge_command( + 'LINSERT mylist after "World" "There"', + { + "command": "LINSERT", + "key": "mylist", + "position_choice": "after", + "value": ['"World"', '"There"'], + }, + ) + + +def test_lpos(judge_command): + judge_command("LPOS mylist c", {"command": "LPOS", "key": "mylist", "element": "c"}) + judge_command( + "LPOS mylist c RANK 2", + { + "command": "LPOS", + "key": "mylist", + "element": "c", + "rank_const": "RANK", + "rank": "2", + }, + ) + judge_command( + "LPOS mylist c RANK -1", + { + "command": "LPOS", + "key": "mylist", + "element": "c", + "rank_const": "RANK", + "rank": "-1", + }, + ) + judge_command( + "LPOS mylist c COUNT 2", + { + "command": "LPOS", + "key": "mylist", + "element": "c", + "count_const": "COUNT", + "count": "2", + }, + ) + judge_command( + "LPOS mylist c RANK -1 COUNT 2", + { + "command": "LPOS", + "key": "mylist", + "element": "c", + "count_const": "COUNT", + "count": "2", + "rank_const": "RANK", + "rank": "-1", + }, + ) diff --git a/tests/unittests/command_parse/test_pubsub.py b/tests/unittests/command_parse/test_pubsub.py new file mode 100644 index 0000000..c7a2b54 --- /dev/null +++ b/tests/unittests/command_parse/test_pubsub.py @@ -0,0 +1,15 @@ +def test_publish(judge_command): + judge_command( + "publish foo bar", {"command": "publish", "channel": "foo", "message": "bar"} + ) + + +def test_subscribe(judge_command): + judge_command("subscribe foo bar", {"command": "subscribe", "channel": "bar"}) + + +def test_pubsub(judge_command): + judge_command( + "PUBSUB NUMSUB foo bar", + {"command": "PUBSUB", "pubsubcmd": "NUMSUB", "channel": "bar"}, + ) diff --git a/tests/unittests/command_parse/test_script.py b/tests/unittests/command_parse/test_script.py new file mode 100644 index 0000000..3b984c3 --- /dev/null +++ b/tests/unittests/command_parse/test_script.py @@ -0,0 +1,22 @@ +def test_eval(judge_command): + judge_command( + 'eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second', + { + "command": "eval", + "double_lua": "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", + "any": "2 key1 key2 first second", + }, + ) + judge_command( + "eval 'return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}' 2 key1 key2 first second", + { + "command": "eval", + "single_lua": "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", + "any": "2 key1 key2 first second", + }, + ) + + +def test_scriptdebug(judge_command): + judge_command("SCRIPT DEBUG YES", {"command": "SCRIPT DEBUG", "scriptdebug": "YES"}) + judge_command("SCRIPT DEBUG no", {"command": "SCRIPT DEBUG", "scriptdebug": "no"}) diff --git a/tests/unittests/command_parse/test_server.py b/tests/unittests/command_parse/test_server.py new file mode 100644 index 0000000..5129e5f --- /dev/null +++ b/tests/unittests/command_parse/test_server.py @@ -0,0 +1,201 @@ +def test_client_setname(judge_command): + judge_command( + "CLIENT SETNAME foobar", {"command": "CLIENT SETNAME", "value": "foobar"} + ) + + +def test_client_unblock(judge_command): + judge_command( + "CLIENT UNBLOCK 33 TIMEOUT", + {"command": "CLIENT UNBLOCK", "clientid": "33", "error": "TIMEOUT"}, + ) + judge_command("CLIENT UNBLOCK 33", {"command": "CLIENT UNBLOCK", "clientid": "33"}) + + +def test_flushdb(judge_command): + judge_command("FLUSHDB async", {"command": "FLUSHDB", "async": "async"}) + judge_command("FLUSHDB", {"command": "FLUSHDB"}) + judge_command("FLUSHDB ASYNC", {"command": "FLUSHDB", "async": "ASYNC"}) + judge_command("FLUSHALL ASYNC", {"command": "FLUSHALL", "async": "ASYNC"}) + + +def test_client_list(judge_command): + judge_command("client list", {"command": "client list"}) + judge_command("client list TYPE REPLICA1", None) + judge_command( + "client list type master", + {"command": "client list", "type_const": "type", "conntype": "master"}, + ) + judge_command( + "client list TYPE REPLICA", + {"command": "client list", "type_const": "TYPE", "conntype": "REPLICA"}, + ) + + +def test_configset(judge_command): + judge_command( + "config set foo bar", + {"command": "config set", "parameter": "foo", "value": "bar"}, + ) + judge_command( + "config set requirepass ''", + {"command": "config set", "parameter": "requirepass", "value": "''"}, + ) + + +def test_shutdown(judge_command): + judge_command("shutdown save", {"command": "shutdown", "shutdown": "save"}) + judge_command("shutdown NOSAVE", {"command": "shutdown", "shutdown": "NOSAVE"}) + + +def test_clientpause(judge_command): + judge_command("client pause 3000", {"command": "client pause", "timeout": "3000"}) + + +def test_client_reply(judge_command): + judge_command("client reply on", {"command": "client reply", "switch": "on"}) + + +def test_client_kill(judge_command): + judge_command( + "CLIENT KILL addr 127.0.0.1:12345 type pubsub", + { + "command": "CLIENT KILL", + "addr": "addr", + "ip_port": "127.0.0.1:12345", + "type_const": "type", + "conntype": "pubsub", + }, + ) + judge_command( + "CLIENT KILL 127.0.0.1:12345 ", + {"command": "CLIENT KILL", "ip_port": "127.0.0.1:12345"}, + ) + judge_command( + "CLIENT KILL id 123455 type pubsub skipme no", + { + "command": "CLIENT KILL", + "const_id": "id", + "clientid": "123455", + "type_const": "type", + "conntype": "pubsub", + "skipme": "skipme", + "yes": "no", + }, + ) + + +def test_client_kill_username(judge_command): + """since redis-server 6.0""" + judge_command( + "client kill USER default", + {"command": "client kill", "const_user": "USER", "username": "default"}, + ) + + +def test_client_kill_unordered_arguments(judge_command): + judge_command( + "CLIENT KILL type pubsub addr 127.0.0.1:12345", + { + "command": "CLIENT KILL", + "addr": "addr", + "ip_port": "127.0.0.1:12345", + "type_const": "type", + "conntype": "pubsub", + }, + ) + + +def test_psync(judge_command): + judge_command( + "PSYNC abc 123", {"command": "PSYNC", "replicationid": "abc", "offset": "123"} + ) + judge_command("PSYNC", None) + + +def test_latency_graph(judge_command): + judge_command( + "latency graph command", {"command": "latency graph", "graphevent": "command"} + ) + judge_command( + "latency graph fork", {"command": "latency graph", "graphevent": "fork"} + ) + judge_command("latency graph", None) + + +def test_latency_reset(judge_command): + judge_command( + "latency reset command fork aof-fsync-always", + {"command": "latency reset", "graphevent": "aof-fsync-always"}, + ) + judge_command( + "latency reset fork", {"command": "latency reset", "graphevent": "fork"} + ) + judge_command("latency reset", {"command": "latency reset"}) + + +def test_lolwut(judge_command): + judge_command("lolwut", {"command": "lolwut"}) + # only works before redis 6 + judge_command("lolwut 5", {"command": "lolwut", "any": "5"}) + judge_command("lolwut 5 1", {"command": "lolwut", "any": "5 1"}) + # redis 6 + judge_command( + "lolwut VERSION 5 5", + {"command": "lolwut", "version": "VERSION", "version_num": "5", "any": "5"}, + ) + + +def test_info(judge_command): + judge_command("info cpu", {"command": "info", "section": "cpu"}) + judge_command("info", {"command": "info"}) + judge_command("info all", {"command": "info", "section": "all"}) + judge_command("info CPU", {"command": "info", "section": "CPU"}) + + +def test_bgsave(judge_command): + judge_command("bgsave", {"command": "bgsave"}) + judge_command("bgsave schedule", {"command": "bgsave", "schedule": "schedule"}) + judge_command("BGSAVE SCHEDULE", {"command": "BGSAVE", "schedule": "SCHEDULE"}) + + +def test_acl_cat(judge_command): + judge_command("acl cat", {"command": "acl cat"}) + judge_command("acl CAT", {"command": "acl CAT"}) + judge_command( + "ACL CAT scripting", {"command": "ACL CAT", "categoryname": "scripting"} + ) + judge_command("ACL CAT WATCH", {"command": "ACL CAT", "categoryname": "WATCH"}) + + +def test_acl_deluser(judge_command): + judge_command( + "acl deluser laixintao", {"command": "acl deluser", "username": "laixintao"} + ) + judge_command( + "acl deluser laixintao antirez", + {"command": "acl deluser", "username": "antirez"}, + ) + + +def test_acl_log(judge_command): + judge_command("acl log 2", {"command": "acl log", "count": "2"}) + judge_command("acl log reset", {"command": "acl log", "reset_const": "reset"}) + judge_command("acl log ", {"command": "acl log"}) + + +def test_acl_setuser(judge_command): + judge_command("ACL SETUSER alice", {"command": "ACL SETUSER", "username": "alice"}) + judge_command( + "ACL SETUSER alice on >p1pp0 ~cached:* +get", + {"command": "ACL SETUSER", "username": "alice", "rule": "+get"}, + ) + judge_command( + "ACL SETUSER alan allkeys +@string +@set -SADD >alanpassword", + {"command": "ACL SETUSER", "username": "alan", "rule": ">alanpassword"}, + ) + + +def test_acl_getuser(judge_command): + judge_command("acl getuser alan", {"command": "acl getuser", "username": "alan"}) + judge_command("acl getuser", None) diff --git a/tests/unittests/command_parse/test_set_parse.py b/tests/unittests/command_parse/test_set_parse.py new file mode 100644 index 0000000..d35a52b --- /dev/null +++ b/tests/unittests/command_parse/test_set_parse.py @@ -0,0 +1,40 @@ +def test_sadd(judge_command): + judge_command( + "SADD foo m1 m2 m3", {"command": "SADD", "key": "foo", "members": "m1 m2 m3"} + ) + judge_command("SADD foo m1", {"command": "SADD", "key": "foo", "members": "m1"}) + judge_command("SADD foo", None) + + +def test_sdiffstore(judge_command): + judge_command( + "SDIFFSTORE foo m1 m2 m3", + {"command": "SDIFFSTORE", "destination": "foo", "keys": "m1 m2 m3"}, + ) + judge_command( + "SDIFFSTORE foo m1", + {"command": "SDIFFSTORE", "destination": "foo", "keys": "m1"}, + ) + judge_command("SDIFFSTORE foo", None) + + +def test_is_member(judge_command): + judge_command("SISMEMBER foo m1 m2 m3", None) + judge_command( + "SISMEMBER foo m1", {"command": "SISMEMBER", "key": "foo", "member": "m1"} + ) + judge_command("SISMEMBER foo", None) + + +def test_smove(judge_command): + judge_command( + "SMOVE foo bar m2", + {"command": "SMOVE", "key": "foo", "newkey": "bar", "member": "m2"}, + ) + judge_command("SMOVE foo m1", None) + judge_command("SMOVE foo", None) + + +def test_spop(judge_command): + judge_command("SPOP set", {"command": "SPOP", "key": "set"}) + judge_command("SPOP set 3", {"command": "SPOP", "key": "set", "count": "3"}) diff --git a/tests/unittests/command_parse/test_sorted_set_parse.py b/tests/unittests/command_parse/test_sorted_set_parse.py new file mode 100644 index 0000000..a0af616 --- /dev/null +++ b/tests/unittests/command_parse/test_sorted_set_parse.py @@ -0,0 +1,172 @@ +import pytest + + +def test_zcount(judge_command): + judge_command( + "zcount foo -10 0", + {"command": "zcount", "key": "foo", "min": "-10", "max": "0"}, + ) + + +def test_bzpopmax(judge_command): + judge_command( + "bzpopmax set set2 set3 4", + {"command": "bzpopmax", "keys": "set set2 set3", "timeout": "4"}, + ) + judge_command( + "bzpopmin set 4", {"command": "bzpopmin", "keys": "set", "timeout": "4"} + ) + + +def test_zadd(judge_command): + judge_command( + "zadd t 100 qewqr 23 pp 11 oo", + { + "command": "zadd", + "key": "t", + "score": "11", # FIXME: only have last one + "member": "oo", + }, + ) + judge_command( + "zadd t incr 100 foo", + { + "command": "zadd", + "key": "t", + "incr": "incr", + "score": "100", # FIXME: only have last one + "member": "foo", + }, + ) + judge_command( + "zadd t NX CH incr 100 foo", + { + "command": "zadd", + "key": "t", + "condition": "NX", + "changed": "CH", + "incr": "incr", + "score": "100", # FIXME: only have last one + "member": "foo", + }, + ) + + +def test_zincrby(judge_command): + judge_command( + "zincrby t 10 foo", + {"command": "zincrby", "key": "t", "float": "10", "member": "foo"}, + ) + judge_command( + "zincrby t 2.3 foo", + {"command": "zincrby", "key": "t", "float": "2.3", "member": "foo"}, + ) + + +def test_zlexcount(judge_command): + judge_command( + "zlexcount a - +", + {"command": "zlexcount", "key": "a", "lexmin": "-", "lexmax": "+"}, + ) + judge_command( + "zlexcount a (aaaa [z", + {"command": "zlexcount", "key": "a", "lexmin": "(aaaa", "lexmax": "[z"}, + ) + judge_command( + "ZLEXCOUNT myset - [c", + {"command": "ZLEXCOUNT", "key": "myset", "lexmin": "-", "lexmax": "[c"}, + ) + judge_command( + "ZLEXCOUNT myset [aaa (g", + {"command": "ZLEXCOUNT", "key": "myset", "lexmin": "[aaa", "lexmax": "(g"}, + ) + + +def test_zrange(judge_command): + judge_command( + "zrange foo -1 10", + {"command": "zrange", "key": "foo", "start": "-1", "end": "10"}, + ) + judge_command( + "zrange foo 0 -1", + {"command": "zrange", "key": "foo", "start": "0", "end": "-1"}, + ) + judge_command( + "zrange foo 0 -1 withscores", + { + "command": "zrange", + "key": "foo", + "start": "0", + "end": "-1", + "withscores": "withscores", + }, + ) + + +@pytest.mark.xfail(reason="Not implemented yet") +def test_zinterstore(judge_command): + judge_command("ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3", {}) + judge_command("ZINTERSTORE out 2 zset1 zset2 WEIGHTS -1 -2", {}) + judge_command("ZINTERSTORE out 2 zset1 zset2 WEIGHTS 0.2 0.3", {}) + + +def test_zrangebylex(judge_command): + judge_command( + "ZRANGEBYLEX myzset [aaa (g", + {"command": "ZRANGEBYLEX", "key": "myzset", "lexmin": "[aaa", "lexmax": "(g"}, + ) + judge_command( + "ZRANGEBYLEX myzset - (c", + {"command": "ZRANGEBYLEX", "key": "myzset", "lexmin": "-", "lexmax": "(c"}, + ) + judge_command( + "ZRANGEBYLEX myzset - (c limit 10 100", + { + "command": "ZRANGEBYLEX", + "key": "myzset", + "lexmin": "-", + "lexmax": "(c", + "limit": "limit", + "offset": "10", + "count": "100", + }, + ) + judge_command( + "ZRANGEBYLEX myzset - (c limit 10 -1", + { + "command": "ZRANGEBYLEX", + "key": "myzset", + "lexmin": "-", + "lexmax": "(c", + "limit": "limit", + "offset": "10", + "count": "-1", + }, + ) + + +def test_zrangebyscore(judge_command): + judge_command( + "ZRANGEBYSCORE myzset -inf +inf", + {"command": "ZRANGEBYSCORE", "key": "myzset", "min": "-inf", "max": "+inf"}, + ) + judge_command( + "ZRANGEBYSCORE myzset 1 2", + {"command": "ZRANGEBYSCORE", "key": "myzset", "min": "1", "max": "2"}, + ) + judge_command( + "ZRANGEBYSCORE myzset (1 (2", + {"command": "ZRANGEBYSCORE", "key": "myzset", "min": "(1", "max": "(2"}, + ) + judge_command( + "ZRANGEBYSCORE myzset -inf +inf LIMIT 10 100", + { + "command": "ZRANGEBYSCORE", + "key": "myzset", + "min": "-inf", + "max": "+inf", + "limit": "LIMIT", + "offset": "10", + "count": "100", + }, + ) diff --git a/tests/unittests/command_parse/test_stream.py b/tests/unittests/command_parse/test_stream.py new file mode 100644 index 0000000..5807aa2 --- /dev/null +++ b/tests/unittests/command_parse/test_stream.py @@ -0,0 +1,459 @@ +def test_xrange(judge_command): + judge_command( + "XRANGE somestream - +", + {"command": "XRANGE", "key": "somestream", "stream_id": ["-", "+"]}, + ) + judge_command( + "XRANGE somestream 1526985054069 1526985055069", + { + "command": "XRANGE", + "key": "somestream", + "stream_id": ["1526985054069", "1526985055069"], + }, + ) + judge_command( + "XRANGE somestream 1526985054069 1526985055069-10", + { + "command": "XRANGE", + "key": "somestream", + "stream_id": ["1526985054069", "1526985055069-10"], + }, + ) + judge_command( + "XRANGE somestream 1526985054069 1526985055069-10 count 10", + { + "command": "XRANGE", + "key": "somestream", + "stream_id": ["1526985054069", "1526985055069-10"], + "count_const": "count", + "count": "10", + }, + ) + + +def test_xgroup_create(judge_command): + judge_command( + "XGROUP CREATE mykey mygroup 123", + { + "command": "XGROUP", + "stream_create": "CREATE", + "key": "mykey", + "group": "mygroup", + "stream_id": "123", + }, + ) + judge_command( + "XGROUP CREATE mykey mygroup $", + { + "command": "XGROUP", + "stream_create": "CREATE", + "key": "mykey", + "group": "mygroup", + "stream_id": "$", + }, + ) + # short of a parameter + judge_command("XGROUP CREATE mykey mygroup", None) + judge_command("XGROUP CREATE mykey", None) + + +def test_xgroup_setid(judge_command): + judge_command( + "XGROUP SETID mykey mygroup 123", + { + "command": "XGROUP", + "stream_setid": "SETID", + "key": "mykey", + "group": "mygroup", + "stream_id": "123", + }, + ) + judge_command( + "XGROUP SETID mykey mygroup $", + { + "command": "XGROUP", + "stream_setid": "SETID", + "key": "mykey", + "group": "mygroup", + "stream_id": "$", + }, + ) + # two subcommand together shouldn't match + judge_command("XGROUP CREATE mykey mygroup 123 SETID mykey mygroup $", None) + + +def test_xgroup_destroy(judge_command): + judge_command( + "XGROUP destroy mykey mygroup", + { + "command": "XGROUP", + "stream_destroy": "destroy", + "key": "mykey", + "group": "mygroup", + }, + ) + judge_command("XGROUP destroy mykey", None) + judge_command("XGROUP DESTROY mykey mygroup $", None) + + +def test_xgroup_delconsumer(judge_command): + judge_command( + "XGROUP delconsumer mykey mygroup myconsumer", + { + "command": "XGROUP", + "stream_delconsumer": "delconsumer", + "key": "mykey", + "group": "mygroup", + "consumer": "myconsumer", + }, + ) + judge_command( + "XGROUP delconsumer mykey mygroup $", + { + "command": "XGROUP", + "stream_delconsumer": "delconsumer", + "key": "mykey", + "group": "mygroup", + "consumer": "$", + }, + ) + judge_command("XGROUP delconsumer mykey mygroup", None) + + +def test_xgroup_stream(judge_command): + judge_command( + "XACK mystream group1 123123", + { + "command": "XACK", + "key": "mystream", + "group": "group1", + "stream_id": "123123", + }, + ) + judge_command( + "XACK mystream group1 123123 111", + {"command": "XACK", "key": "mystream", "group": "group1", "stream_id": "111"}, + ) + + +def test_xinfo(judge_command): + judge_command( + "XINFO consumers mystream mygroup", + { + "command": "XINFO", + "stream_consumers": "consumers", + "key": "mystream", + "group": "mygroup", + }, + ) + judge_command( + "XINFO GROUPS mystream", + {"command": "XINFO", "stream_groups": "GROUPS", "key": "mystream"}, + ) + judge_command( + "XINFO STREAM mystream", + {"command": "XINFO", "stream": "STREAM", "key": "mystream"}, + ) + judge_command("XINFO HELP", {"command": "XINFO", "help": "HELP"}) + judge_command("XINFO consumers mystream mygroup GROUPS mystream", None) + judge_command("XINFO groups mystream mygroup", None) + + +def test_xinfo_with_full(judge_command): + judge_command( + "XINFO STREAM mystream FULL", + { + "command": "XINFO", + "stream": "STREAM", + "key": "mystream", + "full_const": "FULL", + }, + ) + judge_command( + "XINFO STREAM mystream FULL count 10", + { + "command": "XINFO", + "stream": "STREAM", + "key": "mystream", + "full_const": "FULL", + "count_const": "count", + "count": "10", + }, + ) + + +def test_xpending(judge_command): + judge_command( + "XPENDING mystream group55", + {"command": "XPENDING", "key": "mystream", "group": "group55"}, + ) + judge_command( + "XPENDING mystream group55 myconsumer", + { + "command": "XPENDING", + "key": "mystream", + "group": "group55", + "consumer": "myconsumer", + }, + ) + judge_command( + "XPENDING mystream group55 - + 10", + { + "command": "XPENDING", + "key": "mystream", + "group": "group55", + "stream_id": ["-", "+"], + "count": "10", + }, + ) + judge_command( + "XPENDING mystream group55 - + 10 myconsumer", + { + "command": "XPENDING", + "key": "mystream", + "group": "group55", + "stream_id": ["-", "+"], + "count": "10", + "consumer": "myconsumer", + }, + ) + judge_command("XPENDING mystream group55 - + ", None) + + +def test_xadd(judge_command): + judge_command( + "xadd mystream MAXLEN ~ 1000 * key value", + { + "command": "xadd", + "key": "mystream", + "maxlen": "MAXLEN", + "approximately": "~", + "count": "1000", + "sfield": "key", + "svalue": "value", + "stream_id": "*", + }, + ) + # test for MAXLEN option + judge_command( + "xadd mystream MAXLEN 1000 * key value", + { + "command": "xadd", + "key": "mystream", + "maxlen": "MAXLEN", + "count": "1000", + "sfield": "key", + "svalue": "value", + "stream_id": "*", + }, + ) + judge_command( + "xadd mystream * key value", + { + "command": "xadd", + "key": "mystream", + "sfield": "key", + "svalue": "value", + "stream_id": "*", + }, + ) + # spcify stream id + judge_command( + "xadd mystream 123-123 key value", + { + "command": "xadd", + "key": "mystream", + "sfield": "key", + "svalue": "value", + "stream_id": "123-123", + }, + ) + judge_command( + "xadd mystream 123-123 key value foo bar hello world", + { + "command": "xadd", + "key": "mystream", + "sfield": "hello", + "svalue": "world", + "stream_id": "123-123", + }, + ) + + +def test_xtrim(judge_command): + judge_command( + " XTRIM mystream MAXLEN 2", + {"command": "XTRIM", "key": "mystream", "maxlen": "MAXLEN", "count": "2"}, + ) + judge_command( + " XTRIM mystream MAXLEN ~ 2", + { + "command": "XTRIM", + "key": "mystream", + "maxlen": "MAXLEN", + "count": "2", + "approximately": "~", + }, + ) + judge_command(" XTRIM mystream", None) + + +def test_xdel(judge_command): + judge_command( + "XDEL mystream 1581165000000 1549611229000 1581060831000", + {"command": "XDEL", "key": "mystream", "stream_id": "1581060831000"}, + ) + judge_command( + "XDEL mystream 1581165000000", + {"command": "XDEL", "key": "mystream", "stream_id": "1581165000000"}, + ) + + +def test_xclaim(judge_command): + judge_command( + "XCLAIM mystream mygroup Alice 3600000 1526569498055-0", + { + "command": "XCLAIM", + "key": "mystream", + "group": "mygroup", + "consumer": "Alice", + "millisecond": "3600000", + "stream_id": "1526569498055-0", + }, + ) + judge_command( + "XCLAIM mystream mygroup Alice 3600000 1526569498055-0 123 456 789", + { + "command": "XCLAIM", + "key": "mystream", + "group": "mygroup", + "consumer": "Alice", + "millisecond": "3600000", + "stream_id": "789", + }, + ) + judge_command( + "XCLAIM mystream mygroup Alice 3600000 1526569498055-0 IDEL 300", + { + "command": "XCLAIM", + "key": "mystream", + "group": "mygroup", + "consumer": "Alice", + "millisecond": ["3600000", "300"], + "stream_id": "1526569498055-0", + "idel": "IDEL", + }, + ) + judge_command( + "XCLAIM mystream mygroup Alice 3600000 1526569498055-0 retrycount 7", + { + "command": "XCLAIM", + "key": "mystream", + "group": "mygroup", + "consumer": "Alice", + "millisecond": "3600000", + "stream_id": "1526569498055-0", + "retrycount": "retrycount", + "count": "7", + }, + ) + judge_command( + "XCLAIM mystream mygroup Alice 3600000 1526569498055-0 TIME 123456789", + { + "command": "XCLAIM", + "key": "mystream", + "group": "mygroup", + "consumer": "Alice", + "millisecond": "3600000", + "stream_id": "1526569498055-0", + "time": "TIME", + "timestamp": "123456789", + }, + ) + judge_command( + "XCLAIM mystream mygroup Alice 3600000 1526569498055-0 FORCE", + { + "command": "XCLAIM", + "key": "mystream", + "group": "mygroup", + "consumer": "Alice", + "millisecond": "3600000", + "stream_id": "1526569498055-0", + "force": "FORCE", + }, + ) + judge_command( + "XCLAIM mystream mygroup Alice 3600000 1526569498055-0 JUSTID", + { + "command": "XCLAIM", + "key": "mystream", + "group": "mygroup", + "consumer": "Alice", + "millisecond": "3600000", + "stream_id": "1526569498055-0", + "justid": "JUSTID", + }, + ) + + +def test_xread(judge_command): + judge_command( + "XREAD COUNT 2 STREAMS mystream writers 0-0 0-0", + { + "command": "XREAD", + "count_const": "COUNT", + "count": "2", + "streams": "STREAMS", + # FIXME current grammar can't support multiple tokens + # so the ids will be recongized to keys. + "keys": "mystream writers 0-0", + "stream_id": "0-0", + }, + ) + judge_command( + "XREAD COUNT 2 BLOCK 1000 STREAMS mystream writers 0-0 0-0", + { + "command": "XREAD", + "count_const": "COUNT", + "count": "2", + "streams": "STREAMS", + "keys": "mystream writers 0-0", + "block": "BLOCK", + "millisecond": "1000", + "stream_id": "0-0", + }, + ) + + +def test_xreadgroup(judge_command): + judge_command( + "XREADGROUP GROUP mygroup1 Bob COUNT 1 BLOCK 100 NOACK STREAMS key1 1 key2 2", + { + "command": "XREADGROUP", + "stream_group": "GROUP", + "group": "mygroup1", + "consumer": "Bob", + "count_const": "COUNT", + "count": "1", + "block": "BLOCK", + "millisecond": "100", + "noack": "NOACK", + "streams": "STREAMS", + "keys": "key1 1 key2", + "stream_id": "2", + }, + ) + judge_command( + "XREADGROUP GROUP mygroup1 Bob STREAMS key1 1 key2 2", + { + "command": "XREADGROUP", + "stream_group": "GROUP", + "group": "mygroup1", + "consumer": "Bob", + "streams": "STREAMS", + "keys": "key1 1 key2", + "stream_id": "2", + }, + ) + + judge_command("XREADGROUP GROUP group consumer", None) diff --git a/tests/unittests/command_parse/test_string_parse.py b/tests/unittests/command_parse/test_string_parse.py new file mode 100644 index 0000000..df56b25 --- /dev/null +++ b/tests/unittests/command_parse/test_string_parse.py @@ -0,0 +1,345 @@ +def test_set(judge_command): + judge_command("SET abc bar", {"command": "SET", "key": "abc", "value": "bar"}) + judge_command( + "SET abc bar EX 10", + { + "command": "SET", + "key": "abc", + "value": "bar", + "expiration": "EX", + "millisecond": "10", + }, + ) + judge_command( + "SET abc bar px 10000", + { + "command": "SET", + "key": "abc", + "value": "bar", + "expiration": "px", + "millisecond": "10000", + }, + ) + judge_command( + "SET abc bar px 10000 nx", + { + "command": "SET", + "key": "abc", + "value": "bar", + "expiration": "px", + "millisecond": "10000", + "condition": "nx", + }, + ) + judge_command( + "SET abc bar px 10000 XX", + { + "command": "SET", + "key": "abc", + "value": "bar", + "expiration": "px", + "millisecond": "10000", + "condition": "XX", + }, + ) + judge_command( + "SET abc bar XX px 10000", + { + "command": "SET", + "key": "abc", + "value": "bar", + "expiration": "px", + "millisecond": "10000", + "condition": "XX", + }, + ) + judge_command( + "SET abc bar XX", + {"command": "SET", "key": "abc", "value": "bar", "condition": "XX"}, + ) + # keepttl + judge_command( + "SET abc bar XX keepttl", + { + "command": "SET", + "key": "abc", + "value": "bar", + "condition": "XX", + "keepttl": "keepttl", + }, + ) + judge_command( + "SET abc bar keepttl XX", + { + "command": "SET", + "key": "abc", + "value": "bar", + "condition": "XX", + "keepttl": "keepttl", + }, + ) + judge_command( + "SET abc bar XX px 10000 KEEPTTL", + { + "command": "SET", + "key": "abc", + "value": "bar", + "expiration": "px", + "millisecond": "10000", + "condition": "XX", + "keepttl": "KEEPTTL", + }, + ) + + +def test_append(judge_command): + judge_command("append foo bar", {"command": "append", "key": "foo", "value": "bar"}) + judge_command( + "APPEND foo 'bar'", {"command": "APPEND", "key": "foo", "value": "'bar'"} + ) + judge_command("APPEND foo", None) + + +def test_bitcount(judge_command): + judge_command("bitcount foo", {"command": "bitcount", "key": "foo"}) + judge_command( + "bitcount foo 1 5", + {"command": "bitcount", "key": "foo", "start": "1", "end": "5"}, + ) + judge_command( + "bitcount foo 1 -5", + {"command": "bitcount", "key": "foo", "start": "1", "end": "-5"}, + ) + judge_command( + "bitcount foo -2 -1", + {"command": "bitcount", "key": "foo", "start": "-2", "end": "-1"}, + ) + judge_command("bitcount foo -2", None) + + +def test_getrange(judge_command): + judge_command("getrange foo", None) + judge_command( + "getrange foo 1 5", + {"command": "getrange", "key": "foo", "start": "1", "end": "5"}, + ) + judge_command( + "getrange foo 1 -5", + {"command": "getrange", "key": "foo", "start": "1", "end": "-5"}, + ) + judge_command( + "getrange foo -2 -1", + {"command": "getrange", "key": "foo", "start": "-2", "end": "-1"}, + ) + judge_command("getrange foo -2", None) + + +def test_get_set(judge_command): + judge_command("GETSET abc bar", {"command": "GETSET", "key": "abc", "value": "bar"}) + + +def test_incr(judge_command): + judge_command("INCR foo", {"command": "INCR", "key": "foo"}) + judge_command("INCR", None) + judge_command("INCR foo 1", None) + + +def test_incr_by(judge_command): + judge_command("INCRBY foo", None) + judge_command("INCRBY", None) + judge_command("INCRBY foo 1", {"command": "INCRBY", "key": "foo", "delta": "1"}) + judge_command("INCRBY foo 200", {"command": "INCRBY", "key": "foo", "delta": "200"}) + judge_command("INCRBY foo -21", {"command": "INCRBY", "key": "foo", "delta": "-21"}) + + +def test_decr(judge_command): + judge_command("DECR foo", {"command": "DECR", "key": "foo"}) + judge_command("DECR", None) + judge_command("DECR foo 1", None) + + +def test_decr_by(judge_command): + judge_command("DECRBY foo", None) + judge_command("DECRBY", None) + judge_command("DECRBY foo 1", {"command": "DECRBY", "key": "foo", "delta": "1"}) + judge_command("DECRBY foo 200", {"command": "DECRBY", "key": "foo", "delta": "200"}) + judge_command("DECRBY foo -21", {"command": "DECRBY", "key": "foo", "delta": "-21"}) + + +def test_command_set_range(judge_command): + judge_command( + "SETRANGE foo 10 bar", + {"command": "SETRANGE", "key": "foo", "offset": "10", "value": "bar"}, + ) + judge_command("SETRANGE foo bar", None) + judge_command( + "SETRANGE Redis 10 'hello world'", + { + "command": "SETRANGE", + "key": "Redis", + "offset": "10", + "value": "'hello world'", + }, + ) + + +def test_command_set_ex(judge_command): + judge_command( + "SETEX key 10 value", + {"command": "SETEX", "key": "key", "second": "10", "value": "value"}, + ) + judge_command("SETEX foo 10", None) + judge_command( + "setex Redis 10 'hello world'", + {"command": "setex", "key": "Redis", "second": "10", "value": "'hello world'"}, + ) + + +def test_command_setbit(judge_command): + judge_command( + "SETBIT key 10 0", + {"command": "SETBIT", "key": "key", "offset": "10", "bit": "0"}, + ) + judge_command( + "SETBIT foo 10 1", + {"command": "SETBIT", "key": "foo", "offset": "10", "bit": "1"}, + ) + judge_command("SETBIT foo 10 10", None) + judge_command("SETBIT foo 10 abc", None) + judge_command("SETBIT foo 10", None) + judge_command("SETBIT foo", None) + + +def test_command_getbit(judge_command): + judge_command("GETBIT key 10", {"command": "GETBIT", "key": "key", "offset": "10"}) + judge_command("GETBIT foo 0", {"command": "GETBIT", "key": "foo", "offset": "0"}) + judge_command("GETBIT foo -1", None) + judge_command("SETBIT foo abc", None) + judge_command("SETBIT foo", None) + + +def test_command_incrbyfloat(judge_command): + judge_command("INCRBYFLOAT key", None) + judge_command( + "INCRBYFLOAT key 1.1", {"command": "INCRBYFLOAT", "key": "key", "float": "1.1"} + ) + judge_command( + "INCRBYFLOAT key .1", {"command": "INCRBYFLOAT", "key": "key", "float": ".1"} + ) + judge_command( + "INCRBYFLOAT key 1.", {"command": "INCRBYFLOAT", "key": "key", "float": "1."} + ) + judge_command( + "INCRBYFLOAT key 5.0e3", + {"command": "INCRBYFLOAT", "key": "key", "float": "5.0e3"}, + ) + judge_command( + "INCRBYFLOAT key -5.0e3", + {"command": "INCRBYFLOAT", "key": "key", "float": "-5.0e3"}, + ) + + +def test_command_mget(judge_command): + judge_command("mget foo bar", {"command": "mget", "keys": "foo bar"}) + + +def test_mset(judge_command): + judge_command("mset foo bar", {"command": "mset", "key": "foo", "value": "bar"}) + judge_command( + "mset foo bar hello world", + {"command": "mset", "key": "hello", "value": "world"}, + ) + + +def test_psetex(judge_command): + judge_command( + "psetex foo 1000 bar", + {"command": "psetex", "key": "foo", "value": "bar", "millisecond": "1000"}, + ) + judge_command("psetex foo bar", None) + + +def test_bitop(judge_command): + judge_command( + "BITOP AND dest key1 key2", + {"command": "BITOP", "operation": "AND", "key": "dest", "keys": "key1 key2"}, + ) + judge_command( + "BITOP AND dest key1", + {"command": "BITOP", "operation": "AND", "key": "dest", "keys": "key1"}, + ) + judge_command("BITOP AND dest", None) + + +def test_bitpos(judge_command): + judge_command( + "BITPOS mykey 1 3 5", + {"command": "BITPOS", "key": "mykey", "bit": "1", "start": "3", "end": "5"}, + ) + judge_command("BITPOS mykey 1", {"command": "BITPOS", "key": "mykey", "bit": "1"}) + judge_command( + "BITPOS mykey 1 3", + {"command": "BITPOS", "key": "mykey", "bit": "1", "start": "3"}, + ) + + +def test_bitfield(judge_command): + judge_command( + "BITFIELD mykey INCRBY i5 100 1 GET u4 0", + { + "command": "BITFIELD", + "key": "mykey", + "incrby": "INCRBY", + "inttype": ["i5", "u4"], + "offset": ["100", "0"], + "value": "1", + "get": "GET", + }, + ) + judge_command( + "BITFIELD mystring SET i8 #0 100", + { + "command": "BITFIELD", + "key": "mystring", + "set": "SET", + "inttype": "i8", + "offset": "#0", + "value": "100", + }, + ) + judge_command( + "BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1", + { + "command": "BITFIELD", + "key": "mykey", + "incrby": "incrby", + "inttype": "u2", + "offset": "102", + "value": "1", + "overflow": "OVERFLOW", + "overflow_option": "SAT", + }, + ) + + +def test_stralgo(judge_command): + judge_command( + "STRALGO LCS STRINGS ohmytext mynewtext", + { + "command": "STRALGO", + "str_algo": "LCS", + "strings_const": "STRINGS", + "values": "ohmytext mynewtext", + }, + ) + + # Due to redis' command design, this can't be fix in any ways. + judge_command( + "STRALGO LCS STRINGS ohmytext mynewtext LEN", + { + "command": "STRALGO", + "str_algo": "LCS", + "strings_const": "STRINGS", + "values": "ohmytext mynewtext LEN", + }, + ) diff --git a/tests/unittests/test_client.py b/tests/unittests/test_client.py new file mode 100644 index 0000000..e78da17 --- /dev/null +++ b/tests/unittests/test_client.py @@ -0,0 +1,504 @@ +import re +import pytest +import redis +from unittest.mock import MagicMock, patch +from textwrap import dedent + +from prompt_toolkit.formatted_text import FormattedText + +from iredis.client import Client +from iredis.config import config, load_config_files +from iredis.completers import IRedisCompleter +from iredis.entry import Rainbow, prompt_message +from iredis.exceptions import NotSupport + + +@pytest.fixture +def completer(): + return IRedisCompleter() + + +@pytest.mark.parametrize( + "_input, command_name, expect_args", + [ + ("keys *", "keys", ["*"]), + ("DEL abc foo bar", "DEL", ["abc", "foo", "bar"]), + ("cluster info", "cluster info", []), + ("CLUSTER failover FORCE", "CLUSTER failover", ["FORCE"]), + ], +) +def test_send_command(_input, command_name, expect_args): + client = Client("127.0.0.1", "6379", None) + client.execute = MagicMock() + next(client.send_command(_input, None)) + args, kwargs = client.execute.call_args + assert args == (command_name, *expect_args) + + +def test_client_not_support_hello_command(iredis_client): + with pytest.raises(NotSupport): + iredis_client.pre_hook("hello 3", "hello", "3", None) + + +def test_patch_completer(): + client = Client("127.0.0.1", "6379", None) + completer = IRedisCompleter() + client.pre_hook( + "MGET foo bar hello world", "MGET", "foo bar hello world", completer + ) + assert completer.key_completer.words == ["world", "hello", "bar", "foo"] + assert completer.key_completer.words == ["world", "hello", "bar", "foo"] + + client.pre_hook("GET bar", "GET", "bar", completer) + assert completer.key_completer.words == ["bar", "world", "hello", "foo"] + + +def test_get_server_verison_after_client(config): + Client("127.0.0.1", "6379", None) + assert re.match(r"\d+\..*", config.version) + + config.version = "Unknown" + config.no_info = True + Client("127.0.0.1", "6379", None) + assert config.version == "Unknown" + + +def test_do_help(config): + client = Client("127.0.0.1", "6379", None) + config.version = "5.0.0" + resp = client.do_help("SET") + assert resp[10] == ("", "1.0.0 (Avaiable on your redis-server: 5.0.0)") + config.version = "2.0.0" + resp = client.do_help("cluster", "addslots") + assert resp[10] == ("", "3.0.0 (Not avaiable on your redis-server: 2.0.0)") + + +def test_rainbow_iterator(): + "test color infinite iterator" + original_color = Rainbow.color + Rainbow.color = list(range(0, 3)) + assert list(zip(range(10), Rainbow())) == [ + (0, 0), + (1, 1), + (2, 2), + (3, 1), + (4, 0), + (5, 1), + (6, 2), + (7, 1), + (8, 0), + (9, 1), + ] + Rainbow.color = original_color + + +def test_prompt_message(iredis_client, config): + config.rainbow = False + assert prompt_message(iredis_client) == "127.0.0.1:6379[15]> " + + config.rainbow = True + assert prompt_message(iredis_client)[:3] == [ + ("#cc2244", "1"), + ("#bb4444", "2"), + ("#996644", "7"), + ] + + +def test_on_connection_error_retry(iredis_client, config): + config.retry_times = 1 + mock_connection = MagicMock() + mock_connection.read_response.side_effect = [ + redis.exceptions.ConnectionError( + "Error 61 connecting to 127.0.0.1:7788. Connection refused." + ), + "hello", + ] + original_connection = iredis_client.connection + iredis_client.connection = mock_connection + value = iredis_client.execute("None", "GET", ["foo"]) + assert value == "hello" # be rendered + + mock_connection.disconnect.assert_called_once() + mock_connection.connect.assert_called_once() + + iredis_client.connection = original_connection + + +def test_on_connection_error_retry_without_retrytimes(iredis_client, config): + config.retry_times = 0 + mock_connection = MagicMock() + mock_connection.read_response.side_effect = [ + redis.exceptions.ConnectionError( + "Error 61 connecting to 127.0.0.1:7788. Connection refused." + ), + "hello", + ] + iredis_client.connection = mock_connection + with pytest.raises(redis.exceptions.ConnectionError): + iredis_client.execute("None", "GET", ["foo"]) + + mock_connection.disconnect.assert_not_called() + mock_connection.connect.assert_not_called() + + +def test_socket_keepalive(config): + config.socket_keepalive = True + from iredis.client import Client + + newclient = Client("127.0.0.1", "6379", 0) + assert newclient.connection.socket_keepalive + + # keepalive off + config.socket_keepalive = False + + newclient = Client("127.0.0.1", "6379", 0) + assert not newclient.connection.socket_keepalive + + +def test_not_retry_on_authentication_error(iredis_client, config): + config.retry_times = 2 + mock_connection = MagicMock() + mock_connection.read_response.side_effect = [ + redis.exceptions.AuthenticationError("Authentication required."), + "hello", + ] + iredis_client.connection = mock_connection + with pytest.raises(redis.exceptions.ConnectionError): + iredis_client.execute("None", "GET", ["foo"]) + + +@pytest.mark.skipif("int(os.environ['REDIS_VERSION']) < 6") +def test_auto_select_db_and_auth_for_reconnect_only_6(iredis_client, config): + config.retry_times = 2 + config.raw = True + next(iredis_client.send_command("select 2")) + assert iredis_client.connection.db == 2 + + resp = next(iredis_client.send_command("auth 123")) + + assert ( + b"ERROR AUTH <password> called without any " + b"password configured for the default user. " + b"Are you sure your configuration is correct?" in resp + ) + assert iredis_client.connection.password is None + + next(iredis_client.send_command("config set requirepass 'abc'")) + next(iredis_client.send_command("auth abc")) + assert iredis_client.connection.password == "abc" + assert ( + iredis_client.execute("ACL SETUSER", "default", "on", "nopass", "~*", "+@all") + == b"OK" + ) + + +@pytest.mark.skipif("int(os.environ['REDIS_VERSION']) > 5") +def test_auto_select_db_and_auth_for_reconnect_only_5(iredis_client, config): + config.retry_times = 2 + config.raw = True + next(iredis_client.send_command("select 2")) + assert iredis_client.connection.db == 2 + + resp = next(iredis_client.send_command("auth 123")) + + assert b"Client sent AUTH, but no password is set" in resp + assert iredis_client.connection.password is None + + next(iredis_client.send_command("config set requirepass 'abc'")) + next(iredis_client.send_command("auth abc")) + assert iredis_client.connection.password == "abc" + next(iredis_client.send_command("config set requirepass ''")) + + +def test_split_shell_command(iredis_client, completer): + assert iredis_client.split_command_and_pipeline(" get json | rg . ", completer) == ( + " get json ", + "rg . ", + ) + + assert iredis_client.split_command_and_pipeline( + """ get "json | \\" hello" | rg . """, completer + ) == (""" get "json | \\" hello" """, "rg . ") + + +def test_running_with_pipeline(clean_redis, iredis_client, capfd, completer): + config.shell = True + clean_redis.set("foo", "hello \n world") + with pytest.raises(StopIteration): + next(iredis_client.send_command("get foo | grep w", completer)) + out, err = capfd.readouterr() + assert out == " world\n" + + +def test_running_with_multiple_pipeline(clean_redis, iredis_client, capfd, completer): + config.shell = True + clean_redis.set("foo", "hello world\nhello iredis") + with pytest.raises(StopIteration): + next( + iredis_client.send_command("get foo | grep hello | grep iredis", completer) + ) + out, err = capfd.readouterr() + assert out == "hello iredis\n" + + +def test_can_not_connect_on_startup(capfd): + with pytest.raises(SystemExit): + Client("localhost", "16111", 15) + out, err = capfd.readouterr() + assert "connecting to localhost:16111." in err + + +def test_peek_key_not_exist(iredis_client, clean_redis, config): + config.raw = False + peek_result = list(iredis_client.do_peek("non-exist-key")) + assert peek_result == ["non-exist-key doesn't exist."] + + +def test_peek_string(iredis_client, clean_redis): + clean_redis.set("foo", "bar") + peek_result = list(iredis_client.do_peek("foo")) + + assert peek_result == [ + FormattedText( + [ + ("class:dockey", "key: "), + ("", "string (embstr) mem: 50 bytes, ttl: -1"), + ("", "\n"), + ("class:dockey", "strlen: "), + ("", "3"), + ("", "\n"), + ("class:dockey", "value: "), + ("", '"bar"'), + ] + ) + ] + + +def test_peek_list_fetch_all(iredis_client, clean_redis): + clean_redis.lpush("mylist", *[f"hello-{index}" for index in range(5)]) + peek_result = list(iredis_client.do_peek("mylist")) + + assert peek_result == [ + FormattedText( + [ + ("class:dockey", "key: "), + ("", "list (quicklist) mem: 176 bytes, ttl: -1"), + ("", "\n"), + ("class:dockey", "llen: "), + ("", "5"), + ("", "\n"), + ("class:dockey", "elements: "), + ("", "\n"), + ("", "1)"), + ("", " "), + ("class:string", '"hello-4"'), + ("", "\n"), + ("", "2)"), + ("", " "), + ("class:string", '"hello-3"'), + ("", "\n"), + ("", "3)"), + ("", " "), + ("class:string", '"hello-2"'), + ("", "\n"), + ("", "4)"), + ("", " "), + ("class:string", '"hello-1"'), + ("", "\n"), + ("", "5)"), + ("", " "), + ("class:string", '"hello-0"'), + ] + ) + ] + + +def test_peek_list_fetch_part(iredis_client, clean_redis): + clean_redis.lpush("mylist", *[f"hello-{index}" for index in range(40)]) + peek_result = list(iredis_client.do_peek("mylist")) + assert len(peek_result[0]) == 91 + + +def test_peek_set_fetch_all(iredis_client, clean_redis): + clean_redis.sadd("myset", *[f"hello-{index}" for index in range(5)]) + peek_result = list(iredis_client.do_peek("myset")) + assert len(peek_result[0]) == 27 + + +def test_peek_set_fetch_part(iredis_client, clean_redis): + clean_redis.sadd("myset", *[f"hello-{index}" for index in range(40)]) + peek_result = list(iredis_client.do_peek("myset")) + + assert peek_result[0][0] == ("class:dockey", "key: ") + assert peek_result[0][1][1].startswith("set (hashtable) mem: 2") + + +def test_peek_zset_fetch_all(iredis_client, clean_redis): + clean_redis.zadd( + "myzset", dict(zip([f"hello-{index}" for index in range(3)], range(3))) + ) + peek_result = list(iredis_client.do_peek("myzset")) + assert peek_result[0][0:9] == FormattedText( + [ + ("class:dockey", "key: "), + ("", "zset (ziplist) mem: 92 bytes, ttl: -1"), + ("", "\n"), + ("class:dockey", "zcount: "), + ("", "3"), + ("", "\n"), + ("class:dockey", "members: "), + ("", "\n"), + ("", "1)"), + ] + ) + + +def test_peek_zset_fetch_part(iredis_client, clean_redis): + clean_redis.zadd( + "myzset", dict(zip([f"hello-{index}" for index in range(40)], range(40))) + ) + peek_result = list(iredis_client.do_peek("myzset")) + assert peek_result[0][0:8] == FormattedText( + [ + ("class:dockey", "key: "), + ("", "zset (ziplist) mem: 556 bytes, ttl: -1"), + ("", "\n"), + ("class:dockey", "zcount: "), + ("", "40"), + ("", "\n"), + ("class:dockey", "members (first 40): "), + ("", "\n"), + ] + ) + + +def test_peek_hash_fetch_all(iredis_client, clean_redis): + for key, value in zip( + [f"hello-{index}" for index in range(3)], [f"hi-{index}" for index in range(3)] + ): + clean_redis.hset("myhash", key, value) + peek_result = list(iredis_client.do_peek("myhash")) + assert len(peek_result[0]) == 28 + + +def test_peek_hash_fetch_part(iredis_client, clean_redis): + for key, value in zip( + [f"hello-{index}" for index in range(100)], + [f"hi-{index}" for index in range(100)], + ): + clean_redis.hset("myhash", key, value) + peek_result = list(iredis_client.do_peek("myhash")) + assert len(peek_result[0]) == 707 + + +def test_peek_stream(iredis_client, clean_redis): + clean_redis.xadd("mystream", {"foo": "bar", "hello": "world"}) + peek_result = list(iredis_client.do_peek("mystream")) + + assert peek_result[0][0:18] == FormattedText( + [ + ("class:dockey", "key: "), + ("", "stream (unknown) mem: 601 bytes, ttl: -1"), + ("", "\n"), + ("class:dockey", "XINFO: "), + ("", "\n"), + ("", " 1)"), + ("", " "), + ("class:string", '"length"'), + ("", "\n"), + ("", " 2)"), + ("", " "), + ("class:string", '"1"'), + ("", "\n"), + ("", " 3)"), + ("", " "), + ("class:string", '"radix-tree-keys"'), + ("", "\n"), + ("", " 4)"), + ] + ) + + +def test_mem_not_called_before_redis_4(config, iredis_client, clean_redis): + config.version = "3.2.9" + + def wrapper(func): + def execute(command_name, *args): + print(command_name) + if command_name.upper() == "MEMORY USAGE": + raise Exception("MEMORY USAGE not supported!") + return func(command_name, *args) + + return execute + + iredis_client.execute = wrapper(iredis_client.execute) + clean_redis.set("foo", "bar") + result = list(iredis_client.do_peek("foo")) + assert result[0][1] == ("", "string (embstr), ttl: -1") + + +def test_mem_not_called_when_cant_get_server_version( + config, iredis_client, clean_redis +): + config.version = None + + def wrapper(func): + def execute(command_name, *args): + print(command_name) + if command_name.upper() == "MEMORY USAGE": + raise Exception("MEMORY USAGE not supported!") + return func(command_name, *args) + + return execute + + iredis_client.execute = wrapper(iredis_client.execute) + clean_redis.set("foo", "bar") + result = list(iredis_client.do_peek("foo")) + assert result[0][1] == ("", "string (embstr), ttl: -1") + + +def test_reissue_command_on_redis_cluster(iredis_client, clean_redis): + mock_response = iredis_client.connection = MagicMock() + mock_response.read_response.side_effect = redis.exceptions.ResponseError( + "MOVED 12182 127.0.0.1:7002" + ) + iredis_client.reissue_with_redirect = MagicMock() + iredis_client.execute("set", "foo", "bar") + assert iredis_client.reissue_with_redirect.call_args == ( + ( + "MOVED 12182 127.0.0.1:7002", + "set", + "foo", + "bar", + ), + ) + + +def test_reissue_command_on_redis_cluster_with_password_in_dsn( + iredis_client, clean_redis +): + config_content = dedent( + """ + [main] + log_location = /tmp/iredis1.log + no_info=True + [alias_dsn] + cluster-7003=redis://foo:bar@127.0.0.1:7003 + """ + ) + with open("/tmp/iredisrc", "w+") as etc_config: + etc_config.write(config_content) + + config_obj = load_config_files("/tmp/iredisrc") + config.alias_dsn = config_obj["alias_dsn"] + + mock_execute_by_connection = iredis_client.execute_by_connection = MagicMock() + with patch("redis.connection.Connection.connect"): + iredis_client.reissue_with_redirect( + "MOVED 12182 127.0.0.1:7003", "set", "foo", "bar" + ) + + call_args = mock_execute_by_connection.call_args[0] + print(call_args) + assert list(call_args[1:]) == ["set", "foo", "bar"] + assert call_args[0].password == "bar" diff --git a/tests/unittests/test_completers.py b/tests/unittests/test_completers.py new file mode 100644 index 0000000..d0344fd --- /dev/null +++ b/tests/unittests/test_completers.py @@ -0,0 +1,356 @@ +from unittest.mock import MagicMock, patch + +import pendulum +from prompt_toolkit.formatted_text import FormattedText +from prompt_toolkit.completion import Completion + +from iredis.completers import MostRecentlyUsedFirstWordCompleter +from iredis.completers import IRedisCompleter, TimestampCompleter, IntegerTypeCompleter + + +def test_LUF_completer_touch(): + c = MostRecentlyUsedFirstWordCompleter(3, ["one", "two"]) + c.touch("hello") + assert c.words == ["hello", "one", "two"] + + c.touch("foo") + assert c.words == ["foo", "hello", "one"] + + c.touch("hello") + assert c.words == ["hello", "foo", "one"] + + +def test_LUF_completer_touch_words(): + c = MostRecentlyUsedFirstWordCompleter(3, []) + c.touch_words(["hello", "world", "foo", "bar"]) + assert c.words == ["bar", "foo", "world"] + + c.touch_words(["one", "two"]) + assert c.words == ["two", "one", "bar"] + + +def test_newbie_mode_complete_without_meta_dict(): + fake_document = MagicMock() + fake_document.text_before_cursor = fake_document.text = "GEOR" + completer = IRedisCompleter(hint=False) + completions = list(completer.get_completions(fake_document, None)) + assert [word.text for word in completions] == ["GEORADIUS", "GEORADIUSBYMEMBER"] + assert [word.display_meta for word in completions] == [ + FormattedText([("", "")]), + FormattedText([("", "")]), + ] + + +def test_newbie_mode_complete_with_meta_dict(): + fake_document = MagicMock() + fake_document.text_before_cursor = fake_document.text = "GEOR" + completer = IRedisCompleter(hint=True) + completions = list(completer.get_completions(fake_document, None)) + + assert sorted([completion.display_meta for completion in completions]) == [ + FormattedText( + [ + ( + "", + "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member", # noqa + ) + ] + ), + FormattedText( + [ + ( + "", + "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point", # noqa + ) + ] + ), + ] + + +def test_newbie_mode_complete_with_meta_dict_command_is_lowercase(): + fake_document = MagicMock() + fake_document.text_before_cursor = fake_document.text = "geor" + completer = IRedisCompleter(hint=True) + completions = list(completer.get_completions(fake_document, None)) + + assert sorted([completion.display_meta for completion in completions]) == [ + FormattedText( + [ + ( + "", + "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member", # noqa + ) + ] + ), + FormattedText( + [ + ( + "", + "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point", # noqa + ) + ] + ), + ] + + +def test_iredis_completer_update_for_response(): + c = IRedisCompleter() + c.update_completer_for_response( + "HGETALL", + (), + [ + b"Behave", + b"misbehave", + b"Interpret", + b"misinterpret", + b"Lead", + b"mislead", + b"Trust", + b"mistrust", + ], + ) + assert c.field_completer.words == ["Trust", "Lead", "Interpret", "Behave"] + + +def test_categoryname_completer_update_for_response(): + c = IRedisCompleter() + c.update_completer_for_response( + "ACL CAT", + (), + [b"scripting", b"watch"], + ) + assert sorted(c.catetoryname_completer.words) == ["scripting", "watch"] + c.update_completer_for_response( + "ACL CAT", + ("scripting"), + [b"foo", b"bar"], + ) + assert sorted(c.catetoryname_completer.words) == ["scripting", "watch"] + + +def test_completer_when_there_are_spaces_in_command(): + c = IRedisCompleter() + c.update_completer_for_response( + "ACL cat", + (), + [b"scripting", b"watch"], + ) + assert sorted(c.catetoryname_completer.words) == ["scripting", "watch"] + + c.update_completer_for_response( + "acl \t cat", + (), + [b"hello", b"world"], + ) + assert sorted(c.catetoryname_completer.words) == [ + "hello", + "scripting", + "watch", + "world", + ] + + +def test_iredis_completer_no_exception_for_none_response(): + c = IRedisCompleter() + c.update_completer_for_response("XPENDING", None, None) + c.update_completer_for_response("KEYS", None, None) + + +def test_group_completer(): + fake_document = MagicMock() + previous_commands = ["xgroup create abc world 123", "xgroup setid abc hello 123"] + fake_document.text = fake_document.text_before_cursor = "XGROUP DESTROY key " + completer = IRedisCompleter() + for command in previous_commands: + completer.update_completer_for_input(command) + completions = list(completer.get_completions(fake_document, None)) + assert completions == [ + Completion( + text="hello", + start_position=0, + display=FormattedText([("", "hello")]), + display_meta=FormattedText([("", "")]), + ), + Completion( + text="world", + start_position=0, + display=FormattedText([("", "world")]), + display_meta=FormattedText([("", "")]), + ), + ] + + +@patch("iredis.completers.pendulum.now") +def test_timestamp_completer_humanize_time_completion(fake_now): + fake_now.return_value = pendulum.from_timestamp(1578487013) + c = TimestampCompleter() + + fake_document = MagicMock() + fake_document.text = fake_document.text_before_cursor = "30" + completions = list(c.get_completions(fake_document, None)) + + assert completions == [ + Completion( + text="1575895013000", + start_position=-2, + display=FormattedText([("", "1575895013000")]), + display_meta="30 days ago (2019-12-09 12:36:53)", + ), + Completion( + text="1578379013000", + start_position=-2, + display=FormattedText([("", "1578379013000")]), + display_meta="30 hours ago (2020-01-07 06:36:53)", + ), + Completion( + text="1578485213000", + start_position=-2, + display=FormattedText([("", "1578485213000")]), + display_meta="30 minutes ago (2020-01-08 12:06:53)", + ), + Completion( + text="1578486983000", + start_position=-2, + display=FormattedText([("", "1578486983000")]), + display_meta="30 seconds ago (2020-01-08 12:36:23)", + ), + ] + + # No plural + fake_document.text = fake_document.text_before_cursor = "1" + completions = list(c.get_completions(fake_document, None)) + + assert completions == [ + Completion( + text="1546951013000", + start_position=-1, + display=FormattedText([("", "1546951013000")]), + display_meta="1 year ago (2019-01-08 12:36:53)", + ), + Completion( + text="1575808613000", + start_position=-1, + display=FormattedText([("", "1575808613000")]), + display_meta="1 month ago (2019-12-08 12:36:53)", + ), + Completion( + text="1578400613000", + start_position=-1, + display=FormattedText([("", "1578400613000")]), + display_meta="1 day ago (2020-01-07 12:36:53)", + ), + Completion( + text="1578483413000", + start_position=-1, + display=FormattedText([("", "1578483413000")]), + display_meta="1 hour ago (2020-01-08 11:36:53)", + ), + Completion( + text="1578486953000", + start_position=-1, + display=FormattedText([("", "1578486953000")]), + display_meta="1 minute ago (2020-01-08 12:35:53)", + ), + Completion( + text="1578487012000", + start_position=-1, + display=FormattedText([("", "1578487012000")]), + display_meta="1 second ago (2020-01-08 12:36:52)", + ), + ] + + +def test_timestamp_completer_datetime_format_time_completion(): + c = TimestampCompleter() + fake_document = MagicMock() + fake_document.text = fake_document.text_before_cursor = "2020-02-07" + completions = list(c.get_completions(fake_document, None)) + + assert completions == [ + Completion( + text="1581033600000", + start_position=-10, + display=FormattedText([("", "1581033600000")]), + display_meta="2020-02-07T00:00:00+00:00", + ) + ] + + +def test_integer_type_completer(): + c = IntegerTypeCompleter() + fake_document = MagicMock() + fake_document.text = fake_document.get_word_before_cursor.return_value = "i" + completions = list(c.get_completions(fake_document, None)) + assert len(completions) == 64 + + fake_document.text = fake_document.get_word_before_cursor.return_value = "u" + completions = list(c.get_completions(fake_document, None)) + assert len(completions) == 63 + + c.touch("u4") + assert list(c.get_completions(fake_document, None))[0].text == "u4" + + +def test_completion_casing(): + c = IRedisCompleter(completion_casing="auto") + fake_document = MagicMock() + fake_document.text = fake_document.text_before_cursor = "ge" + assert [ + completion.text for completion in c.get_completions(fake_document, None) + ] == [ + "get", + "getset", + "getbit", + "geopos", + "geoadd", + "geohash", + "geodist", + "getrange", + "georadius", + "georadiusbymember", + ] + + c = IRedisCompleter(completion_casing="auto") + fake_document.text = fake_document.text_before_cursor = "GET" + assert [ + completion.text for completion in c.get_completions(fake_document, None) + ] == ["GET", "GETSET", "GETBIT", "GETRANGE"] + + c = IRedisCompleter(completion_casing="upper") + fake_document.text = fake_document.text_before_cursor = "get" + assert [ + completion.text for completion in c.get_completions(fake_document, None) + ] == ["GET", "GETSET", "GETBIT", "GETRANGE"] + + c = IRedisCompleter(completion_casing="lower") + fake_document.text = fake_document.text_before_cursor = "GET" + assert [ + completion.text for completion in c.get_completions(fake_document, None) + ] == ["get", "getset", "getbit", "getrange"] + + +def test_username_completer(): + completer = IRedisCompleter() + completer.update_completer_for_input("acl deluser laixintao") + completer.update_completer_for_input("acl deluser antirez") + + fake_document = MagicMock() + fake_document.text_before_cursor = fake_document.text = "acl deluser " + completions = list(completer.get_completions(fake_document, None)) + assert sorted([completion.text for completion in completions]) == [ + "antirez", + "laixintao", + ] + + +def test_username_touch_for_response(): + c = IRedisCompleter() + c.update_completer_for_response( + "acl users", + (), + [b"hello", b"world"], + ) + assert sorted(c.username_completer.words) == [ + "hello", + "world", + ] diff --git a/tests/unittests/test_entry.py b/tests/unittests/test_entry.py new file mode 100644 index 0000000..bfc7bec --- /dev/null +++ b/tests/unittests/test_entry.py @@ -0,0 +1,272 @@ +import pytest +import tempfile +from unittest.mock import patch +from prompt_toolkit.formatted_text import FormattedText + +from iredis.entry import ( + gather_args, + parse_url, + SkipAuthFileHistory, + write_result, + is_too_tall, +) + +from iredis.utils import DSN + + +@pytest.mark.parametrize( + "is_tty,raw_arg_is_raw,final_config_is_raw", + [ + (True, None, False), + (True, True, True), + (True, False, False), + (False, None, True), + (False, True, True), + (False, False, True), # not tty + ], +) +def test_command_entry_tty(is_tty, raw_arg_is_raw, final_config_is_raw, config): + # is tty + raw -> raw + with patch("sys.stdout.isatty") as patch_tty: + + patch_tty.return_value = is_tty + if raw_arg_is_raw is None: + call = ["iredis"] + elif raw_arg_is_raw is True: + call = ["iredis", "--raw"] + elif raw_arg_is_raw is False: + call = ["iredis", "--no-raw"] + else: + raise Exception() + gather_args.main(call, standalone_mode=False) + assert config.raw == final_config_is_raw + + +def test_disable_pager(): + from iredis.config import config + + gather_args.main(["iredis", "--decode", "utf-8"], standalone_mode=False) + assert config.enable_pager + + gather_args.main(["iredis", "--no-pager"], standalone_mode=False) + assert not config.enable_pager + + +def test_command_with_decode_utf_8(): + from iredis.config import config + + gather_args.main(["iredis", "--decode", "utf-8"], standalone_mode=False) + assert config.decode == "utf-8" + + gather_args.main(["iredis"], standalone_mode=False) + assert config.decode == "" + + +def test_command_with_shell_pipeline(): + from iredis.config import config + + gather_args.main(["iredis", "--no-shell"], standalone_mode=False) + assert config.shell is False + + gather_args.main(["iredis"], standalone_mode=False) + assert config.shell is True + + +def test_command_shell_options_higher_priority(): + from iredis.config import config + from textwrap import dedent + + config_content = dedent( + """ + [main] + shell = False + """ + ) + with open("/tmp/iredisrc", "w+") as etc_config: + etc_config.write(config_content) + + gather_args.main(["iredis", "--iredisrc", "/tmp/iredisrc"], standalone_mode=False) + assert config.shell is False + + gather_args.main( + ["iredis", "--shell", "--iredisrc", "/tmp/iredisrc"], standalone_mode=False + ) + assert config.shell is True + + +@pytest.mark.parametrize( + "url,dsn", + [ + ( + "redis://localhost:6379/3", + DSN( + scheme="redis", + host="localhost", + port=6379, + path=None, + db=3, + username=None, + password=None, + ), + ), + ( + "redis://localhost:6379", + DSN( + scheme="redis", + host="localhost", + port=6379, + path=None, + db=0, + username=None, + password=None, + ), + ), + ( + "rediss://localhost:6379", + DSN( + scheme="rediss", + host="localhost", + port=6379, + path=None, + db=0, + username=None, + password=None, + ), + ), + ( + "redis://username:password@localhost:6379", + DSN( + scheme="redis", + host="localhost", + port=6379, + path=None, + db=0, + username="username", + password="password", + ), + ), + ( + "redis://:password@localhost:6379", + DSN( + scheme="redis", + host="localhost", + port=6379, + path=None, + db=0, + username=None, + password="password", + ), + ), + ( + "redis://username@localhost:12345", + DSN( + scheme="redis", + host="localhost", + port=12345, + path=None, + db=0, + username="username", + password=None, + ), + ), + ( + # query string won't work for redis:// + "redis://username@localhost:6379?db=2", + DSN( + scheme="redis", + host="localhost", + port=6379, + path=None, + db=0, + username="username", + password=None, + ), + ), + ( + "unix://username:password2@/tmp/to/socket.sock?db=0", + DSN( + scheme="unix", + host=None, + port=None, + path="/tmp/to/socket.sock", + db=0, + username="username", + password="password2", + ), + ), + ( + "unix://:password3@/path/to/socket.sock", + DSN( + scheme="unix", + host=None, + port=None, + path="/path/to/socket.sock", + db=0, + username=None, + password="password3", + ), + ), + ( + "unix:///tmp/socket.sock", + DSN( + scheme="unix", + host=None, + port=None, + path="/tmp/socket.sock", + db=0, + username=None, + password=None, + ), + ), + ], +) +def test_parse_url(url, dsn): + assert parse_url(url) == dsn + + +@pytest.mark.parametrize( + "command,record", + [ + ("set foo bar", True), + ("set auth bar", True), + ("auth 123", False), + ("AUTH hello", False), + ("AUTH hello world", False), + ], +) +def test_history(command, record): + f = tempfile.TemporaryFile("w+") + history = SkipAuthFileHistory(f.name) + assert history._loaded_strings == [] + history.append_string(command) + assert (command in history._loaded_strings) is record + + +def test_write_result_for_str(capsys): + write_result("hello") + captured = capsys.readouterr() + assert captured.out == "hello\n" + + +def test_write_result_for_bytes(capsys): + write_result(b"hello") + captured = capsys.readouterr() + assert captured.out == "hello\n" + + +def test_write_result_for_formatted_text(): + ft = FormattedText([("class:keyword", "set"), ("class:string", "hello world")]) + # just this test not raise means ok + write_result(ft) + + +def test_is_too_tall_for_formatted_text(): + ft = FormattedText([("class:key", f"key-{index}\n") for index in range(21)]) + assert is_too_tall(ft, 20) + assert not is_too_tall(ft, 22) + + +def test_is_too_tall_for_bytes(): + byte_text = b"".join([b"key\n" for index in range(21)]) + assert is_too_tall(byte_text, 20) + assert not is_too_tall(byte_text, 23) diff --git a/tests/unittests/test_markdown_doc_render.py b/tests/unittests/test_markdown_doc_render.py new file mode 100644 index 0000000..0538299 --- /dev/null +++ b/tests/unittests/test_markdown_doc_render.py @@ -0,0 +1,28 @@ +""" +This test ensures that all of redis-doc's markdown can be rendered. +Why do we need this? +see: +https://github.com/antirez/redis-doc/commit/02b3d1a345093c1794fd86273e9d516fffd3b819 +""" + +import pytest +from importlib_resources import read_text + +from iredis.commands import commands_summary +from iredis.data import commands as commands_data +from iredis.markdown import render + + +doc_files = [] +for command, info in commands_summary.items(): + command_docs_name = "-".join(command.split()).lower() + if info["group"] == "iredis": + continue + doc_files.append(f"{command_docs_name}.md") + + +@pytest.mark.parametrize("filename", doc_files) +def test_markdown_render(filename): + print(filename) + doc = read_text(commands_data, filename) + render(doc) diff --git a/tests/unittests/test_render_functions.py b/tests/unittests/test_render_functions.py new file mode 100644 index 0000000..2da9b35 --- /dev/null +++ b/tests/unittests/test_render_functions.py @@ -0,0 +1,486 @@ +import os +import time +from prompt_toolkit.formatted_text import FormattedText +from iredis import renders +from iredis.config import config +from iredis.completers import IRedisCompleter + + +def strip_formatted_text(formatted_text): + return "".join(text[1] for text in formatted_text) + + +def test_render_simple_string_raw_using_raw_render(): + assert renders.OutputRender.render_raw(b"OK") == b"OK" + assert renders.OutputRender.render_raw(b"BUMPED 1") == b"BUMPED 1" + assert renders.OutputRender.render_raw(b"STILL 1") == b"STILL 1" + + +def test_render_simple_string(): + assert renders.OutputRender.render_simple_string(b"OK") == FormattedText( + [("class:success", "OK")] + ) + assert renders.OutputRender.render_simple_string(b"BUMPED 1") == FormattedText( + [("class:success", "BUMPED 1")] + ) + assert renders.OutputRender.render_simple_string(b"STILL 1") == FormattedText( + [("class:success", "STILL 1")] + ) + + +def test_render_list_index(): + raw = ["hello", "world", "foo"] + out = renders._render_list([item.encode() for item in raw], raw) + out = strip_formatted_text(out) + assert isinstance(out, str) + assert "3)" in out + assert "1)" in out + assert "4)" not in out + + +def test_render_list_index_const_width(): + raw = ["hello"] * 100 + out = renders._render_list([item.encode() for item in raw], raw) + out = strip_formatted_text(out) + assert isinstance(out, str) + assert " 1)" in out + assert "\n100)" in out + + raw = ["hello"] * 1000 + out = renders._render_list([item.encode() for item in raw], raw) + out = strip_formatted_text(out) + assert " 1)" in out + assert "\n 999)" in out + assert "\n1000)" in out + + raw = ["hello"] * 10 + out = renders._render_list([item.encode() for item in raw], raw) + out = strip_formatted_text(out) + assert " 1)" in out + assert "\n 9)" in out + assert "\n10)" in out + + +def test_render_list_using_raw_render(): + raw = ["hello", "world", "foo"] + out = renders.OutputRender.render_raw([item.encode() for item in raw]) + assert b"hello\nworld\nfoo" == out + + +def test_render_list_with_nil_init(): + raw = [b"hello", None, b"world"] + out = renders.OutputRender.render_list(raw) + out = strip_formatted_text(out) + assert out == '1) "hello"\n2) (nil)\n3) "world"' + + +def test_render_list_with_nil_init_while_config_raw(): + raw = [b"hello", None, b"world"] + out = renders.OutputRender.render_raw(raw) + assert out == b"hello\n\nworld" + + +def test_render_list_with_empty_list_raw(): + raw = [] + out = renders.OutputRender.render_raw(raw) + assert out == b"" + + +def test_render_list_with_empty_list(): + raw = [] + out = renders.OutputRender.render_list(raw) + out = strip_formatted_text(out) + assert out == "(empty list or set)" + + +def test_ensure_str_bytes(): + assert renders.ensure_str(b"hello world") == r"hello world" + assert renders.ensure_str(b"hello'world") == r"hello'world" + assert renders.ensure_str("你好".encode()) == r"\xe4\xbd\xa0\xe5\xa5\xbd" + + +def test_double_quotes(): + assert renders.double_quotes('hello"world') == r'"hello\"world"' + assert renders.double_quotes('"hello\\world"') == '"\\"hello\\world\\""' + + assert renders.double_quotes("'") == '"\'"' + assert renders.double_quotes("\\") == '"\\"' + assert renders.double_quotes('"') == '"\\""' + + +def test_render_int(): + config.raw = False + assert renders.OutputRender.render_int(12) == FormattedText( + [("class:type", "(integer) "), ("", "12")] + ) + + +def test_render_int_raw(): + assert renders.OutputRender.render_raw(12) == b"12" + + +def test_render_list_or_string(): + assert renders.OutputRender.render_list_or_string("") == '""' + assert renders.OutputRender.render_list_or_string("foo") == '"foo"' + assert renders.OutputRender.render_list_or_string( + [b"foo", b"bar"] + ) == FormattedText( + [ + ("", "1)"), + ("", " "), + ("class:string", '"foo"'), + ("", "\n"), + ("", "2)"), + ("", " "), + ("class:string", '"bar"'), + ] + ) + + +def test_render_list_or_string_nil_and_empty_list(): + assert renders.OutputRender.render_list_or_string(None) == FormattedText( + [("class:type", "(nil)")] + ) + assert renders.OutputRender.render_list_or_string([]) == FormattedText( + [("class:type", "(empty list or set)")] + ) + + +def test_render_raw_nil_and_empty_list(): + assert renders.OutputRender.render_raw(None) == b"" + assert renders.OutputRender.render_raw([]) == b"" + + +def test_list_or_string(): + config.raw = False + assert renders.OutputRender.render_string_or_int(b"10.1") == '"10.1"' + assert renders.OutputRender.render_string_or_int(3) == FormattedText( + [("class:type", "(integer) "), ("", "3")] + ) + + +def test_command_keys(): + completer = IRedisCompleter() + completer.key_completer.words = [] + config.raw = False + rendered = renders.OutputRender.command_keys([b"cat", b"dog", b"banana"]) + completer.update_completer_for_response("KEYS", None, [b"cat", b"dog", b"banana"]) + + assert rendered == FormattedText( + [ + ("", "1)"), + ("", " "), + ("class:key", '"cat"'), + ("", "\n"), + ("", "2)"), + ("", " "), + ("class:key", '"dog"'), + ("", "\n"), + ("", "3)"), + ("", " "), + ("class:key", '"banana"'), + ] + ) + assert completer.key_completer.words == ["banana", "dog", "cat"] + + +def test_command_scan(): + completer = IRedisCompleter() + completer.key_completer.words = [] + config.raw = False + rendered = renders.OutputRender.command_scan( + [b"44", [b"a", b"key:__rand_int__", b"dest", b" a"]] + ) + completer.update_completer_for_response( + "SCAN", ("0",), [b"44", [b"a", b"key:__rand_int__", b"dest", b" a"]] + ) + + assert rendered == FormattedText( + [ + ("class:type", "(cursor) "), + ("class:integer", "44"), + ("", "\n"), + ("", "1)"), + ("", " "), + ("class:key", '"a"'), + ("", "\n"), + ("", "2)"), + ("", " "), + ("class:key", '"key:__rand_int__"'), + ("", "\n"), + ("", "3)"), + ("", " "), + ("class:key", '"dest"'), + ("", "\n"), + ("", "4)"), + ("", " "), + ("class:key", '" a"'), + ] + ) + assert completer.key_completer.words == [" a", "dest", "key:__rand_int__", "a"] + + +def test_command_sscan(): + completer = IRedisCompleter() + completer.member_completer.words = [] + rendered = renders.OutputRender.command_sscan( + [b"44", [b"a", b"member:__rand_int__", b"dest", b" a"]] + ) + completer.update_completer_for_response( + "SSCAN", (0), [b"44", [b"a", b"member:__rand_int__", b"dest", b" a"]] + ) + + assert rendered == FormattedText( + [ + ("class:type", "(cursor) "), + ("class:integer", "44"), + ("", "\n"), + ("", "1)"), + ("", " "), + ("class:member", '"a"'), + ("", "\n"), + ("", "2)"), + ("", " "), + ("class:member", '"member:__rand_int__"'), + ("", "\n"), + ("", "3)"), + ("", " "), + ("class:member", '"dest"'), + ("", "\n"), + ("", "4)"), + ("", " "), + ("class:member", '" a"'), + ] + ) + assert completer.member_completer.words == [ + " a", + "dest", + "member:__rand_int__", + "a", + ] + + +def test_command_sscan_config_raw(): + completer = IRedisCompleter() + completer.member_completer.words = [] + rendered = renders.OutputRender.render_raw( + [b"44", [b"a", b"member:__rand_int__", b"dest", b" a"]] + ) + completer.update_completer_for_response( + "SSCAN", (0), [b"44", [b"a", b"member:__rand_int__", b"dest", b" a"]] + ) + + assert rendered == b"44\na\nmember:__rand_int__\ndest\n a" + assert completer.member_completer.words == [ + " a", + "dest", + "member:__rand_int__", + "a", + ] + + +def test_render_members(): + completer = IRedisCompleter() + completer.member_completer.words = [] + config.withscores = True + resp = [b"duck", b"667", b"camel", b"708"] + rendered = renders.OutputRender.render_members(resp) + completer.update_completer_for_response("ZRANGE", ("foo", "0", "-1"), resp) + + assert rendered == FormattedText( + [ + ("", "1)"), + ("", " "), + ("class:integer", "667 "), + ("class:member", '"duck"'), + ("", "\n"), + ("", "2)"), + ("", " "), + ("class:integer", "708 "), + ("class:member", '"camel"'), + ] + ) + assert completer.member_completer.words == ["camel", "duck"] + + +def test_render_members_config_raw(): + completer = IRedisCompleter() + completer.member_completer.words = [] + config.withscores = True + resp = [b"duck", b"667", b"camel", b"708"] + rendered = renders.OutputRender.render_raw(resp) + completer.update_completer_for_response("ZRANGE", (), resp) + + assert rendered == b"duck\n667\ncamel\n708" + assert completer.member_completer.words == ["camel", "duck"] + + +def test_render_unixtime_config_raw(): + # fake the timezone and reload + os.environ["TZ"] = "Asia/Shanghai" + time.tzset() + rendered = renders.OutputRender.render_unixtime(1570469891) + + assert rendered == FormattedText( + [ + ("class:type", "(integer) "), + ("", "1570469891"), + ("", "\n"), + ("class:type", "(local time)"), + ("", " "), + ("", "2019-10-08 01:38:11"), + ] + ) + + +def test_render_unixtime(): + rendered = renders.OutputRender.render_raw(1570469891) + + assert rendered == b"1570469891" + + +def test_bulk_string_reply(): + assert renders.OutputRender.render_bulk_string(b"'\"") == '''"'\\""''' + + +def test_bulk_string_reply_raw(): + assert renders.OutputRender.render_raw(b"hello") == b"hello" + + +def test_render_bulk_string_decoded(): + EXPECTED_RENDER = """# Server\nredis_version:5.0.5\nredis_git_sha1:00000000\nredis_git_dirty:0\nredis_build_id:31cd6e21ec924b46""" # noqa + _input = b"# Server\r\nredis_version:5.0.5\r\nredis_git_sha1:00000000\r\nredis_git_dirty:0\r\nredis_build_id:31cd6e21ec924b46" # noqa + assert renders.OutputRender.render_bulk_string_decode(_input) == EXPECTED_RENDER + + +def test_render_bulk_string_decoded_with_decoded_utf8(): + EXPECTED_RENDER = """# Server\nredis_version:5.0.5\nredis_git_sha1:00000000\nredis_git_dirty:0\nredis_build_id:31cd6e21ec924b46""" # noqa + _input = "# Server\r\nredis_version:5.0.5\r\nredis_git_sha1:00000000\r\nredis_git_dirty:0\r\nredis_build_id:31cd6e21ec924b46" # noqa + assert renders.OutputRender.render_bulk_string_decode(_input) == EXPECTED_RENDER + + +def test_render_time(): + value = [b"1571305643", b"765481"] + assert renders.OutputRender.render_time(value) == FormattedText( + [ + ("class:type", "(unix timestamp) "), + ("", "1571305643"), + ("", "\n"), + ("class:type", "(millisecond) "), + ("", "765481"), + ("", "\n"), + ("class:type", "(convert to local timezone) "), + ("", "2019-10-17 17:47:23.765481"), + ] + ) + + assert renders.OutputRender.render_raw(value) == b"1571305643\n765481" + + +def test_render_nested_pairs(): + text = [ + b"peak.allocated", + 10160336, + b"lua.caches", + 0, + b"db.0", + [b"overhead.hashtable.main", 648, b"overhead.hashtable.expires", 32], + b"db.1", + [b"overhead.hashtable.main", 112, b"overhead.hashtable.expires", 32], + b"fragmentation", + b"0.062980629503726959", + b"fragmentation.bytes", + -9445680, + ] + + assert renders.OutputRender.render_raw(text) == ( + b"peak.allocated\n10160336\nlua.caches\n0\ndb.0\noverhead.hashtable.main\n64" + b"8\noverhead.hashtable.expires\n32\ndb.1\noverhead.hashtable.main\n112\nove" + b"rhead.hashtable.expires\n32\nfragmentation\n0.062980629503726959\nfragmentat" + b"ion.bytes\n-9445680" + ) + + assert renders.OutputRender.render_nested_pair(text) == FormattedText( + [ + ("class:string", "peak.allocated: "), + ("class:value", "10160336"), + ("", "\n"), + ("class:string", "lua.caches: "), + ("class:value", "0"), + ("", "\n"), + ("class:string", "db.0: "), + ("", "\n"), + ("class:string", " overhead.hashtable.main: "), + ("class:value", "648"), + ("", "\n"), + ("class:string", " overhead.hashtable.expires: "), + ("class:value", "32"), + ("", "\n"), + ("class:string", "db.1: "), + ("", "\n"), + ("class:string", " overhead.hashtable.main: "), + ("class:value", "112"), + ("", "\n"), + ("class:string", " overhead.hashtable.expires: "), + ("class:value", "32"), + ("", "\n"), + ("class:string", "fragmentation: "), + ("class:value", "0.062980629503726959"), + ("", "\n"), + ("class:string", "fragmentation.bytes: "), + ("class:value", "-9445680"), + ] + ) + + +def test_render_nested_list(): + text = [[b"get", 2, [b"readonly", b"fast"], 1, 1, 1]] + assert renders.OutputRender.render_list(text) == FormattedText( + [ + ("", "1)"), + ("", " "), + ("", "1)"), + ("", " "), + ("class:string", '"get"'), + ("", "\n"), + ("", " "), + ("", "2)"), + ("", " "), + ("class:string", '"2"'), + ("", "\n"), + ("", " "), + ("", "3)"), + ("", " "), + ("", "1)"), + ("", " "), + ("class:string", '"readonly"'), + ("", "\n"), + ("", " "), + ("", "2)"), + ("", " "), + ("class:string", '"fast"'), + ("", "\n"), + ("", " "), + ("", "4)"), + ("", " "), + ("class:string", '"1"'), + ("", "\n"), + ("", " "), + ("", "5)"), + ("", " "), + ("class:string", '"1"'), + ("", "\n"), + ("", " "), + ("", "6)"), + ("", " "), + ("class:string", '"1"'), + ] + ) + + +def test_render_bytes(config): + assert renders.OutputRender.render_bytes(b"bytes\n") == b"bytes" + + +def test_render_bytes_raw(config): + assert renders.OutputRender.render_raw(b"bytes\n") == b"bytes\n" diff --git a/tests/unittests/test_utils.py b/tests/unittests/test_utils.py new file mode 100644 index 0000000..c9b5eff --- /dev/null +++ b/tests/unittests/test_utils.py @@ -0,0 +1,122 @@ +import re +import time +import pytest +from unittest.mock import patch + +from iredis.utils import timer, strip_quote_args +from iredis.commands import split_command_args, split_unknown_args +from iredis.utils import command_syntax +from iredis.style import STYLE +from iredis.exceptions import InvalidArguments, AmbiguousCommand +from iredis.commands import commands_summary +from prompt_toolkit import print_formatted_text + + +def test_timer(): + with patch("iredis.utils.logger") as mock_logger: + timer("foo") + time.sleep(0.1) + timer("bar") + mock_logger.debug.assert_called() + args, kwargs = mock_logger.debug.call_args + matched = re.match(r"\[timer (\d)\] (0\.\d+) -> bar", args[0]) + + assert matched.group(1) == str(3) + assert 0.1 <= float(matched.group(2)) <= 0.2 + + # --- test again --- + timer("foo") + time.sleep(0.2) + timer("bar") + mock_logger.debug.assert_called() + args, kwargs = mock_logger.debug.call_args + matched = re.match(r"\[timer (\d)\] (0\.\d+) -> bar", args[0]) + + assert matched.group(1) == str(5) + assert 0.2 <= float(matched.group(2)) <= 0.3 + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ("hello world", ["hello", "world"]), + ("'hello world'", ["hello world"]), + ('''hello"world"''', ["helloworld"]), + (r'''hello\"world"''', [r"hello\world"]), + (r'"\\"', [r"\\"]), + (r"\\", [r"\\"]), + (r"\abcd ef", [r"\abcd", "ef"]), + # quotes in quotes + (r""" 'hello"world' """, ['hello"world']), + (r""" "hello'world" """, ["hello'world"]), + (r""" 'hello\'world'""", ["hello'world"]), + (r""" "hello\"world" """, ['hello"world']), + (r"''", [""]), # set foo "" is a legal command + (r'""', [""]), # set foo "" is a legal command + (r"\\", ["\\\\"]), # blackslash are legal + ("\\hello\\", ["\\hello\\"]), # blackslash are legal + ], +) +def test_stipe_quote_escaple_in_quote(test_input, expected): + assert list(strip_quote_args(test_input)) == expected + + +@pytest.mark.parametrize( + "command,expected,args", + [ + ("GET a", "GET", ["a"]), + ("cluster info", "cluster info", []), + ("getbit foo 17", "getbit", ["foo", "17"]), + ("command ", "command", []), + (" command count ", "command count", []), + (" command count ", "command count", []), # command with multi space + (" command count ' hello world'", "command count", [" hello world"]), + ("set foo 'hello world'", "set", ["foo", "hello world"]), + ], +) +def test_split_commands(command, expected, args): + parsed_command, parsed_args = split_command_args(command) + assert expected == parsed_command + assert args == parsed_args + + +def test_split_commands_fail_on_unknown_command(): + with pytest.raises(InvalidArguments): + split_command_args("FOO BAR") + + +@pytest.mark.parametrize( + "command", + ["command in", "command in", "Command in", "COMMAND in"], +) +def test_split_commands_fail_on_partially_input(command): + with pytest.raises(AmbiguousCommand): + split_command_args(command) + + +def test_split_commands_fail_on_unfinished_command(): + with pytest.raises(InvalidArguments): + split_command_args("setn") + + +def test_render_bottom_with_command_json(): + for command, info in commands_summary.items(): + print_formatted_text(command_syntax(command, info), style=STYLE) + + +@pytest.mark.parametrize( + "raw,command,args", + [ + ("abc 123", "abc", ["123"]), + ("abc", "abc", []), + ("abc foo bar", "abc", ["foo", "bar"]), + ("abc 'foo bar'", "abc", ["foo bar"]), + ('abc "foo bar"', "abc", ["foo bar"]), + ('abc "foo bar" 3 hello', "abc", ["foo bar", "3", "hello"]), + ('abc "foo \nbar"', "abc", ["foo \nbar"]), + ], +) +def test_split_unknown_commands(raw, command, args): + parsed_command, parsed_args = split_unknown_args(raw) + assert command == parsed_command + assert args == parsed_args |