rustis 0.24.0

Redis async driver for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
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-latest
    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-latest
    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-latest
    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
  # Report the public-API breaks a pull request introduces, so each one is a
  # deliberate entry in `CHANGELOG.md` rather than a discovery made after
  # publishing.
  #
  # It reports and never fails, which is a deliberate reversal. The job used to
  # compare against the previous tag and fail when the version in `Cargo.toml`
  # did not cover the difference -- but the version is raised at release time, so
  # from the first break after a tag until the next release every pull request
  # was red, including ones touching nothing but a workflow file. A check that is
  # red for reasons the author cannot act on is a check people learn to ignore,
  # and it took the whole run down with it. Verifying that the bump covers the
  # changes is the right question at publish time, not on a branch: `publish.yml`
  # asks it there, before the crate leaves the machine.
  #
  # The baseline is the pull request's own base commit, not the previous tag, so
  # what the report describes is what *this* branch changes. Against the tag it
  # re-listed every break already merged, which is the noise that made the
  # signal unusable.
  #
  # 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]`.
  semver:
    name: Semver report
    runs-on: ubuntu-latest
    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:
          # The base commit has to be in the clone to serve as the baseline.
          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
        env:
          BASELINE: ${{ github.event.pull_request.base.sha }}
        run: |
          # Never fails the job: `set -e` is off for the run, and the exit status
          # is turned into a summary instead of a failure.
          set +e
          echo "baseline: $BASELINE"
          output=$(cargo semver-checks --baseline-rev "$BASELINE" \
            --only-explicit-features \
            --features tokio-runtime,tokio-rustls,pool,json,client-cache 2>&1)
          status=$?
          echo "$output"
          {
            if [ $status -eq 0 ]; then
              echo "### Semver report: no public-API break against the base"
            else
              echo "### Semver report: public-API breaks against the base"
              echo
              echo "Each one belongs in \`CHANGELOG.md\` under \`[Unreleased]\`."
              echo "\`publish.yml\` is what enforces that the version bump covers them."
              echo
              echo '```'
              echo "$output"
              echo '```'
            fi
          } >> "$GITHUB_STEP_SUMMARY"
          if [ $status -ne 0 ]; then
            echo "::notice title=Semver::public-API breaks reported, see the job summary"
          fi
          exit 0

  # 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-latest
    timeout-minutes: 20
    env:
      CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
    strategy:
      fail-fast: false
      matrix:
        features:
          - tokio-runtime
          - tokio-runtime,pool
          - tokio-runtime,json
          - tokio-runtime,client-cache
          - tokio-runtime,pool,json,client-cache
          - tokio-runtime,tokio-rustls
          - tokio-runtime,tokio-native-tls
          - 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 }}

  # The combinations that must *not* build. Each is rejected by a `compile_error!`
  # in `lib.rs`, and what is checked is the message: a build failing for some
  # unrelated reason, or failing with the dozen "not found in this scope" errors
  # the guard exists to replace, fails this job too.
  #
  # `--no-default-features` alone is the entry the matrix above cannot hold: it
  # names no feature, and its absence is what let the crate stay unbuildable
  # without a runtime for as long as it did.
  check-rejected-features:
    name: Check rejected features (${{ matrix.name }})
    runs-on: ubuntu-latest
    timeout-minutes: 20
    env:
      CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
    strategy:
      fail-fast: false
      matrix:
        include:
          - name: no runtime
            features: ""
            expected: "rustis needs an async runtime feature"
          - name: both TLS runtimes
            features: tokio-runtime,tokio-rustls,tokio-native-tls
            expected: "cannot be enabled at the same time"
          - name: rustls alone
            features: tokio-runtime,rustls
            expected: "Feature `rustls` cannot be enabled on its own"
          - name: native-tls alone
            features: tokio-runtime,native-tls
            expected: "Feature `native-tls` cannot be enabled on its own"
    steps:
      - name: Checkout sources
        uses: actions/checkout@v5
      - name: cargo check --no-default-features --features ${{ matrix.features }} must fail
        run: |
          set +e
          output=$(cargo check --no-default-features --features "${{ matrix.features }}" 2>&1)
          status=$?
          echo "$output"
          if [ $status -eq 0 ]; then
            echo "::error::expected the build to fail, but it succeeded"
            exit 1
          fi
          if ! echo "$output" | grep -qF '${{ matrix.expected }}'; then
            echo "::error::build failed without the expected guard: ${{ matrix.expected }}"
            exit 1
          fi

  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: |
          # `--nocapture` because the report is printed by a test that passes, and
          # the harness swallows the stdout of a passing test. The step cannot
          # fail on an unexplained row -- see the module documentation -- so what
          # it does instead is put the rows in the job summary.
          output=$(cargo test --release --features pool,tokio-rustls,json,client-cache \
            response_shape -- --test-threads=1 --nocapture 2>&1)
          echo "$output"
          count=$(echo "$output" | sed -n 's/.*, \([0-9][0-9]*\) unexplained.*/\1/p' | tail -1)
          if [ -n "$count" ] && [ "$count" != "0" ]; then
            {
              echo "### Response shape report: $count unexplained observation(s)"
              echo
              echo "A declared response type the server's reply contradicts, or a shape"
              echo "nobody has triaged yet. Fix the type, or add the row to"
              echo "\`src/tests/response_shape_baseline.tsv\` with its reason."
              echo
              echo '```'
              echo "$output" | grep 'answered'
              echo '```'
            } >> "$GITHUB_STEP_SUMMARY"
            echo "::notice title=Response shapes::$count unexplained observation(s), see the job summary"
          fi

  # `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: |
          # See the same step in `test`: `--nocapture` for a passing test's output,
          # and an unexplained row goes to the job summary instead of failing.
          output=$(cargo test --release --no-default-features \
            --features tokio-runtime,tokio-native-tls,pool,json,client-cache \
            response_shape -- --test-threads=1 --nocapture 2>&1)
          echo "$output"
          count=$(echo "$output" | sed -n 's/.*, \([0-9][0-9]*\) unexplained.*/\1/p' | tail -1)
          if [ -n "$count" ] && [ "$count" != "0" ]; then
            {
              echo "### Response shape report (native-tls): $count unexplained observation(s)"
              echo
              echo "A declared response type the server's reply contradicts, or a shape"
              echo "nobody has triaged yet. Fix the type, or add the row to"
              echo "\`src/tests/response_shape_baseline.tsv\` with its reason."
              echo
              echo '```'
              echo "$output" | grep 'answered'
              echo '```'
            } >> "$GITHUB_STEP_SUMMARY"
            echo "::notice title=Response shapes (native-tls)::$count unexplained observation(s), see the job summary"
          fi