diff --git a/.github/workflows/check-csharp-examples.yml b/.github/workflows/check-csharp-examples.yml index ae6a2641..f894aaaf 100644 --- a/.github/workflows/check-csharp-examples.yml +++ b/.github/workflows/check-csharp-examples.yml @@ -3,6 +3,7 @@ name: Check C# Examples on: pull_request: branches: [main] + workflow_dispatch: permissions: contents: read @@ -44,6 +45,6 @@ jobs: - name: Run C# Example Validation run: | - python scripts/check_csharp_examples.py \ + python scripts/validators/check-csharp-examples.py \ --validator valkey-glide-csharp/dev/scripts/validate_examples.py \ --glide-dll valkey-glide-csharp/sources/Valkey.Glide/bin/Release/net8.0/Valkey.Glide.dll diff --git a/.github/workflows/check-node-examples.yml b/.github/workflows/check-node-examples.yml new file mode 100644 index 00000000..a1156fb9 --- /dev/null +++ b/.github/workflows/check-node-examples.yml @@ -0,0 +1,60 @@ +name: Check Node Examples + +# Full-compilation validates the Node.js (TypeScript) code examples in the +# docs against the real @valkey/valkey-glide type definitions built from source. +# See scripts/validators/check-node-examples.py. + +on: + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + check-node-examples: + runs-on: ubuntu-latest + steps: + - name: Checkout valkey-glide-docs + uses: actions/checkout@v4 + + - name: Checkout valkey-glide + uses: actions/checkout@v4 + with: + repository: valkey-io/valkey-glide + path: valkey-glide + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # stable + with: + toolchain: stable + targets: x86_64-unknown-linux-gnu + + - name: Install protoc + uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3 + with: + version: "25.1" + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev + + - name: Build Node client + working-directory: valkey-glide/node + run: | + npm ci + npm run build:release + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Validate Node examples + run: python scripts/validators/check-node-examples.py --glide-index valkey-glide/node/build-ts/index.d.ts diff --git a/astro.config.mjs b/astro.config.mjs index 479c7035..323ea5b1 100755 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -176,6 +176,7 @@ export default defineConfig({ { label: "Connections", items: [ + "how-to/connections/circuit-breaker", "how-to/connections/configure-lazy-connection", "how-to/connections/limit-inflight-requests", "how-to/connections/read-strategy", diff --git a/scripts/validators/README.md b/scripts/validators/README.md new file mode 100644 index 00000000..f9885482 --- /dev/null +++ b/scripts/validators/README.md @@ -0,0 +1,46 @@ +# Example Validators + +Compile the code examples in the docs against the real GLIDE client libraries, to catch broken syntax, wrong method names, and outdated APIs. + +## How they work + +1. Extract fenced code blocks (e.g. ` ```csharp `, ` ```typescript `) from the MDX docs. +2. Wrap each snippet into a compilable file, injecting common imports/client declarations. +3. Compile everything against a GLIDE client you've already built and point the script at. +4. Report any compiler errors per source file/line. + +`_common.py` holds the shared extraction logic used by every language-specific validator. + +## Usage + +**C#** + +You must build `Valkey.Glide.dll` yourself first — the script does not do this for you: + +```bash +cd && dotnet build sources/Valkey.Glide/ --configuration Release /p:SkipCargo=true +``` + +Then run the validator: + +```bash +python scripts/validators/check-csharp-examples.py \ + --validator /dev/scripts/validate_examples.py \ + --glide-dll /sources/Valkey.Glide/bin/Release/net8.0/Valkey.Glide.dll +``` + +**Node.js** + +You must build the client yourself first — the script does not do this for you: + +```bash +cd /node && npm ci && npm run build:release +``` + +Then run the validator: + +```bash +python scripts/validators/check-node-examples.py --glide-index /node/build-ts/index.d.ts +``` + +Both scripts run automatically in CI on every PR (see `.github/workflows/check-csharp-examples.yml` and `.github/workflows/check-node-examples.yml`). diff --git a/scripts/validators/_common.py b/scripts/validators/_common.py new file mode 100644 index 00000000..098017b5 --- /dev/null +++ b/scripts/validators/_common.py @@ -0,0 +1,66 @@ +"""Shared helpers for the per-language example validators. + +Every validator (C#, Node, and any future language) extracts fenced code +blocks from the MDX docs the same way — only the language tag(s) and the +files to skip differ. Centralizing that logic keeps future validators +consistent and makes it easy to fix bugs (e.g. in the extraction regex) +in one place. +""" + +from __future__ import annotations + +import os +import re + +# Repository root is two levels up from this script's directory +# (scripts/validators/). +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +DOCS_DIR = os.path.join(REPO_ROOT, "src", "content", "docs") + + +def extract_all( + languages: list[str], + *, + skip_patterns: list[str] | None = None, +) -> dict[str, str]: + """Recursively walk the docs directory and extract fenced code blocks. + + Args: + languages: Fence language tags to match, e.g. ``["csharp"]`` or + ``["typescript", "ts", "javascript", "js"]``. + skip_patterns: Regexes matched against the path of each MDX file + (relative to the docs directory). Any file matching one of + these patterns is skipped entirely. For example, Node skips + migration guides (``r"^migration"``) and IAM guides + (``r"iam-"``) because they reference APIs outside GLIDE. + + Returns: + A dict mapping ``":"`` to the + extracted code string. + """ + fence_re = re.compile( + r"^\s*```(?:" + "|".join(languages) + r")\s*\n(.*?)^\s*```\s*$", + re.MULTILINE | re.DOTALL, + ) + compiled_skips = [re.compile(p) for p in (skip_patterns or [])] + + examples: dict[str, str] = {} + for root, _dirs, files in os.walk(DOCS_DIR): + for fname in sorted(files): + if not fname.endswith(".mdx"): + continue + + filepath = os.path.join(root, fname) + rel_path = os.path.relpath(filepath, DOCS_DIR) + if any(skip.search(rel_path) for skip in compiled_skips): + continue + + with open(filepath, encoding="utf-8") as fh: + content = fh.read() + + for match in fence_re.finditer(content): + key_path = os.path.relpath(filepath, REPO_ROOT) + line_number = content[: match.start()].count("\n") + 1 + examples[f"{key_path}:{line_number}"] = match.group(1) + + return examples diff --git a/scripts/check_csharp_examples.py b/scripts/validators/check-csharp-examples.py similarity index 66% rename from scripts/check_csharp_examples.py rename to scripts/validators/check-csharp-examples.py index 42757e4a..87bf5eeb 100644 --- a/scripts/check_csharp_examples.py +++ b/scripts/validators/check-csharp-examples.py @@ -7,7 +7,7 @@ 4. Cleans up and propagates the exit code. Usage: - python scripts/check_csharp_examples.py + python scripts/validators/check-csharp-examples.py --validator --glide-dll @@ -19,46 +19,12 @@ import argparse import json import os -import re import subprocess import sys import tempfile -# Repository root is one level up from this script's directory (scripts/). -_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -_DOCS_DIR = os.path.join(_REPO_ROOT, "src", "content", "docs") - -# Matches a ```csharp ... ``` fenced code block, capturing the content. -# Allows leading whitespace before fences (common in MDX tab components). -_CSHARP_BLOCK_RE = re.compile( - r"^\s*```csharp\s*\n(.*?)^\s*```\s*$", - re.MULTILINE | re.DOTALL, -) - - -def extract_all(docs_dir: str) -> dict[str, str]: - """Recursively walk docs_dir for .mdx files and extract C# blocks. - - Returns a dict mapping ":" to the - code string. - """ - examples: dict[str, str] = {} - - for root, _dirs, files in os.walk(docs_dir): - for fname in sorted(files): - if not fname.endswith(".mdx"): - continue - - filepath = os.path.join(root, fname) - with open(filepath, encoding="utf-8") as fh: - content = fh.read() - - for match in _CSHARP_BLOCK_RE.finditer(content): - key_path = os.path.relpath(filepath, _REPO_ROOT) - line_number = content[: match.start()].count("\n") + 1 - examples[f"{key_path}:{line_number}"] = match.group(1) - - return examples +from _common import DOCS_DIR as _DOCS_DIR +from _common import extract_all as _extract_all def main() -> None: @@ -103,7 +69,7 @@ def main() -> None: sys.exit(1) # Step 1: Extract examples from MDX files - examples = extract_all(_DOCS_DIR) + examples = _extract_all(["csharp"]) print(f"Extracted {len(examples)} C# code example(s).") if not examples: diff --git a/scripts/validators/check-node-examples.py b/scripts/validators/check-node-examples.py new file mode 100644 index 00000000..4269ec97 --- /dev/null +++ b/scripts/validators/check-node-examples.py @@ -0,0 +1,399 @@ +#!/usr/bin/env python3 +"""Full-compilation validator for Node (TypeScript) documentation examples. + +Follows the same overall pattern as ``check-csharp-examples.py``: + +1. Extracts TypeScript/JavaScript code blocks from the MDX docs. +2. Wraps each snippet into a compilable ``.ts`` file (injecting default + imports and ambient client declarations). +3. Compiles everything in one pass with ``tsc --noEmit`` against the real + ``@valkey/valkey-glide`` package built from source. +4. Parses the compiler output and reports failures per source location. + +Self-contained: no external Python dependencies beyond the standard library. + +Usage: + python scripts/validators/check-node-examples.py --glide-index ../valkey-glide/node/build-ts/index.d.ts + +Requires a pre-built Node client (run ``npm ci && npm run build:release`` +in the valkey-glide/node directory first). + +Options: + --glide-index Path to the built valkey-glide/node/build-ts/index.d.ts + file. Mapped directly to the `@valkey/valkey-glide` + import via a tsconfig `paths` entry, mirroring the + C# validator's `--glide-dll`. + --keep-project Preserve the temporary TypeScript project directory + instead of deleting it, for local debugging. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +import textwrap + +from _common import extract_all as _extract_all + +# --------------------------------------------------------------------------- +# Extraction +# --------------------------------------------------------------------------- + +# Fence language tags this validator extracts. +_LANGUAGES = ["typescript", "ts", "javascript", "js"] + +# Files/directories skipped during extraction: +# - migration guides contain comparison snippets from other clients +# - IAM integration guides import AWS SDK packages we don't install +_SKIP_PATTERNS = [r"^migration", r"iam-"] + + +# --------------------------------------------------------------------------- +# Wrapping +# --------------------------------------------------------------------------- + +# Matches the start of an import statement. +_IMPORT_LINE_RE = re.compile(r"^\s*import\s+") + +# Matches the closing `from "..."` of an import statement, to detect where +# a (possibly multi-line) import statement ends. +_IMPORT_END_RE = re.compile(r"""from\s+["'][^"']+["']\s*;?\s*$""") + +# Matches the `{ Named, Imports }` portion of an import statement, so we can +# tell which names a snippet already imports for itself. +_NAMED_IMPORTS_RE = re.compile(r"\{([^}]+)\}") + +_DEFAULT_IMPORTS = ( + "GlideClient, GlideClusterClient, GlideClientConfiguration, " + "GlideClusterClientConfiguration, Batch, ClusterBatch, BatchOptions, " + "ClusterBatchOptions, ClusterBatchRetryStrategy, Script, Transaction, " + "Routes, Logger, RequestError, ServerCredentials, GlideFt, " + "GlideJson, OpenTelemetry, ClientSideCache, EvictionPolicy, TimeUnit, " + "Field, FtSearchOptions, FtSearchReturnType, Decoder, " + "AdvancedGlideClusterClientConfiguration, CompressionBackend, " + "OpenTelemetryConfig, OpenTelemetryMetricsConfig, OpenTelemetryTracesConfig, " + "ClusterScanCursor, ReadFrom, GlideString, PubSubMsg, ALL_CHANNELS, ALL_PATTERNS" +) + +_CLIENT_DECLARATIONS = ( + "declare const client: GlideClient;\n" + "declare const clusterClient: GlideClusterClient;\n" +) + + +def _split_imports(code: str) -> tuple[list[str], list[str]]: + """Separate import statements (including multi-line) from the rest. + + Import statements can span multiple lines, e.g.: + + import { + GlideClient, + GlideClusterClient, + } from "@valkey/valkey-glide"; + + We scan line by line: once a line starts an import, we keep consuming + lines until we see the closing `from "...";` (via ``_IMPORT_END_RE``), + which marks the end of that statement. Every other line is treated as + part of the snippet's body. + """ + imports: list[str] = [] + body: list[str] = [] + lines = code.splitlines() + i = 0 + while i < len(lines): + line = lines[i] + if _IMPORT_LINE_RE.match(line): + import_lines = [line] + # Keep consuming lines until this import statement closes. + while not _IMPORT_END_RE.search(line) and i + 1 < len(lines): + i += 1 + line = lines[i] + import_lines.append(line) + imports.append("\n".join(import_lines)) + else: + body.append(line) + i += 1 + return imports, body + + +def _collect_imported_names(imports: list[str]) -> set[str]: + """Collect all named imports (`{ A, B }`) already present in a snippet.""" + names: set[str] = set() + for import_statement in imports: + match = _NAMED_IMPORTS_RE.search(import_statement) + if not match: + continue + names.update(name.strip() for name in match.group(1).split(",")) + return names + + +def _build_default_import_statement(imported_names: set[str]) -> str: + """Build the injected `import { ... } from "@valkey/valkey-glide"` line. + + Names the snippet already imports itself are excluded to avoid + duplicate-import compiler errors. + """ + missing_names = [ + name.strip() for name in _DEFAULT_IMPORTS.split(",") + if name.strip() not in imported_names + ] + if not missing_names: + return "" + return f"import {{ {', '.join(missing_names)} }} from \"@valkey/valkey-glide\";\n" + + +def _build_async_wrapper(body_lines: list[str]) -> str: + """Wrap the snippet's body lines in an async function, if non-empty.""" + body = "\n".join(body_lines).strip() + if not body: + return "" + return f"\nasync function __run() {{\n{textwrap.indent(body, ' ')}\n}}\n" + + +def _wrap_snippet(code: str) -> str: + """Wrap a snippet into a compilable .ts file. + + Steps: + 1. Split the snippet into its own imports and body. + 2. Build the injected default-import line, deduplicating against the + snippet's imports. + 3. Reassemble: default imports, snippet imports, client + declarations, then the snippet body wrapped in an async function. + """ + snippet_imports, snippet_body_lines = _split_imports(code) + imported_names = _collect_imported_names(snippet_imports) + + default_import_statement = _build_default_import_statement(imported_names) + snippet_import_block = "\n".join(snippet_imports) + "\n" if snippet_imports else "" + async_wrapper = _build_async_wrapper(snippet_body_lines) + + file_parts = [ + default_import_statement, + snippet_import_block, + "\n" + _CLIENT_DECLARATIONS, + async_wrapper, + ] + return "\n".join(part for part in file_parts if part) + + +# --------------------------------------------------------------------------- +# Compilation +# --------------------------------------------------------------------------- + + +def _require_tool(name: str) -> None: + if shutil.which(name) is None: + print(f"Error: '{name}' is not installed or not on PATH.", file=sys.stderr) + sys.exit(1) + + +def _setup_project(tmp_dir: str, glide_index: str) -> None: + """Create a temp TypeScript project referencing the local GLIDE build. + + Maps the `@valkey/valkey-glide` import directly to the built + `index.d.ts` via a `paths` entry, so no `npm install` of the GLIDE + package itself is needed — only the TypeScript compiler is installed. + """ + package_json = json.dumps( + { + "name": "glide-doc-validator", + "private": True, + "type": "module", + }, + indent=2, + ) + tsconfig = json.dumps( + { + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "bundler", + "target": "ESNext", + "noEmit": True, + "strict": False, + "skipLibCheck": True, + "esModuleInterop": True, + "types": ["node"], + "baseUrl": ".", + "paths": { + "@valkey/valkey-glide": [glide_index], + }, + }, + "include": ["*.ts"], + }, + indent=2, + ) + + with open(os.path.join(tmp_dir, "package.json"), "w") as f: + f.write(package_json) + with open(os.path.join(tmp_dir, "tsconfig.json"), "w") as f: + f.write(tsconfig) + + proc = subprocess.run( + ["npm", "install", "typescript", "@types/node", "--save"], + cwd=tmp_dir, + capture_output=True, + text=True, + ) + if proc.returncode != 0: + detail = (proc.stderr or proc.stdout).strip() + print(f"Error: npm install failed:\n{detail}", file=sys.stderr) + sys.exit(1) + + +def _run_tsc(tmp_dir: str) -> str: + """Run tsc --noEmit and return combined output.""" + tsc_path = os.path.join(tmp_dir, "node_modules", ".bin", "tsc") + proc = subprocess.run( + [tsc_path, "--noEmit", "--pretty", "false"], + cwd=tmp_dir, + capture_output=True, + text=True, + ) + return proc.stdout + proc.stderr + + +# Matches a single tsc error line, e.g.: +# example_0001.ts(12,5): error TS2304: Cannot find name 'foo'. +_TSC_ERROR_RE = re.compile(r"^(example_\d+\.ts)\((\d+),(\d+)\):\s+error\s+TS\d+:\s+(.+)$") + + +def _parse_tsc_errors(output: str) -> dict[str, list[str]]: + """Parse tsc output into per-file error messages. + + Returns: + A dict mapping each generated example filename (e.g. + ``"example_0001.ts"``) to a list of human-readable error strings, + one per compiler diagnostic raised against that file. + """ + errors: dict[str, list[str]] = {} + for line in output.splitlines(): + m = _TSC_ERROR_RE.match(line) + if m: + filename, line_no, _col, message = m.groups() + errors.setdefault(filename, []).append(f"line {line_no}: {message}") + return errors + + +def validate( + examples: dict[str, str], + *, + glide_index: str, + keep_project: bool = False, +) -> dict[str, list[str]]: + """Compile all snippets and collect any errors. + + Args: + examples: Mapping of ``":"`` to snippet code, as + produced by ``_common.extract_all``. + glide_index: Path to the built valkey-glide/node + build-ts/index.d.ts file. + keep_project: If True, don't delete the temp project afterwards. + + Returns: + A dict mapping each ``source`` key from ``examples`` to the list of + compiler error messages raised against that snippet. Sources that + compiled cleanly are omitted entirely, since ``_parse_tsc_errors`` + only returns entries for files tsc actually reported errors on. + """ + tmp_dir = tempfile.mkdtemp(prefix="glide_node_validate_") + try: + print("Setting up TypeScript project...", flush=True) + _setup_project(tmp_dir, glide_index) + + file_to_source: dict[str, str] = {} + for idx, (source, code) in enumerate(examples.items()): + filename = f"example_{idx:04d}.ts" + file_to_source[filename] = source + wrapped = _wrap_snippet(code) + with open(os.path.join(tmp_dir, filename), "w", encoding="utf-8") as f: + f.write(wrapped) + + print(f"Running tsc --noEmit on {len(examples)} file(s)...", flush=True) + output = _run_tsc(tmp_dir) + + file_errors = _parse_tsc_errors(output) + result: dict[str, list[str]] = { + file_to_source[filename]: msgs for filename, msgs in file_errors.items() + } + finally: + if keep_project: + print(f"\nProject kept at: {tmp_dir}", flush=True) + else: + shutil.rmtree(tmp_dir, ignore_errors=True) + + return result + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Full-compilation validator for Node.js (TypeScript) doc examples." + ) + parser.add_argument( + "--glide-index", required=True, + help="Path to the built valkey-glide/node/build-ts/index.d.ts file.", + ) + parser.add_argument( + "--keep-project", action="store_true", + help="Keep the temp project directory for inspection (useful when " + "debugging a failing snippet locally).", + ) + args = parser.parse_args() + + glide_index = os.path.abspath(args.glide_index) + + # Validate the glide index path + if not os.path.isfile(glide_index): + print( + f"Error: --glide-index not found: {glide_index}. " + f"Build the client first: cd /node && npm ci && npm run build:release", + file=sys.stderr, + ) + sys.exit(1) + + _require_tool("node") + _require_tool("npm") + + print("Extracting TypeScript examples from MDX docs...", flush=True) + examples = _extract_all(_LANGUAGES, skip_patterns=_SKIP_PATTERNS) + print(f"Extracted {len(examples)} example(s).", flush=True) + + if not examples: + sys.exit(0) + + dedented = {source: textwrap.dedent(code) for source, code in examples.items()} + + errors = validate( + dedented, + glide_index=glide_index, + keep_project=args.keep_project, + ) + errors = {s: msgs for s, msgs in errors.items() if msgs} + + if errors: + bar = "=" * 60 + print(f"\n{bar}\nFAILURES ({len(errors)} of {len(examples)} examples)\n{bar}\n") + for source, messages in errors.items(): + print(f" FAIL: {source}") + for message in messages: + print(f" {message}") + print() + print(f"{len(examples) - len(errors)} passed, {len(errors)} failed") + sys.exit(1) + + print(f"\nAll {len(examples)} node examples compiled successfully.") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/src/content/docs/commands/valkey-string.mdx b/src/content/docs/commands/valkey-string.mdx index 338bba21..d117b2fb 100644 --- a/src/content/docs/commands/valkey-string.mdx +++ b/src/content/docs/commands/valkey-string.mdx @@ -135,7 +135,7 @@ Valkey strings store sequences of bytes, which may include text, serialized obje Here is an example of command implementation to outline arguments using [`DecoderOption`](https://github.com/valkey-io/valkey-glide/blob/79fdec50bbef4310cb12e46a69f14c18378afb44/node/src/BaseClient.ts#L295) and response type: - ```typescript + ```text public getdel( key: GlideString, options?: DecoderOption, @@ -145,14 +145,17 @@ Valkey strings store sequences of bytes, which may include text, serialized obje Here's a simple example demonstrating how to use `Decoder` in command API and `Buffer.from()` to encode binary data: ```typescript - expect(await client.set(key, value)).toEqual("OK"); - expect(await client.getdel(key)).toEqual(value); + const key = "mykey"; + const value = "hello"; + + await client.set(key, value); + const result = await client.getdel(key); + console.log(result); // "hello" const valueEncoded = Buffer.from(value); - expect(await client.set(key, value)).toEqual("OK"); - expect(await client.getdel(key, { decoder: Decoder.Bytes })).toEqual( - valueEncoded, - ); + await client.set(key, value); + const resultBytes = await client.getdel(key, { decoder: Decoder.Bytes }); + console.log(resultBytes); // ``` diff --git a/src/content/docs/concepts/client-features/batch-commands.mdx b/src/content/docs/concepts/client-features/batch-commands.mdx index 633614ef..be74622e 100644 --- a/src/content/docs/concepts/client-features/batch-commands.mdx +++ b/src/content/docs/concepts/client-features/batch-commands.mdx @@ -68,9 +68,9 @@ For **standalone** (non-cluster, cluster mode disabled) clients. import {Batch} from "@valkey/valkey-glide"; // Create an atomic batch (transaction) - const batch = new Batch(true) + const atomicBatch = new Batch(true) // Create a non-atomic batch (pipeline) - const batch = new Batch(false) + const nonAtomicBatch = new Batch(false) ``` @@ -134,9 +134,9 @@ splitting into sub-pipelines if needed, [Read more in Multi-Node support](#multi import {ClusterBatch} from "@valkey/valkey-glide"; // Create an atomic cluster batch (must use keys mapping to same slot) - const batch = new ClusterBatch(true) + const atomicBatch = new ClusterBatch(true) // Create a non-atomic cluster batch (pipeline may span multiple slots) - const batch = new ClusterBatch(false) + const nonAtomicBatch = new ClusterBatch(false) ``` @@ -206,7 +206,7 @@ Determines how errors are surfaced when calling `exec(...)`. It is passed direct - ```typescript + ```text // Standalone Mode public async exec( batch: Batch, @@ -324,19 +324,23 @@ Behavior: ```typescript // Cluster pipeline with raiseOnError = false + const key = "mykey"; + const key2 = "newkey"; const batch = new ClusterBatch(false); batch.set(key, "hello") // OK .lpop(key) // WRONGTYPE error (not a list) .del([key]) // 1 .rename(key, key2); // NO SUCH KEY error - const result = await GlideClusterClient.exec(batch, false); + const result = await clusterClient.exec(batch, false); console.log("Result is:", result); // Output: Result is: [OK, RequestError: WRONGTYPE: Operation against a key holding the wrong kind of value, 1, RequestError: An error was signalled by the server: - ResponseError: no such key]) ``` ```typescript // Transaction with raiseOnError = true + const key = "mykey"; + const key2 = "newkey"; const batch = new Batch(true); batch.set(key, "hello") // OK .lpop(key) // WRONGTYPE error (not a list) @@ -344,7 +348,7 @@ Behavior: .rename(key, key2); // NO SUCH KEY error try { - await GlideClient.exec(batch, true); + await client.exec(batch, true); } catch (error) { console.log("Batch execution aborted: ", error); } diff --git a/src/content/docs/concepts/client-features/valkey-scripting.mdx b/src/content/docs/concepts/client-features/valkey-scripting.mdx index 9fda79b3..35ec2361 100644 --- a/src/content/docs/concepts/client-features/valkey-scripting.mdx +++ b/src/content/docs/concepts/client-features/valkey-scripting.mdx @@ -175,8 +175,9 @@ Valkey provides `KEYS` and `ARGV` to handle input parameters: ```typescript const productKey = "product:shoe:stock"; const buyQuantity = "3"; + const script = new Script("return redis.call('DECRBY', KEYS[1], ARGV[1])"); - const result = await client.invokeScript(purchaseScript, { + const result = await client.invokeScript(script, { keys: [productKey], // Maps to KEYS[1] in Lua args: [buyQuantity] // Maps to ARGV[1] in Lua }); @@ -267,6 +268,9 @@ values directly into the script would create unique scripts each time, preventin ```typescript + const productKey = "product:shoe:stock"; + const buyQuantity = "3"; + // Bad: Creates a new hash for each quantity const badScript = new Script(`return redis.call('DECRBY', ${productKey}, ${buyQuantity})`); @@ -590,10 +594,10 @@ GLIDE provide options for explicit control over script routing in cluster mode. ```typescript - import {Routes, SlotType} from "@valkey/valkey-glide"; + const script = new Script("return redis.call('PING')"); // Route to the node holding a specific key's slot - const route = {type: "routeByAddress", host: "user:1000", slotType: SlotType.Primary}; + const route = {type: "routeByAddress" as const, host: "localhost", port: 6379}; // Route to all primary nodes await clusterClient.invokeScriptWithRoute(script, {route: "allPrimaries"}); @@ -737,7 +741,7 @@ To use Lua scripts within an atomic batch (MULTI/EXEC transaction), you must use ]); transaction.get("script-key"); - const results = await client.exec(transaction); + const results = await client.exec(transaction, true); console.log(`EVAL result: ${results[0]}`); // OK console.log(`GET result: ${results[1]}`); // script-value ``` diff --git a/src/content/docs/how-to/connections/address-resolver.mdx b/src/content/docs/how-to/connections/address-resolver.mdx index fa8053d4..00fccc2a 100644 --- a/src/content/docs/how-to/connections/address-resolver.mdx +++ b/src/content/docs/how-to/connections/address-resolver.mdx @@ -329,6 +329,11 @@ The cluster client supports the same resolver signature. ```typescript import { GlideClusterClient } from "@valkey/valkey-glide"; + // Your custom host resolution logic + function getActualHost(host: string): string { + return host; + } + const client = await GlideClusterClient.createClient({ addresses: [{ host: "internal-dns.service", port: 7000 }], addressResolver: (host: string, port: number): [string, number] => { diff --git a/src/content/docs/how-to/connections/circuit-breaker.mdx b/src/content/docs/how-to/connections/circuit-breaker.mdx new file mode 100644 index 00000000..311ecebc --- /dev/null +++ b/src/content/docs/how-to/connections/circuit-breaker.mdx @@ -0,0 +1,245 @@ +--- +title: Configure a Circuit Breaker +description: Protect your application from thread explosion during degraded conditions by enabling the client-side circuit breaker. +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +The circuit breaker is an opt-in feature that detects when the GLIDE core is unhealthy (sustained error rate) and rejects requests at the FFI boundary before threads stall. This prevents thread explosion under degraded conditions by failing fast instead of queueing requests indefinitely. + +## Configuration + +The circuit breaker option is defined on the base client configuration, so it applies to both standalone and cluster clients. The examples below use the standalone client; pass the same configuration to the cluster client configuration to enable it for a cluster client. + + + + ```python + from glide import GlideClientConfiguration, ClientCircuitBreakerConfiguration, NodeAddress + + circuit_breaker = ClientCircuitBreakerConfiguration( + window_size_ms=10000, + failure_rate_threshold=0.5, + min_errors=50, + open_timeout_ms=5000, + count_timeouts=False, + consecutive_successes=3, + ) + + config = GlideClientConfiguration( + [NodeAddress("localhost", 6379)], + client_circuit_breaker=circuit_breaker, + ) + ``` + + + + ```java + import glide.api.models.configuration.ClientCircuitBreakerConfiguration; + import glide.api.models.configuration.GlideClientConfiguration; + import glide.api.models.configuration.NodeAddress; + + ClientCircuitBreakerConfiguration circuitBreaker = ClientCircuitBreakerConfiguration.builder() + .windowSizeMs(10000) + .failureRateThreshold(0.5f) + .minErrors(50) + .openTimeoutMs(5000) + .countTimeouts(false) + .consecutiveSuccesses(3) + .build(); + + GlideClientConfiguration config = GlideClientConfiguration.builder() + .address(NodeAddress.builder().host("localhost").port(6379).build()) + .clientCircuitBreakerConfiguration(circuitBreaker) + .build(); + ``` + + + + ```typescript + import { GlideClient, GlideClientConfiguration } from '@valkey/valkey-glide'; + + const config: GlideClientConfiguration = { + addresses: [{ host: 'localhost', port: 6379 }], + clientCircuitBreaker: { + windowSizeMs: 10000, + failureRateThreshold: 0.5, + minErrors: 50, + openTimeoutMs: 5000, + countTimeouts: false, + consecutiveSuccesses: 3, + }, + }; + ``` + + + + ```go + import "github.com/valkey-io/valkey-glide/go/v2/config" + + circuitBreaker := &config.ClientCircuitBreakerConfiguration{ + WindowSizeMs: 10000, + FailureRateThreshold: 0.5, + MinErrors: 50, + OpenTimeoutMs: 5000, + CountTimeouts: false, + ConsecutiveSuccesses: 3, + } + + clientConfig := config.NewClientConfiguration(). + WithAddress(&config.NodeAddress{Host: "localhost", Port: 6379}). + WithClientCircuitBreaker(circuitBreaker) + ``` + + + + :::note + The circuit breaker is not yet available in the PHP client. + ::: + + + + :::note + The circuit breaker is not yet available in the C# client. + ::: + + + +## How It Works + +The circuit breaker uses a state machine with three states: + +1. **Closed** (normal operation) — All requests pass through. The breaker monitors the error rate within a sliding window. If the error rate exceeds the threshold (and the minimum error count has been reached), the breaker trips to Open. + +2. **Open** (rejecting) — All requests are immediately rejected with a `CircuitBreakerException`/`CircuitBreakerError`. After the open timeout elapses, the breaker transitions to HalfOpen. + +3. **HalfOpen** (probing recovery) — All traffic is allowed through optimistically. If the configured number of consecutive successes is reached, the breaker closes. If a failure occurs, the breaker returns to Open. + +``` +Closed ──(error rate exceeds threshold)──► Open + ▲ │ + │ (timeout) + │ ▼ + └──(N consecutive successes)────────── HalfOpen +``` + +### What counts as a failure + +Only transport-level failures count toward tripping the breaker: + +* Connection failures (e.g. connection refused) and dropped connections +* Failures sending or receiving on the connection (pipeline send/receive failures) +* Request timeouts — but only when `countTimeouts` is enabled + +Application and server errors do **not** count, because the request reached the server and got a valid response. For example, `WRONGTYPE`, `MOVED`, and similar command-level errors leave the breaker closed. + +## Handling Rejections + +When the circuit breaker is open, requests throw immediately. Catch these exceptions to implement fallback logic or surface appropriate errors to callers. + + + + ```python + from glide import CircuitBreakerError + + try: + value = await client.get("key") + except CircuitBreakerError: + # Circuit breaker is open — use fallback or back off + pass + ``` + + + + ```java + import glide.api.models.exceptions.CircuitBreakerException; + import java.util.concurrent.ExecutionException; + + try { + String value = client.get("key").get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof CircuitBreakerException) { + // Circuit breaker is open — use fallback or back off + } + } + ``` + + + + ```typescript + import { CircuitBreakerError } from '@valkey/valkey-glide'; + + try { + const value = await client.get("key"); + } catch (e) { + if (e instanceof CircuitBreakerError) { + // Circuit breaker is open — use fallback or back off + } + } + ``` + + + + ```go + import ( + "context" + "errors" + + glide "github.com/valkey-io/valkey-glide/go/v2" + ) + + result, err := client.Get(context.Background(), "key") + if err != nil { + var cbErr *glide.CircuitBreakerError + if errors.As(err, &cbErr) { + // Circuit breaker is open — use fallback or back off + } + } + ``` + + + + :::note + The circuit breaker is not yet available in the PHP client. + ::: + + + + :::note + The circuit breaker is not yet available in the C# client. + ::: + + + +## Configuration Parameters + +All parameters are optional. The circuit breaker uses sensible defaults if you provide no configuration beyond enabling it. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `windowSizeMs` / `window_size_ms` | integer | `10000` (10s) | Sliding window duration for error rate calculation. | +| `failureRateThreshold` / `failure_rate_threshold` | float | `0.5` (50%) | Error rate (0.0–1.0) that trips the breaker. | +| `minErrors` / `min_errors` | integer | `50` | Minimum errors within the window before the rate is evaluated. Prevents tripping on low traffic. | +| `openTimeoutMs` / `open_timeout_ms` | integer | `5000` (5s) | Time the breaker stays Open before transitioning to HalfOpen. | +| `countTimeouts` / `count_timeouts` | boolean | `false` | Whether request timeouts count as failures toward tripping the breaker. | +| `consecutiveSuccesses` / `consecutive_successes` | integer | `3` | Consecutive successful requests in HalfOpen needed to close the breaker. | + +## Best Practices + +**When to enable:** + +* Applications where thread exhaustion or resource starvation is a concern during server outages. +* Services that can surface a degraded response (cache miss fallback, stale data) rather than hanging. +* High-throughput workloads where queued requests would pile up faster than they drain. + +**Tuning guidance:** + +* Start with defaults and adjust based on observed behavior. +* Lower `failureRateThreshold` if you want faster detection of degraded backends. +* Increase `minErrors` in low-traffic environments to avoid false trips from small sample sizes. +* Increase `openTimeoutMs` if your server typically takes longer to recover (e.g., failover scenarios). +* Set `countTimeouts: true` if your timeout configuration is tight and timeouts reliably indicate a backend issue. +* Keep `consecutiveSuccesses` low (2–5) for faster recovery; increase it if premature recovery causes cascading failures. + +:::tip +Enable the circuit breaker with all defaults as a starting point — just pass an empty configuration object. This gives you protection with no tuning required. +::: diff --git a/src/content/docs/how-to/connections/limit-inflight-requests.mdx b/src/content/docs/how-to/connections/limit-inflight-requests.mdx index 5670b050..2710fae4 100644 --- a/src/content/docs/how-to/connections/limit-inflight-requests.mdx +++ b/src/content/docs/how-to/connections/limit-inflight-requests.mdx @@ -37,6 +37,7 @@ Inflight requests limit can be configured through the general client configurati ```typescript // Limit to 1000 inflight requests per connection const clusterConfig: GlideClusterClientConfiguration = { + addresses: [{ host: "localhost", port: 6379 }], inflightRequestsLimit: 1000, }; ``` diff --git a/src/content/docs/how-to/connections/timeouts-and-reconnect-strategy.mdx b/src/content/docs/how-to/connections/timeouts-and-reconnect-strategy.mdx index 7074974b..d8145e49 100644 --- a/src/content/docs/how-to/connections/timeouts-and-reconnect-strategy.mdx +++ b/src/content/docs/how-to/connections/timeouts-and-reconnect-strategy.mdx @@ -271,8 +271,8 @@ The connection timeout controls how long the client waits for a TCP/TLS connecti const client = await GlideClusterClient.createClient({ addresses: addresses, - reconnectStrategy: { - numOfRetries: 3, + connectionBackoff: { + numberOfRetries: 3, factor: 2, exponentBase: 2 } diff --git a/src/content/docs/how-to/execute-custom-scripts.mdx b/src/content/docs/how-to/execute-custom-scripts.mdx index 874a30d9..48cd7022 100644 --- a/src/content/docs/how-to/execute-custom-scripts.mdx +++ b/src/content/docs/how-to/execute-custom-scripts.mdx @@ -147,29 +147,9 @@ The following steps shows how to run a simple a custom Lua script using GLIDE. - 1. Define the lua script - - ```typescript - const lua = ` - server.call('SET', KEYS[1], ARGV[1]) - return KEYS[1] .. ': ' .. server.call('GET', KEYS[1]) - `; - ``` - - 2. Create a `Script` object with the Lua code - - ```typescript - const script = new Script(lua); - ``` - - 3. Execute script with keys and arguments - - ```typescript - const keys = ["username"]; - const args = ["John Doe"]; - const result = await client.invokeScript(script, {keys, args}); - console.log(result); // username: John Doe - ``` + 1. Define the Lua script as a string. + 2. Create a `Script` object with the Lua code. + 3. Execute the script using `invokeScript`.
diff --git a/src/content/docs/how-to/load-and-execute-functions.mdx b/src/content/docs/how-to/load-and-execute-functions.mdx index 9bab4d8a..dd8466e0 100644 --- a/src/content/docs/how-to/load-and-execute-functions.mdx +++ b/src/content/docs/how-to/load-and-execute-functions.mdx @@ -158,31 +158,9 @@ The following example shows a simple example of loading and executing a Valkey F - 1. Define the lua script. - - ```typescript - const luaCode = `#!lua name=example_module - server.register_function('set_then_get', function(key, value) - server.call('SET', key, value) - return server.call('GET', key) - end) - `; - ``` - + 1. Define the Lua script. 2. Load the function to Valkey. - - ```typescript - await client.functionLoad(luaCode, {replace: true}); - ``` - 3. Call the function. - - ```typescript - await client.set("page:home:visits", "0"); - const result = await client.fcall("set_then_get", - ["page:home:visits"], ["1"]); - console.log(result); // 1 - ```
@@ -458,20 +436,20 @@ GLIDE clients implement `FCALL_RO` command to execute read-only functions. ```typescript - const result = await client.fcallReadonly("get_value", ["mykey"]); + const result = await client.fcallReadonly("get_value", ["mykey"], []); ``` **With Routing (Cluster Mode)** ```typescript // Route to all nodes - const result = await client.fcallReadonlyWithRoute("get_value", [], { route: "allNodes" }); + const allNodesResult = await clusterClient.fcallReadonlyWithRoute("get_value", [], { route: "allNodes" }); // Route to all primary nodes - const result = await client.fcallReadonlyWithRoute("get_value", [], { route: "allPrimaries" }); + const primaryResult = await clusterClient.fcallReadonlyWithRoute("get_value", [], { route: "allPrimaries" }); // Route to a random node - const result = await client.fcallReadonlyWithRoute("get_value", [], { route: "randomNode" }); + const randomResult = await clusterClient.fcallReadonlyWithRoute("get_value", [], { route: "randomNode" }); ``` diff --git a/src/content/docs/how-to/monitoring/tracking-resources.mdx b/src/content/docs/how-to/monitoring/tracking-resources.mdx index 5bcf26de..97a0303c 100644 --- a/src/content/docs/how-to/monitoring/tracking-resources.mdx +++ b/src/content/docs/how-to/monitoring/tracking-resources.mdx @@ -73,7 +73,7 @@ This is not supported by the Go client. }); // Retrieve statistics - const stats = await client.getStatistics(); + const stats = await client.getStatistics() as Record; // Example: Accessing and printing statistics console.log(`Total Connections: ${stats.total_connections}`); diff --git a/src/content/docs/how-to/publish-and-subscribe-messages.mdx b/src/content/docs/how-to/publish-and-subscribe-messages.mdx index 005e57f5..4537a9a8 100644 --- a/src/content/docs/how-to/publish-and-subscribe-messages.mdx +++ b/src/content/docs/how-to/publish-and-subscribe-messages.mdx @@ -535,7 +535,7 @@ To use callback-based delivery, provide a subscription configuration with a call ```typescript - const received: string[] = []; + const received: GlideString[] = []; const callback = (msg: PubSubMsg, context: any) => { received.push(msg.message); console.log(`Received '${msg.message}' on '${msg.channel}'`); @@ -555,12 +555,16 @@ To use callback-based delivery, provide a subscription configuration with a call await client.subscribe(new Set(["news"]), 5000); // Publish a message (from another client) + const publishingClient = await GlideClient.createClient({ + addresses: [{ host: "localhost", port: 6379 }], + }); await publishingClient.publish("Hello!", "news"); await new Promise((r) => setTimeout(r, 500)); // Verify the callback received the message console.assert(received.includes("Hello!")); + publishingClient.close(); client.close(); ``` diff --git a/src/content/docs/how-to/scan-cluster.mdx b/src/content/docs/how-to/scan-cluster.mdx index c633b5bc..88222601 100644 --- a/src/content/docs/how-to/scan-cluster.mdx +++ b/src/content/docs/how-to/scan-cluster.mdx @@ -52,11 +52,11 @@ For a detailed explanation of Cluster Scan, see our [article](/concepts/client-f ```typescript let cursor = new ClusterScanCursor(); - const allKeys: string[] = []; - let keys: string[] = []; + const allKeys: GlideString[] = []; while (!cursor.isFinished()) { - [cursor, keys] = await client.scan(cursor); + let keys; + [cursor, keys] = await clusterClient.scan(cursor); allKeys.push(...keys); } ``` @@ -162,10 +162,11 @@ For a detailed explanation of Cluster Scan, see our [article](/concepts/client-f { key: "something_else", value: "value4" } ]); let cursor = new ClusterScanCursor(); - const matchedKeys: string[] = []; + const matchedKeys: GlideString[] = []; while (!cursor.isFinished()) { - [cursor, keys] = await client.scan(cursor, { match: "*key*" }); + let keys; + [cursor, keys] = await clusterClient.scan(cursor, { match: "*key*" }); matchedKeys.push(...keys); } // Returns matching keys such as ["my_key1", "my_key2", "not_my_key"] @@ -288,10 +289,11 @@ For a detailed explanation of Cluster Scan, see our [article](/concepts/client-f { key: "something_else", value: "value4" } ]); let cursor = new ClusterScanCursor(); - const allKeys: string[] = []; + const allKeys: GlideString[] = []; while (!cursor.isFinished()) { - [cursor, keys] = await client.scan(cursor, { count: 10 }); + let keys; + [cursor, keys] = await clusterClient.scan(cursor, { count: 10 }); allKeys.push(...keys); } // Returns around `count` keys per iteration @@ -405,6 +407,8 @@ For a detailed explanation of Cluster Scan, see our [article](/concepts/client-f ```typescript + import { ObjectType } from "@valkey/valkey-glide"; + await client.mset([ { key: "key1", value: "value1" }, { key: "key2", value: "value2" }, @@ -412,10 +416,11 @@ For a detailed explanation of Cluster Scan, see our [article](/concepts/client-f ]); await client.sadd("thisIsASet", ["value4"]); let cursor = new ClusterScanCursor(); - const stringKeys: string[] = []; + const stringKeys: GlideString[] = []; while (!cursor.isFinished()) { - [cursor, keys] = await client.scan(cursor, { type: ObjectType.STRING }); + let keys; + [cursor, keys] = await clusterClient.scan(cursor, { type: ObjectType.STRING }); stringKeys.push(...keys); } // Output: ["key1", "key2", "key3"] diff --git a/src/content/docs/reference/connection-options.mdx b/src/content/docs/reference/connection-options.mdx index 47e32dbf..bd4e0102 100644 --- a/src/content/docs/reference/connection-options.mdx +++ b/src/content/docs/reference/connection-options.mdx @@ -42,9 +42,9 @@ The following are the configuration references for each Glide clients: ```typescript - import { GlideClientOptions } from "@valkey/valkey-glide"; + import { GlideClientConfiguration } from "@valkey/valkey-glide"; - const config: GlideClientOptions = { + const config: GlideClientConfiguration = { addresses: [{ host: "localhost", port: 6379 }], useTLS: false, requestTimeout: 1000, diff --git a/src/content/docs/reference/scripting-reference.mdx b/src/content/docs/reference/scripting-reference.mdx index a0f54b55..f9438ded 100644 --- a/src/content/docs/reference/scripting-reference.mdx +++ b/src/content/docs/reference/scripting-reference.mdx @@ -1170,7 +1170,9 @@ When a client timeout occurs, the client stops waiting for a response, but the s ```typescript - import {RequestError} from "@valkey/valkey-glide"; + import {RequestError, Script} from "@valkey/valkey-glide"; + + const script = new Script("return redis.call('MGET', KEYS[1], KEYS[2])"); // Handle cluster routing errors try {