rustis 0.21.0

Redis async driver for Rust
Documentation
name: Build and Test

on:
  push:
    paths:
      - "src/**"
      - "redis/**"
      - ".github/workflows/**"
      - "Cargo.toml"
      - "Cargo.lock"
    branches:
      - "**"
    tags-ignore:
      - "*.*.*"
  pull_request:
    paths:
      - "src/**"
      - ".github/workflows/**"
      - "Cargo.toml"
      - "Cargo.lock"
    branches:
      - "**"

jobs:
  check:
    name: Check
    runs-on: ubuntu-22.04
    timeout-minutes: 20
    env:
      CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
    steps:
      - name: Checkout sources
        uses: actions/checkout@v5
      - name: Run cargo check
        run: cargo check
      - name: Run cargo check --release
        run: cargo check --release
      - name: Checking style
        run: cargo fmt --all -- --check
      # Broken intra-doc links compile fine and only show up as dead links on
      # docs.rs, so nothing here caught them until this step existed. Denying
      # rustdoc warnings also covers links from public docs into private items,
      # which promise the reader a page that will never be rendered.
      - name: Checking documentation
        env:
          RUSTDOCFLAGS: -D warnings
        run: cargo doc --no-deps --no-default-features --features tokio-runtime,tokio-rustls,pool,json,client-cache

  # Compile with the exact toolchain `rust-version` promises, so the promise is
  # verified rather than asserted. The version is read from `Cargo.toml` and not
  # repeated here: one source of truth, and raising the floor stays a one-line
  # manifest change.
  #
  # `cargo check` rather than `--all-targets`: the MSRV covers the library a
  # downstream crate compiles, not the dev-dependencies of our own test suite,
  # which are free to require a newer compiler.
  #
  # Cargo's v3 resolver — the default in edition 2024 — reads `rust-version`
  # when picking dependency versions, so the committed `Cargo.lock` is
  # MSRV-compatible by construction: a dependency that raises its own MSRV above
  # 1.88 is resolved away when the lock is written, not discovered here.
  msrv:
    name: MSRV
    runs-on: ubuntu-22.04
    timeout-minutes: 20
    env:
      CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
    steps:
      - name: Checkout sources
        uses: actions/checkout@v5
      - name: Read the MSRV from Cargo.toml
        id: msrv
        run: |
          VERSION=$(sed -n 's/^rust-version *= *"\(.*\)"/\1/p' Cargo.toml)
          test -n "$VERSION" || { echo "no rust-version in Cargo.toml"; exit 1; }
          echo "declared MSRV: $VERSION"
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
      - name: Install the declared toolchain
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ steps.msrv.outputs.version }}
      - name: cargo check
        run: cargo check --no-default-features --features tokio-runtime,tokio-rustls,pool,json,client-cache

  # Enforce the panic policy declared in `src/lib.rs`: the explicit-panic family
  # and `clippy::arithmetic_side_effects` crate-wide, plus
  # `clippy::indexing_slicing` in `network/` and `resp/`. Those
  # are `deny`, so they already fail without `-D warnings`; the flag is what
  # makes an *unfulfilled* `#[expect(...)]` fail too, so a justification that no
  # longer applies gets removed instead of quietly outliving its reason.
  clippy:
    name: Clippy (panic policy)
    runs-on: ubuntu-22.04
    timeout-minutes: 20
    env:
      CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
    steps:
      - name: Checkout sources
        uses: actions/checkout@v5
      - name: cargo clippy --all-targets
        run: cargo clippy --all-targets --no-default-features --features tokio-runtime,tokio-rustls,pool,json,client-cache -- -D warnings

  # Check that the version bump in `Cargo.toml` matches what actually changed in
  # the public API. In 0.x, cargo treats the minor field as the major one, so a
  # breaking change requires 0.(x+1).0 and a compatible one 0.x.(y+1) — this job
  # fails when the bump is smaller than the changes justify.
  #
  # It compares against the previous tag rather than crates.io: no dependency on
  # the registry, and it works on a branch. Pull requests only, because it builds
  # the crate twice (baseline and current).
  #
  # Its blind spot is worth stating: it reads the *shape* of the public API from
  # rustdoc, so it cannot see a behavior change. A function that keeps its
  # signature but returns a different value passes this job silently. Those
  # belong in `CHANGELOG.md`, written by hand. It also misses some shape changes
  # — the 0.20.0 run did not flag `Command::name()` switching its return type
  # from `Bytes` to `&[u8]`.
  #
  # One-time caveat: tags up to 0.19.3 carry the placeholder `version = "0.1.0"`
  # in their manifest, so against those the tool reads a 0.1.0 -> 0.20.0 jump,
  # calls it a major release and skips every lint. Until 0.20.0 is tagged this
  # job passes vacuously; a real check needs `--release-type minor` locally. From
  # 0.20.0 onwards the baseline manifest is correct and inference works.
  semver:
    name: Semver compatibility
    runs-on: ubuntu-22.04
    timeout-minutes: 30
    if: github.event_name == 'pull_request'
    env:
      CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
    steps:
      - name: Checkout sources
        uses: actions/checkout@v5
        with:
          # Tags are needed to resolve the baseline revision.
          fetch-depth: 0
      - name: Install cargo-semver-checks
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-semver-checks
      # The feature set is the one docs.rs builds, not the default one. Only the
      # features named here have their public surface compared, so `--default-
      # features` — tokio-runtime alone — left `pool`, `json`, `client-cache` and
      # the TLS surface unchecked: three real breaks in 0.20.0 (`Cache::
      # zremrangebyscore`, `TlsConfig`, `resp::JsonRef`) were invisible to this
      # job and were caught by hand.
      - name: cargo semver-checks
        run: |
          BASELINE=$(git describe --tags --abbrev=0 --match '[0-9]*.[0-9]*.[0-9]*')
          echo "baseline: $BASELINE"
          cargo semver-checks --baseline-rev "$BASELINE" \
            --only-explicit-features \
            --features tokio-runtime,tokio-rustls,pool,json,client-cache

  # Compile a curated set of feature combinations (compile-only, no server
  # needed). `--no-default-features` is used so each entry compiles exactly the
  # features it names: the default set would otherwise mask a combination that
  # only builds because something else pulled a dependency in.
  check-features:
    name: Check features (${{ matrix.features }})
    runs-on: ubuntu-22.04
    timeout-minutes: 20
    env:
      CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
    strategy:
      fail-fast: false
      matrix:
        features:
          - tokio-runtime
          - tokio-runtime,pool,json,client-cache
          - tokio-runtime,tokio-rustls,pool,json,client-cache
          - tokio-runtime,tokio-native-tls,pool,json,client-cache
    steps:
      - name: Checkout sources
        uses: actions/checkout@v5
      - name: cargo check --no-default-features --features ${{ matrix.features }}
        run: cargo check --no-default-features --features ${{ matrix.features }}

  test:
    name: Test
    runs-on: ubuntu-latest
    timeout-minutes: 20
    env:
      CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
    steps:
      - name: Checkout sources
        uses: actions/checkout@v5
      - name: Create Redis containers
        run: |
          cd /home/runner/work/rustis/rustis/redis/
          sh ./docker_up.sh
      - name: Wait for Redis to be ready
        timeout-minutes: 3
        run: |
          # `docker compose up -d` returns as soon as the containers start,
          # not when Redis is serving or the cluster has finished forming.
          # Launching the tests too early makes cluster clients loop on
          # reconnection forever, so gate the test step on readiness.
          echo "Waiting for standalone node..."
          until docker exec redis-standalone redis-cli ping 2>/dev/null | grep -q PONG; do
            sleep 1
          done
          echo "Waiting for cluster to reach cluster_state:ok..."
          until docker exec redis-node1 redis-cli -p 7000 cluster info 2>/dev/null | grep -q "cluster_state:ok"; do
            sleep 1
          done
          # The spare servers carry the topology mutators and the Sentinel
          # failover commands, which cannot be sent to the shared deployments
          # above. They belong to no cluster, so there is no `cluster_state:ok`
          # to wait for — answering a ping is the whole readiness condition.
          echo "Waiting for spare cluster nodes..."
          until docker exec redis-spare-node1 redis-cli -p 7006 ping 2>/dev/null | grep -q PONG; do
            sleep 1
          done
          until docker exec redis-spare-node2 redis-cli -p 7007 ping 2>/dev/null | grep -q PONG; do
            sleep 1
          done
          echo "Waiting for the spare Sentinel to see its replica..."
          until docker exec redis-spare-sentinel redis-cli -p 26382 \
            sentinel replicas spareservice 2>/dev/null | grep -q "master-link-status"; do
            sleep 1
          done
          echo "Redis is ready."
      - name: Run cargo test
        run: cargo test --release --features pool,tokio-rustls,json,client-cache -- --test-threads=1
      # A second invocation, after the suite has filled the probe dump: the
      # harness gives no ordering guarantee that would let one run both collect
      # the observations and judge them. This is what keeps a Redis upgrade that
      # changes a reply shape from passing unnoticed.
      - name: Report response shapes
        env:
          RUSTIS_RESPONSE_SHAPE_REPORT: "1"
        run: |
          cargo test --release --features pool,tokio-rustls,json,client-cache \
            response_shape -- --test-threads=1

  # `rustls` and `native-tls` are mutually exclusive — `src/lib.rs` rejects the
  # pair with a `compile_error!`, and `src/tests/tls.rs` defines one `tls()` per
  # backend — so the native-tls backend needs a job of its own rather than an
  # extra feature on the one above. Without it the backend is only ever
  # `cargo check`ed, and a compiled path nothing runs is an untested path.
  #
  # The whole suite runs, not just `tls::`: the `native-tls` cfg sites reach
  # into the config, network and error modules that every test goes through.
  test-native-tls:
    name: Test (native-tls)
    runs-on: ubuntu-latest
    timeout-minutes: 20
    env:
      CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
    steps:
      - name: Checkout sources
        uses: actions/checkout@v5
      - name: Create Redis containers
        run: |
          cd /home/runner/work/rustis/rustis/redis/
          sh ./docker_up.sh
      - name: Wait for Redis to be ready
        timeout-minutes: 3
        run: |
          echo "Waiting for standalone node..."
          until docker exec redis-standalone redis-cli ping 2>/dev/null | grep -q PONG; do
            sleep 1
          done
          echo "Waiting for cluster to reach cluster_state:ok..."
          until docker exec redis-node1 redis-cli -p 7000 cluster info 2>/dev/null | grep -q "cluster_state:ok"; do
            sleep 1
          done
          # The spare servers carry the topology mutators and the Sentinel
          # failover commands, which cannot be sent to the shared deployments
          # above. They belong to no cluster, so there is no `cluster_state:ok`
          # to wait for — answering a ping is the whole readiness condition.
          echo "Waiting for spare cluster nodes..."
          until docker exec redis-spare-node1 redis-cli -p 7006 ping 2>/dev/null | grep -q PONG; do
            sleep 1
          done
          until docker exec redis-spare-node2 redis-cli -p 7007 ping 2>/dev/null | grep -q PONG; do
            sleep 1
          done
          echo "Waiting for the spare Sentinel to see its replica..."
          until docker exec redis-spare-sentinel redis-cli -p 26382 \
            sentinel replicas spareservice 2>/dev/null | grep -q "master-link-status"; do
            sleep 1
          done
          echo "Redis is ready."
      # `--no-default-features` is required here: the default set pulls in
      # `tokio-rustls`, which cannot coexist with `tokio-native-tls`.
      - name: Run cargo test
        run: |
          cargo test --release --no-default-features \
            --features tokio-runtime,tokio-native-tls,pool,json,client-cache \
            -- --test-threads=1
      # A second invocation, after the suite has filled the probe dump: the
      # harness gives no ordering guarantee that would let one run both collect
      # the observations and judge them. This is what keeps a Redis upgrade that
      # changes a reply shape from passing unnoticed.
      - name: Report response shapes
        env:
          RUSTIS_RESPONSE_SHAPE_REPORT: "1"
        run: |
          cargo test --release --no-default-features \
            --features tokio-runtime,tokio-native-tls,pool,json,client-cache \
            response_shape -- --test-threads=1