From 132a1850e6850f26b69702e2a68c9e4c250ce59c Mon Sep 17 00:00:00 2001 From: Boaz Sade Date: Wed, 1 Feb 2023 18:14:15 +0200 Subject: [PATCH 1/3] feat(docs): adding scripts for documentation generation Signed-off-by: Boaz Sade --- scripts/.dockerignore | 3 + scripts/Dockerfile | 8 + scripts/README.md | 122 +++++++ scripts/generate_by_docker.sh | 68 ++++ scripts/generate_commands_docs.py | 508 ++++++++++++++++++++++++++++++ scripts/generate_docs.sh | 153 +++++++++ scripts/requirements.txt | 5 + 7 files changed, 867 insertions(+) create mode 100644 scripts/.dockerignore create mode 100644 scripts/Dockerfile create mode 100644 scripts/README.md create mode 100755 scripts/generate_by_docker.sh create mode 100755 scripts/generate_commands_docs.py create mode 100755 scripts/generate_docs.sh create mode 100644 scripts/requirements.txt diff --git a/scripts/.dockerignore b/scripts/.dockerignore new file mode 100644 index 00000000..31314f83 --- /dev/null +++ b/scripts/.dockerignore @@ -0,0 +1,3 @@ +Dockerfile +dockers +generate_by_docker.sh diff --git a/scripts/Dockerfile b/scripts/Dockerfile new file mode 100644 index 00000000..28bd9752 --- /dev/null +++ b/scripts/Dockerfile @@ -0,0 +1,8 @@ +FROM ubuntu:20.04 + +RUN apt update -y && apt install -y wget vim python3 python3-distutils git +RUN wget https://bootstrap.pypa.io/get-pip.py && python3 get-pip.py +WORKDIR /dragonfly +ADD . . +#RUN python3 -m pip install -r requirements.txt +RUN python3 -m pip install virtualenv diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..e65fb67e --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,122 @@ +# Building Commands Docs + +## Overview +This directory contains a collection of scripts and dockerfile to generate the files that would be used for the website to show commands documents. +Using these scripts we can generate them either from local location or by accessing the file on the remote repository. + +## Requirements +If you would like to run these scripts locally without using a docker image, you must install the following on you local machine (assuming your running on Ubuntu Linux): +- Python 3 (sudo apt install -y python) +- Python pip (wget https://bootstrap.pypa.io/get-pip.py && python3 get-pip.py) +- Python virtual env (python3 -m pip install virtualenv) +- Git (sudo apt install -y git) + +## Building Documents +### Building with Docker +The script generate_by_docker.sh allow building inside docker. +This script accept 2 options: +- --build [image name]: This option would build the docker image. +- --run [volume name] [image name]: this option would generate the files using the script generate_docs.sh and using option --clone for it. +Note that you can run the --build option and then use the docker image to run the script with other options. +For example: +Building the image: +``` +./generate_by_docker.sh -b +``` +Building the image with specific image name: +``` +./generate_by_docker.sh -b build_image +``` + +Generating documentation using the image: +``` +./generate_by_docker.sh -r +``` +Generating the docs using you own volume: +``` +./generate_by_docker.sh -b /path/to/my/host/volume +``` +Generating the docks using your own volume and your own image name: +``` +./generate_by_docker.sh -b /path/to/my/host/volume build_image +``` + +### Running from Terminal +The script generate_docs.sh is the "engine" for running the python script. +This would simplify the running of the python script and add some more options. +With this script, when running without command line options, it would clone [redis-doc](https://github.com/dragonflydb/redis-doc), to the local host, +and write the result to /tmp//docs/content directory. +The options for this script are: +- --clone This would clone the files from https://github.com/dragonflydb/redis-doc.git into local directory and run on it (the default) +- --remote This would use the above URL to fetch files without copying them locally +- --output "dir name" This would place the generated files at the "dir name", default to /tmp/"current utc timestamp"/docs/content +- --local "input dir" Run this using a local files at location "input dir" +- --clean This would remove the build and log files +Note that the default is to run with --clone option + +For example: +Building the documents using data from the [redis-doc](https://github.com/dragonflydb/redis-doc) +``` +./generate_docs.sh +``` + +Building the documents using data from the [redis-doc](https://github.com/dragonflydb/redis-doc) with explicit cloning +``` +./generate_docs.sh --clone +``` + +Building the documents using data from the [redis-doc](https://github.com/dragonflydb/redis-doc) without doing any cloning +``` +./generate_docs.sh --remote +``` + +Building the documents using data from the [redis-doc](https://github.com/dragonflydb/redis-doc) after you already have it locally +``` +./generate_docs.sh --local /path/to/files/from/redis-doc +``` + +Building the documents using data from the [redis-doc](https://github.com/dragonflydb/redis-doc) and setting output directory +./generate_docs.sh --clone --output /path/to/write/content + +Cleaning up (this is best run with the option of the output directory since this cannot be derived directly) +./generate_docs.sh --clean --output /path/to/write/content + +### Running directly (development mode) +The script that generates the output files to website is called generate_commands_docs.py +It is best to create and use python virtual environment for it, so you would not install all dependencies globally. +To install the required dependencies run: +``` +pip3 install -r requirements.txt +``` + +This scripts accepts the following options: +- --output_dir "path" : Output directory to which result will be writing "path" (required). +- --local "path": Running with local repository "input file location" (optional) +Note that if you're not running with --local, the script will connect to the remote [redis-doc](https://github.com/dragonflydb/redis-doc), and generate the output by reading directly from there. +The resulting files will be writing to the output_dir that was passed to the script. +Note that this will place each command in a separate subdirectory and the content is writing to index.md file. +For example: +Building the content without using local files: +``` +./generate_commands_docs.py --output_dir /path/to/write/content +``` + +Building content using existing files from [redis-doc](https://github.com/dragonflydb/redis-doc) +``` +./generate_commands_docs.py --output_dir /path/to/write/content --local /path/to/files/from/redis-doc +``` + + + +## Manually Running +In order to run using the docker image, you would need to make sure that you have docker support on your host - [get docker](https://docs.docker.com/get-docker/). +Then you can either manually build +``` +docker build -t dragonfly_docs_builder . +``` +and +``` +docker run --rm -t -v :/tmp/docs/content/commands \ + dragonfly_docs_builder ./generate_docs.sh -o /tmp/docs/content/commands +``` +Or using the commands above diff --git a/scripts/generate_by_docker.sh b/scripts/generate_by_docker.sh new file mode 100755 index 00000000..5b2eb536 --- /dev/null +++ b/scripts/generate_by_docker.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +# Copyright 2022, DragonflyDB authors. All rights reserved. +# See LICENSE for licensing terms. + +## Building the dockements using docker as the build env +# Option +# -b|--build [tagname] build the docker image locally, default to dragonfly_docs_builder +# -r|--run [image name] run the container and generate the build + +IMAGE_NAME=dragonfly_docs_builder +LOCAL_VOLUME=/tmp/docs/content/commands +CURRENT_RELATIVE=`dirname "$0"` +ABS_SCRIPT_PATH=`( cd "$CURRENT_RELATIVE" && pwd )` + +function build_image { + if [ "$1" != "" ]; then + IMAGE_NAME=$1 + fi + echo "building a new image for the container [$IMAGE_NAME]" + docker build -t ${IMAGE_NAME} . + return $? +} + +function generate_docs { + if [ "$2" != "" ]; then + IMAGE_NAME=$2 + LOCAL_VOLUME=$1 + else + if [ "$1" != "" ]; then + LOCAL_VOLUME=$1 + fi + fi + echo "generating the documents using the docker image [$LOCAL_VOLUME] [$IMAGE_NAME]" + if [ ! -d ${LOCAL_VOLUME} ]; then + mkdir -p ${LOCAL_VOLUME} || { + echo failed to generate local volume dir at ${LOCAL_VOLUME} + return 1 + } + fi + docker run --rm -t -v ${LOCAL_VOLUME}:/tmp/docs/content/commands \ + ${IMAGE_NAME} ./generate_docs.sh -o /tmp/docs/content/commands + return $? +} + + +if [ $# -lt 1 ]; then + echo "usage: -b|--build [tagname] | -r|--run [image name]" + exit 1 +fi + +while [[ $# -gt 0 ]]; do + case $1 in + -b|--build) + build_image $2 + exit $? + ;; + -r|--run) + generate_docs $2 $3 + exit $? + ;; + *) + echo "usage: -b|--build [tagname] | -r|--run [image name]" + exit 1 + ;; + esac +done + diff --git a/scripts/generate_commands_docs.py b/scripts/generate_commands_docs.py new file mode 100755 index 00000000..493c1761 --- /dev/null +++ b/scripts/generate_commands_docs.py @@ -0,0 +1,508 @@ +#!/usr/bin/env python3 + +# Copyright 2022, DragonflyDB authors. All rights reserved. +# See LICENSE for licensing terms. + + +import pathlib +import requests +import sys +import json +import argparse +import logging +from functools import partial +from io import StringIO, TextIOWrapper +from random import choice +import re +import os +from textwrap import fill +from typing import Optional, Sequence +from xmlrpc.client import Boolean +from enum import Enum +from railroad import * +from typing import List +import pytoml +import yaml + +USER = "dragonflydb" +REPO_NAME = "redis-doc" + +# Non-breaking space +NBSP = '\xa0' + +# HTML Word Break Opportunity +WBR = '' + + +class ArgumentType(Enum): + INTEGER = 'integer' + DOUBLE = 'double' + STRING = 'string' + UNIX_TIME = 'unix-time' + PATTERN = 'pattern' + KEY = 'key' + ONEOF = 'oneof' + BLOCK = 'block' + PURE_TOKEN = 'pure-token' + COMMAND = 'command' + + +FM_TYPES = { + '{\n': { + 'eof': '}\n', + 'ext': '.json' + }, + '---\n': { + 'eof': '---\n', + 'ext': '.yaml' + }, + '+++\n': { + 'eof': '+++\n', + 'ext': '.toml' + } +} + +PARSERS = { + '.json': { + 'dump': lambda x, y: json.dump(x, y, indent=4), + 'dumps': lambda x: json.dumps(x, indent=4), + 'load': lambda x: json.load(x), + 'loads': lambda x: json.loads(x), + }, + '.yaml': { + 'dump': lambda x, y: yaml.dump(x, y), + 'dumps': lambda x: yaml.dump(x), + 'load': lambda x: yaml.load(x, Loader=yaml.FullLoader), + 'loads': lambda x: yaml.load(io.StringIO(x), Loader=yaml.FullLoader), + }, + '.toml': { + 'dump': lambda x, y: pytoml.dump(x, y), + 'dumps': lambda x: pytoml.dumps(x), + 'load': lambda x: pytoml.load(x), + 'loads': lambda x: pytoml.loads(x), + }, +} + + +def do_dumps(payload: str, file_type: str): + return PARSERS[file_type]['dumps'](payload) + + +def do_dump_file(payload: str, file_type: str, file_handler: TextIOWrapper): + return PARSERS[file_type]['dump'](payload, file_handler) + + +def do_load(payload: str, file_type: str): + return PARSERS[file_type]["loads"](payload) + + +def do_load_file(payload: str, file_type: str, file_handler: TextIOWrapper): + PARSERS[file_type]["load"](payload, file_handler) + + +def command_filename(name: str) -> str: + return name.lower().replace(' ', '-') + + +def base_url(user, repo_name): + return f'https://raw.githubusercontent.com/{user}/{repo_name}/master/' + + +def read_from_github(path_to_file): + base_name = base_url(USER, REPO_NAME) + url = f'{base_name}/{path_to_file}' + req = requests.get(url) + if req.status_code == requests.codes.ok: + return req.text + else: + logging.debug('Content was not found.') + return None + + +def read_from_local(base_path, path_to_file): + full_path = os.path.join(base_path, path_to_file) + txt = pathlib.Path(full_path).read_text() + return txt + + +def read_md_file(command_name: str, read_f) -> str: + command_name = command_filename(command_name) + content = read_f(f"commands/{command_name}.md") + return content + + +def read_commands_json(read_f) -> dict: + content = read_f('commands.json') + if content is not None: + commands = json.loads(content) + return commands + else: + return None + + +def generate_payload_from_md(file_name: str, read_f): + payload = read_md_file(file_name, read_f) + i = 0 + while i < len(payload): + if payload[i].startswith('\ufeff'): # BOM workaround + payload[i] = payload[i][1:] + if payload[i].strip() == '': # Munch newlines and whitespaces + i += 1 + else: + fm_type = FM_TYPES.get(payload[i]) + break + + if not fm_type: + payload = ''.join(payload) + fm_type = FM_TYPES.get('---\n') + fm_ext = fm_type.get('ext') + return payload, fm_type, None + eof, fm_ext = fm_type.get('eof'), fm_type.get('ext') + + fm_data = {} + if fm_ext == '.json': + fm_data.update(do_load( + fm_ext, ''.join(payload[i:j+1]))) + payload = ''.join(payload[j+1:]) + else: + fm_data.update(do_load( + fm_ext, ''.join(payload[i+1:j]))) + payload = ''.join(payload[j+1:]) + logging.info( + f"we have payload of size {len(payload)} and fm type {fm_type}") + return payload, fm_type, fm_data + + +class RuntimeException(Exception): + def __init__(self, err: str): + self.error = err + + def __str__(self): + return self.error + + +# def syntax(name: str, **kwargs) -> str: +# opts = { +# 'width': kwargs.get('width', 68), +# 'subsequent_indent': ' ' * 2, +# 'break_long_words': False, +# 'break_on_hyphens': False +# } +# args = [name] + [arg.syntax() for arg in self._arguments] +# return fill(' '.join(args), **opts) + + +class Argument: + def __init__(self, data: dict = {}, level: int = 0) -> None: + self._level: int = level + self._name: str = data['name'] + self._type = ArgumentType(data['type']) + self._optional: bool = data.get('optional', False) + self._multiple: bool = data.get('multiple', False) + self._multiple_token: bool = data.get('multiple_token', False) + self._token: str | None = data.get('token') + self._display: str = data.get('display', self._name) + if self._token == '': + self._token = '""' + self._arguments: List[Argument] = [ + Argument(arg, self._level+1) for arg in data.get('arguments', [])] + + def syntax(self, **kwargs) -> str: + show_types = kwargs.get('show_types') + args = '' + if self._type == ArgumentType.BLOCK: + args += ' '.join([arg.syntax() for arg in self._arguments]) + elif self._type == ArgumentType.ONEOF: + args += f' | '.join([arg.syntax() for arg in self._arguments]) + elif self._type != ArgumentType.PURE_TOKEN: + args += self._display + if show_types: + args += f':{self._type.value}' + + syntax = '' + if self._optional: + syntax += '[' + + if self._token: + syntax += f'{self._token}' + if self._type != ArgumentType.PURE_TOKEN: + syntax += NBSP + + if self._type == ArgumentType.ONEOF and (not self._optional or self._token): + syntax += '<' + + if self._multiple: + if self._multiple_token: + syntax += f'{args} [{self._token} {args} ...]' + else: + syntax += f'{args} [{args} ...]' + else: + syntax += args + + if self._type == ArgumentType.ONEOF and (not self._optional or self._token): + syntax = f'{syntax.rstrip()}>' + if self._optional: + syntax = f'{syntax.rstrip()}]' + + return f'{syntax}' + + +def make_args_list(name: str, args: dict) -> Argument: + carg = { + 'name': name, + 'type': ArgumentType.COMMAND.value, + 'arguments': args.get('arguments', []) + } + arguments = Argument(carg, 0) + return arguments + + +def args_syntax(name: str, args: dict) -> str: + arguments = make_args_list(name, args) + s = ' '.join([arg.syntax() for arg in arguments._arguments[1:]]) + return s + + +def help_command(name: str) -> bool: + return name.endswith(" HELP") + + +def container_cmd(cmd_info: dict) -> bool: + return cmd_info.get('arguments') is None and cmd_info.get('arity', 0) == -2 and len(cmd_info.split(' ')) == 1 + + +def command_args(name: str, args: dict, **kwargs) -> str: + opts = { + 'width': kwargs.get('width', 68), + 'subsequent_indent': ' ' * 2, + 'break_long_words': False, + 'break_on_hyphens': False + } + arguments = make_args_list(name, args) + args = [name] + [arg.syntax() for arg in arguments._arguments] + return fill(' '.join(args), **opts) + + +def get_command_tokens(arguments: dict) -> set: + rep = set() + if type(arguments) is list: + for arg in arguments: + rep = rep.union(get_command_tokens(arg)) + else: + if 'token' in arguments: + rep.add(arguments['token']) + if 'arguments' in arguments: + for arg in arguments['arguments']: + rep = rep.union(get_command_tokens(arg)) + return rep + + +def generate_commands_links(name: str, commands: dict, payload: str) -> str: + def fix_links(commands: dict, name: str): + if name: + exclude = set([name]) + tokens = get_command_tokens(commands.get(name)) + exclude.union(tokens) + else: + exclude = set() + + def generate_md_links(m): + command = m.group(1) + if command in commands and command not in exclude: + return f'[`{command}`](/commands/{command_filename(command)})' + else: + return m.group(0) + return generate_md_links + links = fix_links(commands, name) + rep = re.sub(r'`([A-Z][A-Z-_ \.]*)`', links, payload) + rep = re.sub(r'`!([A-Z][A-Z-_ \.]*)`', lambda x: f'`{x[1]}`', rep) + return rep + + +def add_command_frontmatter(name: str, commands: dict, fm_data: dict): + """ Sets a JSON FrontMatter payload for a command page """ + data = commands.get(name) + data.update({ + 'title': name, + 'linkTitle': name, + 'description': data.get('summary'), + 'syntax_str': args_syntax(name, data), + 'syntax_fmt': command_args(name, data), + 'hidden': container_cmd(data) or help_command(name) + }) + if 'replaced_by' in data: + data['replaced_by'] = generate_commands_links( + name, commands, data.get('replaced_by')) + fm_type = FM_TYPES.get('---\n') + fm_ext = fm_type.get('ext') + fm_data.update(data) + + +def persist(payload: str, data: dict, is_json: Boolean, filepath: str, file_type: dict) -> int: + ''' + Save to persist storage + ''' + fm = do_dumps(data, file_type['ext']) + logging.info( + f"saving payload of len {len(payload)} and header of {len(data)} to file {filepath}") + if not is_json: + fm = f'{file_type.get("eof")}{fm}{file_type.get("eof")}' + else: + fm += '\n' + + payload = fm + payload + dir_p = os.path.dirname(filepath) + logging.info( + f"saving payload of len {len(payload)} to file {filepath}, creating at {dir_p}") + pathlib.Path(dir_p).mkdir(parents=True, exist_ok=True) + with open(filepath, 'w') as f: + return f.write(payload) > 0 + + +def convert_command_sections(payload: str): + """ Converts redis-doc section headers to MD """ + rep = re.sub(r'@examples\n', + '## Examples\n', payload) + rep = re.sub(r'@return\n', + '## Return\n', rep) + return rep + + +def convert_reply_shortcuts(payload: str) -> str: + """ Convert RESP2 reply type shortcuts to links """ + def reply(x): + resp2 = { + 'nil': ('resp-bulk-strings', 'Null reply'), + 'simple-string': ('resp-simple-strings', 'Simple string reply'), + 'integer': ('resp-integers', 'Integer reply'), + 'bulk-string': ('resp-bulk-strings', 'Bulk string reply'), + 'array': ('resp-arrays', 'Array reply'), + 'error': ('resp-errors', 'Error reply'), + + } + rep = resp2.get(x.group(1), None) + if rep: + return f'[{rep[1]}](/docs/reference/protocol-spec#{rep[0]})' + return f'[]' + + rep = re.sub(r'@([a-z\-]+)-reply', reply, payload) + return rep + + +def process_command(name: str, commands: dict, payload: str) -> str: + """ New command processing logic """ + payload = generate_commands_links( + name, commands, payload) + payload = convert_command_sections(payload) + payload = convert_reply_shortcuts(payload) + # payload = convert_cli_snippets(payload) + return payload + + +def persist_command(name: str, commands: dict, payload: str, filepath: str, fm_data: dict, file_type: dict) -> int: + """ + name: the name of the command: for example "ACL" + commands: data taken from data/commands.json file and convert to python dict + filepath: path to the index.md file: content/en/commands/ + fm_data: dict with {'github_branch': , 'github_path': ', github_repo': } + is_json: str: + def command_filename(name: str) -> str: + return name.lower().replace(' ', '-') + """ + get the path to the md file from the commands list based on the command name + """ + file_name = command_filename(cmd_name) + return os.path.join(base_name, file_name) + + +def driver(commands: dict, out_dir: str, read_f, support_cmd: List) -> int: + # try: + commands = read_commands_json(read_f) + max = len(support_cmd) + count = 0 + for cmd_name, cmd_info in commands.items(): + if cmd_name in support_cmd: + if count == max: + logging.info(f"finish processing after {count}") + return 0 + else: + count += 1 + if read_md_file(command_filename(cmd_name), read_function): + logging.debug(f"successfully read data for {cmd_name}") + else: + logging.error(f"failed to read the md file for {cmd_name}") + return False + filepath = os.path.join(filepath_from_cmd( + cmd_name, out_dir), "index.md") + default_repos = {'github_branch': "main", + "github_path": filepath, "github_repo": f"https://{USER}/{REPO_NAME}"} + payload, f_type, m_data = generate_payload_from_md( + cmd_name, read_f) + fm_data = m_data if m_data else default_repos + fm_type = f_type if f_type is not None else FM_TYPES['---\n'] + if persist_command(cmd_name, commands, payload, filepath, fm_data, fm_type) == 0: + raise RuntimeException( + f"failed to process command {cmd_name} for file {filepath}") + return 0 + # except Exception as e: + # print( + # f"failed to process commands into documentation: {e}") + # return -1 + + +def process_docs(read_f, out_dir) -> bool: + commands = read_commands_json(read_function) + if commands is None: + logging.error("failed to read commands list") + return False + else: + logging.debug( + f"we have {len(commands)} commands from commands.json file") + cmd = ["HSET", "LLEN", "LMPOP", "LMOVE", + "SET", "GET", "SUBSCRIBE", "PUBLISH"] + if driver(commands, out_dir, read_f, cmd) < 0: + return False + return True + + +if __name__ == "__main__": + logging.basicConfig( + level=logging.DEBUG, format=f'{sys.argv[0]}: %(levelname)s %(asctime)s %(message)s', filename="/tmp/build_commands_documents_messages.log") + parser = argparse.ArgumentParser( + description='converting markdown files to website commands documentation.') + parser.add_argument( + "-l", "--local", help="Running with local repository ", type=str) + # + parser.add_argument( + "-o", "--output_dir", help="Output directory to which result will be writing ", type=str, required=True) + args = parser.parse_args() + if args.local: + if os.path.exists(args.local) and pathlib.Path(args.local).is_dir(): + read_f = partial(read_from_local, args.local) + logging.debug(f"running from files at {args.local}") + read_function = read_f + else: + print( + f"not such directory {args.local} - you must use a valid path to the location of the commands.json and commands directory") + sys.exit(1) + else: + logging.debug(f"running with remote host {base_url(USER, REPO_NAME)}") + read_function = read_from_github + # + p = pathlib.Path(args.output_dir) + p.mkdir(parents=True, exist_ok=True) + if not process_docs(read_function, args.output_dir): + logging.error("failed to process input data to commands documents") + sys.exit(1) + sys.exit(0) diff --git a/scripts/generate_docs.sh b/scripts/generate_docs.sh new file mode 100755 index 00000000..d7497e09 --- /dev/null +++ b/scripts/generate_docs.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash + +# Copyright 2022, DragonflyDB authors. All rights reserved. +# See LICENSE for licensing terms. + +## This would generate files that would be used for the web site as documentation for the supported commands +## The build process is based on 2 components: +## 1. The input files taken from repo https://github.com/dragonflydb/redis-doc +## 2. The python script generate_commands_docs.py found under this directory +## Command line options: +## --clone This would clone the files from https://github.com/dragonflydb/redis-doc.git into local directory and run on it (the default) +## --remote This would use the above URL to fatch files withtout copying them locally +## -- output This would place the generated files at the default to /tmp//docs/content +## --local Run this using a local files at location +## --clean This would remove the build and log files +## Please note that the log from the python script is at /tmp/build_commands_documents_messages + +function generate_docs { + # create python virtual env for this + cd ${ABS_SCRIPT_PATH} + virtualenv docenv || { + echo "failed to create virtual env to run python script" + return 1 + } + ./docenv/bin/pip3 install -r requirements.txt || { + echo "failed to install dependencies for the python script" + rm -rf docenv + return 1 + } + mkdir -p ${OUTPUT_DIR} || { + rm -rf docenv + echo "failed to generate output directory at ${OUTPUT_DIR}" + return 1 + } + if [ "${RUN_LOCAL}" = "no" ]; then + ./docenv/bin/python3 generate_commands_docs.py --output_dir ${OUTPUT_DIR} + else + ./docenv/bin/python3 generate_commands_docs.py --output_dir ${OUTPUT_DIR} --local ${INPUT_DIR} + fi + result=$? + rm -rf ./docenv + return ${result} + +} + +function do_cleanup { + echo "cleaninng up" + rm -rf ${GIT_CLONED_PATH} /tmp/build_commands_documents_messages.log ${OUTPUT_DIR} + echo "finish cleaning ${GIT_CLONED_PATH} tmp/build_commands_documents_messages.log and ${OUTPUT_DIR}" + return 0 +} +function do_git_clone { + current_dir=${PWD} + echo "cloning into ${GIT_CLONED_PATH}" + if [ ! -d ${GIT_CLONED_PATH}/redis-doc ]; then + mkdir -p ${GIT_CLONED_PATH} || { + echo "failed to create ${GIT_CLONED_PATH} to clone files into" + return 1 + } + cd ${GIT_CLONED_PATH} && git clone https://github.com/dragonflydb/redis-doc.git || { + echo "failed to clone from redis docs" + return 1 + } + else + cd ${GIT_CLONED_PATH}/redis-doc && git pull || { + echo "failed to update docs repo" + return 1 + } + fi + cd ${current_dir} + +} + +################# MAIN ######################################################## + +CURRENT_RELATIVE=`dirname "$0"` +ABS_SCRIPT_PATH=`( cd "$CURRENT_RELATIVE" && pwd )` + +# Default values +CURRENT_DATE=$(date +%s) +OUTPUT_DIR=/tmp/${CURRENT_DATE}/docs/content +GIT_CLONED_PATH=/tmp/build_commands_docs/docs_repo +RUN_LOCAL="yes" +DO_CLONE="yes" +INPUT_DIR=${GIT_CLONED_PATH}/redis-doc + +## Process command line args +while [[ $# -gt 0 ]]; do + case $1 in + -c|--clone) + DO_CLONE="yes" + RUN_LOCAL="yes" + INPUT_DIR=${GIT_CLONED_PATH}/redis-doc + shift + ;; + -r|--remote) + RUN_LOCAL="no" + DO_CLONE="no" + shift + ;; + -o|--output) + OUTPUT_DIR=$2 + shift + shift + ;; + -l|--local) + RUN_LOCAL="yes" + DO_CLONE="no" + INPUT_DIR=$2 + shift + shift + ;; + -c|--clean) + DO_DELETE="yes" + shift + ;; + *) + echo "usage: [-l|--local ] [-c|--clone (default option)] [-r|--remote] [-o|--output ] [-d|--delete]" + exit 1 + ;; + esac +done + +echo "Running cleanup ${DO_DELETE}, cloning ${DO_CLONE}, runing on local files ${RUN_LOCAL}, output will be writing to ${OUTPUT_DIR} taking files from ${INPUT_DIR}" + +## Run +if [ "${DO_DELETE}" = "yes" ]; then + do_cleanup + exit 0 +fi + + +if [ "${DO_CLONE}" = "yes" ]; then + do_git_clone + if [ $? -ne 0 ]; then + echo "failed to clone doc repo locally" + exit 1 + else + echo "successfully cloned doc repo to ${INPUT_DIR}" + fi + +fi + +generate_docs + +if [ $? -ne 0 ]; then + echo "failed to generate docs" + exit 1 +else + echo "successfully generated docs to ${OUTPUT_DIR}" + exit 0 +fi + diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 00000000..66306d27 --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1,5 @@ +pytoml==0.1.21 +PyYAML==6.0 +railroad-diagrams==1.1.1 +requests==2.27.1 +semver==2.13.0 From dd902b5e1c9d9b7a9900beaf316c15f5bc297120 Mon Sep 17 00:00:00 2001 From: Vahagn Aharonian Date: Tue, 7 Feb 2023 22:54:42 +0500 Subject: [PATCH 2/3] Commit generated command docs --- docs/command-reference/command-reference.md | 4 + docs/command-reference/get/index.md | 56 +++++++ docs/command-reference/hset/index.md | 67 ++++++++ docs/command-reference/llen/index.md | 54 +++++++ docs/command-reference/lmove/index.md | 155 +++++++++++++++++++ docs/command-reference/lmpop/index.md | 93 +++++++++++ docs/command-reference/publish/index.md | 41 +++++ docs/command-reference/set/index.md | 163 ++++++++++++++++++++ docs/command-reference/subscribe/index.md | 37 +++++ 9 files changed, 670 insertions(+) create mode 100644 docs/command-reference/get/index.md create mode 100644 docs/command-reference/hset/index.md create mode 100644 docs/command-reference/llen/index.md create mode 100644 docs/command-reference/lmove/index.md create mode 100644 docs/command-reference/lmpop/index.md create mode 100644 docs/command-reference/publish/index.md create mode 100644 docs/command-reference/set/index.md create mode 100644 docs/command-reference/subscribe/index.md diff --git a/docs/command-reference/command-reference.md b/docs/command-reference/command-reference.md index aea6b70d..24543afa 100644 --- a/docs/command-reference/command-reference.md +++ b/docs/command-reference/command-reference.md @@ -2,4 +2,8 @@ sidebar_position: 6 --- +import DocCardList from '@theme/DocCardList'; + # Command Reference + + diff --git a/docs/command-reference/get/index.md b/docs/command-reference/get/index.md new file mode 100644 index 00000000..c869408f --- /dev/null +++ b/docs/command-reference/get/index.md @@ -0,0 +1,56 @@ +--- +acl_categories: +- '@read' +- '@string' +- '@fast' +arguments: +- key_spec_index: 0 + name: key + type: key +arity: 2 +command_flags: +- readonly +- fast +complexity: O(1) +description: Get the value of a key +github_branch: main +github_path: /tmp/docs/content/commands/get/index.md +github_repo: https://dragonflydb/redis-doc +group: string +hidden: false +key_specs: +- RO: true + access: true + begin_search: + spec: + index: 1 + type: index + find_keys: + spec: + keystep: 1 + lastkey: 0 + limit: 0 + type: range +linkTitle: GET +since: 1.0.0 +summary: Get the value of a key +syntax_fmt: GET key +syntax_str: '' +title: GET +--- +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](/docs/reference/protocol-spec#resp-bulk-strings): the value of `key`, or `nil` when `key` does not exist. + +## Examples + +```cli +GET nonexisting +SET mykey "Hello" +GET mykey +``` diff --git a/docs/command-reference/hset/index.md b/docs/command-reference/hset/index.md new file mode 100644 index 00000000..f656c97f --- /dev/null +++ b/docs/command-reference/hset/index.md @@ -0,0 +1,67 @@ +--- +acl_categories: +- '@write' +- '@hash' +- '@fast' +arguments: +- key_spec_index: 0 + name: key + type: key +- arguments: + - name: field + type: string + - name: value + type: string + multiple: true + name: field_value + type: block +arity: -4 +command_flags: +- write +- denyoom +- fast +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. +description: Set the string value of a hash field +github_branch: main +github_path: /tmp/docs/content/commands/hset/index.md +github_repo: https://dragonflydb/redis-doc +group: hash +hidden: false +history: +- - 4.0.0 + - Accepts multiple `field` and `value` arguments. +key_specs: +- RW: true + begin_search: + spec: + index: 1 + type: index + find_keys: + spec: + keystep: 1 + lastkey: 0 + limit: 0 + type: range + update: true +linkTitle: HSET +since: 2.0.0 +summary: Set the string value of a hash field +syntax_fmt: HSET key field value [field value ...] +syntax_str: field value [field value ...] +title: HSET +--- +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. + +## Return + +[Integer reply](/docs/reference/protocol-spec#resp-integers): The number of fields that were added. + +## Examples + +```cli +HSET myhash field1 "Hello" +HGET myhash field1 +``` diff --git a/docs/command-reference/llen/index.md b/docs/command-reference/llen/index.md new file mode 100644 index 00000000..07a185e9 --- /dev/null +++ b/docs/command-reference/llen/index.md @@ -0,0 +1,54 @@ +--- +acl_categories: +- '@read' +- '@list' +- '@fast' +arguments: +- key_spec_index: 0 + name: key + type: key +arity: 2 +command_flags: +- readonly +- fast +complexity: O(1) +description: Get the length of a list +github_branch: main +github_path: /tmp/docs/content/commands/llen/index.md +github_repo: https://dragonflydb/redis-doc +group: list +hidden: false +key_specs: +- RO: true + begin_search: + spec: + index: 1 + type: index + find_keys: + spec: + keystep: 1 + lastkey: 0 + limit: 0 + type: range +linkTitle: LLEN +since: 1.0.0 +summary: Get the length of a list +syntax_fmt: LLEN key +syntax_str: '' +title: LLEN +--- +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](/docs/reference/protocol-spec#resp-integers): the length of the list at `key`. + +## Examples + +```cli +LPUSH mylist "World" +LPUSH mylist "Hello" +LLEN mylist +``` diff --git a/docs/command-reference/lmove/index.md b/docs/command-reference/lmove/index.md new file mode 100644 index 00000000..eb4751d9 --- /dev/null +++ b/docs/command-reference/lmove/index.md @@ -0,0 +1,155 @@ +--- +acl_categories: +- '@write' +- '@list' +- '@slow' +arguments: +- key_spec_index: 0 + name: source + type: key +- key_spec_index: 1 + name: destination + type: key +- arguments: + - name: left + token: LEFT + type: pure-token + - name: right + token: RIGHT + type: pure-token + name: wherefrom + type: oneof +- arguments: + - name: left + token: LEFT + type: pure-token + - name: right + token: RIGHT + type: pure-token + name: whereto + type: oneof +arity: 5 +command_flags: +- write +- denyoom +complexity: O(1) +description: Pop an element from a list, push it to another list and return it +github_branch: main +github_path: /tmp/docs/content/commands/lmove/index.md +github_repo: https://dragonflydb/redis-doc +group: list +hidden: false +key_specs: +- RW: true + access: true + begin_search: + spec: + index: 1 + type: index + delete: true + find_keys: + spec: + keystep: 1 + lastkey: 0 + limit: 0 + type: range +- RW: true + begin_search: + spec: + index: 2 + type: index + find_keys: + spec: + keystep: 1 + lastkey: 0 + limit: 0 + type: range + insert: true +linkTitle: LMOVE +since: 6.2.0 +summary: Pop an element from a list, push it to another list and return it +syntax_fmt: LMOVE source destination +syntax_str: destination +title: LMOVE +--- +Atomically returns and removes the first/last element (head/tail depending on +the `wherefrom` argument) of the list stored at `source`, and pushes the +element at the first/last element (head/tail depending on the `whereto` +argument) 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 `LMOVE source destination RIGHT LEFT` 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 first/last element from the list and pushing it as first/last +element of the list, so it can be considered as a list rotation command (or a +no-op if `wherefrom` is the same as `whereto`). + +This command comes in place of the now deprecated [`RPOPLPUSH`](/commands/rpoplpush). Doing +`LMOVE RIGHT LEFT` is equivalent. + +## Return + +[Bulk string reply](/docs/reference/protocol-spec#resp-bulk-strings): the element being popped and pushed. + +## Examples + +```cli +RPUSH mylist "one" +RPUSH mylist "two" +RPUSH mylist "three" +LMOVE mylist myotherlist RIGHT LEFT +LMOVE mylist myotherlist LEFT RIGHT +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`](/commands/rpop) +(using polling), or [`BRPOP`](/commands/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. + +`LMOVE` (or [`BLMOVE`](/commands/blmove) 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`](/commands/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 `LMOVE` 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`](/commands/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/docs/command-reference/lmpop/index.md b/docs/command-reference/lmpop/index.md new file mode 100644 index 00000000..4fd2dcb1 --- /dev/null +++ b/docs/command-reference/lmpop/index.md @@ -0,0 +1,93 @@ +--- +acl_categories: +- '@write' +- '@list' +- '@slow' +arguments: +- name: numkeys + type: integer +- key_spec_index: 0 + multiple: true + name: key + type: key +- arguments: + - name: left + token: LEFT + type: pure-token + - name: right + token: RIGHT + type: pure-token + name: where + type: oneof +- name: count + optional: true + token: COUNT + type: integer +arity: -4 +command_flags: +- write +- movablekeys +complexity: O(N+M) where N is the number of provided keys and M is the number of elements + returned. +description: Pop elements from a list +github_branch: main +github_path: /tmp/docs/content/commands/lmpop/index.md +github_repo: https://dragonflydb/redis-doc +group: list +hidden: false +key_specs: +- RW: true + access: true + begin_search: + spec: + index: 1 + type: index + delete: true + find_keys: + spec: + firstkey: 1 + keynumidx: 0 + keystep: 1 + type: keynum +linkTitle: LMPOP +since: 7.0.0 +summary: Pop elements from a list +syntax_fmt: "LMPOP numkeys key [key ...] [COUNT\_count]" +syntax_str: "key [key ...] [COUNT\_count]" +title: LMPOP +--- +Pops one or more elements from the first non-empty list key from the list of provided key names. + +`LMPOP` and [`BLMPOP`](/commands/blmpop) are similar to the following, more limited, commands: + +- [`LPOP`](/commands/lpop) or [`RPOP`](/commands/rpop) which take only one key, and can return multiple elements. +- [`BLPOP`](/commands/blpop) or [`BRPOP`](/commands/brpop) which take multiple keys, but return only one element from just one key. + +See [`BLMPOP`](/commands/blmpop) for the blocking variant of this command. + +Elements are popped from either the left or right of the first non-empty list based on the passed argument. +The number of returned elements is limited to the lower between the non-empty list's length, and the count argument (which defaults to 1). + +## Return + +[Array reply](/docs/reference/protocol-spec#resp-arrays): specifically: + +* A `nil` when no element could be popped. +* A two-element array with the first element being the name of the key from which elements were popped, and the second element is an array of elements. + +## Examples + +```cli +LMPOP 2 non1 non2 LEFT COUNT 10 +LPUSH mylist "one" "two" "three" "four" "five" +LMPOP 1 mylist LEFT +LRANGE mylist 0 -1 +LMPOP 1 mylist RIGHT COUNT 10 +LPUSH mylist "one" "two" "three" "four" "five" +LPUSH mylist2 "a" "b" "c" "d" "e" +LMPOP 2 mylist mylist2 right count 3 +LRANGE mylist 0 -1 +LMPOP 2 mylist mylist2 right count 5 +LMPOP 2 mylist mylist2 right count 10 +EXISTS mylist mylist2 +``` diff --git a/docs/command-reference/publish/index.md b/docs/command-reference/publish/index.md new file mode 100644 index 00000000..0c2a0ae5 --- /dev/null +++ b/docs/command-reference/publish/index.md @@ -0,0 +1,41 @@ +--- +acl_categories: +- '@pubsub' +- '@fast' +arguments: +- name: channel + type: string +- name: message + type: string +arity: 3 +command_flags: +- pubsub +- loading +- stale +- fast +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). +description: Post a message to a channel +github_branch: main +github_path: /tmp/docs/content/commands/publish/index.md +github_repo: https://dragonflydb/redis-doc +group: pubsub +hidden: false +linkTitle: PUBLISH +since: 2.0.0 +summary: Post a message to a channel +syntax_fmt: PUBLISH channel message +syntax_str: message +title: PUBLISH +--- +Posts a message to the given channel. + +In a Redis Cluster clients can publish to every node. The cluster makes sure +that published messages are forwarded as needed, so clients can subscribe to any +channel by connecting to any one of the nodes. + +## Return + +[Integer reply](/docs/reference/protocol-spec#resp-integers): the number of clients that received the message. Note that in a +Redis Cluster, only clients that are connected to the same node as the +publishing client are included in the count. diff --git a/docs/command-reference/set/index.md b/docs/command-reference/set/index.md new file mode 100644 index 00000000..fa9de028 --- /dev/null +++ b/docs/command-reference/set/index.md @@ -0,0 +1,163 @@ +--- +acl_categories: +- '@write' +- '@string' +- '@slow' +arguments: +- key_spec_index: 0 + name: key + type: key +- name: value + type: string +- arguments: + - name: nx + token: NX + type: pure-token + - name: xx + token: XX + type: pure-token + name: condition + optional: true + since: 2.6.12 + type: oneof +- name: get + optional: true + since: 6.2.0 + token: GET + type: pure-token +- arguments: + - name: seconds + since: 2.6.12 + token: EX + type: integer + - name: milliseconds + since: 2.6.12 + token: PX + type: integer + - name: unix-time-seconds + since: 6.2.0 + token: EXAT + type: unix-time + - name: unix-time-milliseconds + since: 6.2.0 + token: PXAT + type: unix-time + - name: keepttl + since: 6.0.0 + token: KEEPTTL + type: pure-token + name: expiration + optional: true + type: oneof +arity: -3 +command_flags: +- write +- denyoom +complexity: O(1) +description: Set the string value of a key +github_branch: main +github_path: /tmp/docs/content/commands/set/index.md +github_repo: https://dragonflydb/redis-doc +group: string +hidden: false +history: +- - 2.6.12 + - Added the `EX`, `PX`, `NX` and `XX` options. +- - 6.0.0 + - Added the `KEEPTTL` option. +- - 6.2.0 + - Added the `GET`, `EXAT` and `PXAT` option. +- - 7.0.0 + - Allowed the `NX` and `GET` options to be used together. +key_specs: +- RW: true + access: true + begin_search: + spec: + index: 1 + type: index + find_keys: + spec: + keystep: 1 + lastkey: 0 + limit: 0 + type: range + notes: RW and ACCESS due to the optional `GET` argument + update: true + variable_flags: true +linkTitle: SET +since: 1.0.0 +summary: Set the string value of a key +syntax_fmt: "SET key value [NX | XX] [GET] [EX\_seconds | PX\_milliseconds |\n EXAT\_\ + unix-time-seconds | PXAT\_unix-time-milliseconds | KEEPTTL]" +syntax_str: "value [NX | XX] [GET] [EX\_seconds | PX\_milliseconds | EXAT\_unix-time-seconds\ + \ | PXAT\_unix-time-milliseconds | KEEPTTL]" +title: SET +--- +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. +* `EXAT` *timestamp-seconds* -- Set the specified Unix time at which the key will expire, in seconds. +* `PXAT` *timestamp-milliseconds* -- Set the specified Unix time at which the key will expire, 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. +* `GET` -- Return the old string stored at key, or nil if key did not exist. An error is returned and `SET` aborted if the value stored at key is not a string. + +Note: Since the `SET` command options can replace [`SETNX`](/commands/setnx), [`SETEX`](/commands/setex), [`PSETEX`](/commands/psetex), [`GETSET`](/commands/getset), it is possible that in future versions of Redis these commands will be deprecated and finally removed. + +## Return + +[Simple string reply](/docs/reference/protocol-spec#resp-simple-strings): `OK` if `SET` was executed correctly. + +[Null reply](/docs/reference/protocol-spec#resp-bulk-strings): `(nil)` if the `SET` operation was not performed because the user specified the `NX` or `XX` option but the condition was not met. + +If the command is issued with the `GET` option, the above does not apply. It will instead reply as follows, regardless if the `SET` was actually performed: + +[Bulk string reply](/docs/reference/protocol-spec#resp-bulk-strings): the old string value stored at key. + +[Null reply](/docs/reference/protocol-spec#resp-bulk-strings): `(nil)` if the key did not exist. + +## 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](https://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`](/commands/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`](/commands/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/docs/command-reference/subscribe/index.md b/docs/command-reference/subscribe/index.md new file mode 100644 index 00000000..75706ccc --- /dev/null +++ b/docs/command-reference/subscribe/index.md @@ -0,0 +1,37 @@ +--- +acl_categories: +- '@pubsub' +- '@slow' +arguments: +- multiple: true + name: channel + type: string +arity: -2 +command_flags: +- pubsub +- noscript +- loading +- stale +complexity: O(N) where N is the number of channels to subscribe to. +description: Listen for messages published to the given channels +github_branch: main +github_path: /tmp/docs/content/commands/subscribe/index.md +github_repo: https://dragonflydb/redis-doc +group: pubsub +hidden: false +linkTitle: SUBSCRIBE +since: 2.0.0 +summary: Listen for messages published to the given channels +syntax_fmt: SUBSCRIBE channel [channel ...] +syntax_str: '' +title: SUBSCRIBE +--- +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`, [`SSUBSCRIBE`](/commands/ssubscribe), [`PSUBSCRIBE`](/commands/psubscribe), [`UNSUBSCRIBE`](/commands/unsubscribe), [`SUNSUBSCRIBE`](/commands/sunsubscribe), +[`PUNSUBSCRIBE`](/commands/punsubscribe), [`PING`](/commands/ping), [`RESET`](/commands/reset) and [`QUIT`](/commands/quit) commands. + +## Behavior change history + +* `>= 6.2.0`: [`RESET`](/commands/reset) can be called to exit subscribed state. \ No newline at end of file From c0cbbcd0abb09289a04f766ed01bf8200ecfc6d0 Mon Sep 17 00:00:00 2001 From: Vahagn Aharonian Date: Tue, 7 Feb 2023 22:57:29 +0500 Subject: [PATCH 3/3] Temporarily disable broken links check --- docusaurus.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index 3fee9a7a..2187066e 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -10,7 +10,7 @@ const config = { tagline: "Ultra-fast, scalable in-memory datastore", url: "https://dragonflydb.io", baseUrl: process.env.VERCEL_ENV === "preview" ? "/" : "/docs", - onBrokenLinks: "throw", + onBrokenLinks: "warn", onBrokenMarkdownLinks: "warn", favicon: "img/favicon.ico",