Skip to content

Commit 409f728

Browse files
authored
Merge branch 'master' into fix/2914-evalsha-cluster-pipeline
2 parents da63b94 + da03cdc commit 409f728

54 files changed

Lines changed: 5250 additions & 215 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/run-tests/action.yml

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,6 @@ inputs:
4949
client-libs-test-image:
5050
description: 'Client libs test image tag'
5151
required: false
52-
otlp-auth-token:
53-
description: 'OTLP authorization token for CI visibility metrics'
54-
required: false
5552
runs:
5653
using: "composite"
5754
steps:
@@ -60,7 +57,7 @@ runs:
6057
repository: ${{ inputs.repository }}
6158
ref: ${{ inputs.ref }}
6259

63-
- uses: actions/setup-python@v6
60+
- uses: actions/setup-python@v7
6461
with:
6562
python-version: ${{ inputs.python-version }}
6663
cache: 'pip'
@@ -176,8 +173,6 @@ runs:
176173
run: |
177174
set -e
178175
179-
mkdir -p junit-results
180-
181176
run_tests() {
182177
local test_config=$1
183178
local eventloop=""
@@ -315,35 +310,6 @@ runs:
315310
redis-cli -p 16379 CLUSTER NODES
316311
shell: bash
317312

318-
- name: Normalize JUnit XML for metrics
319-
if: ${{ !cancelled() && inputs.otlp-auth-token != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
320-
continue-on-error: true
321-
run: |
322-
python - <<'PY'
323-
import glob
324-
import xml.etree.ElementTree as ET
325-
326-
for path in glob.glob("junit-results/*.xml"):
327-
tree = ET.parse(path)
328-
root = tree.getroot()
329-
if root.tag == "testsuites" and root.get("time") is None:
330-
total = sum(float(ts.get("time") or 0) for ts in root.findall("testsuite"))
331-
root.set("time", f"{total:.6f}")
332-
tree.write(path, xml_declaration=True, encoding="utf-8")
333-
PY
334-
shell: bash
335-
336-
- name: Self Report Metrics
337-
if: ${{ !cancelled() && inputs.otlp-auth-token != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
338-
continue-on-error: true
339-
uses: redis-developer/cae-otel-ci-visibility@v2
340-
with:
341-
junit-xml-folder: "junit-results"
342-
otlp-endpoint: "https://otlp-gateway-prod-us-central-0.grafana.net/otlp/v1/metrics"
343-
otlp-headers: "Authorization=Basic ${{ inputs.otlp-auth-token }}"
344-
env:
345-
OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf"
346-
347313
- name: Upload test results and profiling data
348314
uses: actions/upload-artifact@v7
349315
with:

.github/wordlist.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,14 @@ docstrings
122122
eg
123123
enums
124124
exc
125+
fieldset
126+
fieldsets
125127
firsttimersonly
126128
fo
127129
formatter
128130
genindex
129131
gmail
132+
himport
130133
hiredis
131134
http
132135
idx
@@ -162,6 +165,7 @@ parsers
162165
performant
163166
pmessage
164167
png
168+
positionally
165169
pre
166170
psubscribe
167171
pubsub

.github/workflows/codeql-analysis.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040

4141
# Initializes the CodeQL tools for scanning.
4242
- name: Initialize CodeQL
43-
uses: github/codeql-action/init@v4
43+
uses: github/codeql-action/init@v4.37.3
4444
with:
4545
languages: ${{ matrix.language }}
4646
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -51,7 +51,7 @@ jobs:
5151
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
5252
# If this step fails, then you should remove it and run the build manually (see below)
5353
- name: Autobuild
54-
uses: github/codeql-action/autobuild@v4
54+
uses: github/codeql-action/autobuild@v4.37.3
5555

5656
# ℹ️ Command-line programs to run using the OS shell.
5757
# 📚 https://git.io/JvXDl
@@ -65,4 +65,4 @@ jobs:
6565
# make release
6666

6767
- name: Perform CodeQL Analysis
68-
uses: github/codeql-action/analyze@v4
68+
uses: github/codeql-action/analyze@v4.37.3

.github/workflows/docs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
runs-on: ubuntu-latest
2727
steps:
2828
- uses: actions/checkout@v7
29-
- uses: actions/setup-python@v6
29+
- uses: actions/setup-python@v7
3030
with:
3131
python-version: "3.10"
3232
cache: 'pip'

.github/workflows/integration.yaml

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ env:
3131
# for example after 8.2.1 is published, 8.2 image contains 8.2.1 content
3232
CURRENT_REDIS_VERSION: '8.8.0'
3333
REDIS_VERSION_CUSTOM_MAP: >-
34-
8.10:custom-29557896054-debian
34+
8.10:8.10.0
3535
3636
jobs:
3737
dependency-audit:
@@ -53,7 +53,7 @@ jobs:
5353
runs-on: ubuntu-latest
5454
steps:
5555
- uses: actions/checkout@v7
56-
- uses: actions/setup-python@v6
56+
- uses: actions/setup-python@v7
5757
with:
5858
python-version: "3.10"
5959
cache: 'pip'
@@ -115,7 +115,6 @@ jobs:
115115
# runners are added. Run them once, on the current Redis version's
116116
# cluster cell.
117117
run-multidb-integration: ${{ (matrix.redis-version == needs.redis_version.outputs.CURRENT && matrix.test-config == 'default-unified_responses-cluster') && 'true' || 'false' }}
118-
otlp-auth-token: ${{ secrets.SELF_CHECK_OTEL_AUTHORIZATION_TOKEN }}
119118

120119
python-compatibility-tests:
121120
runs-on: ubuntu-latest
@@ -155,7 +154,6 @@ jobs:
155154
parser-backend: ${{ matrix.parser-backend }}
156155
redis-version: ${{ matrix.redis-version }}
157156
test-config: ${{ matrix.test-config }}
158-
otlp-auth-token: ${{ secrets.SELF_CHECK_OTEL_AUTHORIZATION_TOKEN }}
159157

160158
pypy-compatibility-tests:
161159
runs-on: ubuntu-latest
@@ -198,7 +196,6 @@ jobs:
198196
parser-backend: ${{ matrix.parser-backend }}
199197
redis-version: ${{ matrix.redis-version }}
200198
test-config: ${{ matrix.test-config }}
201-
otlp-auth-token: ${{ secrets.SELF_CHECK_OTEL_AUTHORIZATION_TOKEN }}
202199

203200
hiredis-tests:
204201
runs-on: ubuntu-latest
@@ -240,7 +237,6 @@ jobs:
240237
redis-version: ${{ matrix.redis-version }}
241238
hiredis-version: ${{ matrix.hiredis-version }}
242239
test-config: ${{ matrix.test-config }}
243-
otlp-auth-token: ${{ secrets.SELF_CHECK_OTEL_AUTHORIZATION_TOKEN }}
244240

245241
uvloop-tests:
246242
runs-on: ubuntu-latest
@@ -281,7 +277,6 @@ jobs:
281277
redis-version: ${{ matrix.redis-version }}
282278
event-loop: ${{ matrix.event-loop }}
283279
test-config: ${{ matrix.test-config }}
284-
otlp-auth-token: ${{ secrets.SELF_CHECK_OTEL_AUTHORIZATION_TOKEN }}
285280

286281
build-and-test-package:
287282
name: Validate building and installing the package
@@ -293,7 +288,7 @@ jobs:
293288
extension: ['tar.gz', 'whl']
294289
steps:
295290
- uses: actions/checkout@v7
296-
- uses: actions/setup-python@v6
291+
- uses: actions/setup-python@v7
297292
with:
298293
python-version: "3.10"
299294
- name: Run installed unit tests
@@ -312,7 +307,7 @@ jobs:
312307
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.10', 'pypy-3.11']
313308
steps:
314309
- uses: actions/checkout@v7
315-
- uses: actions/setup-python@v6
310+
- uses: actions/setup-python@v7
316311
with:
317312
python-version: ${{ matrix.python-version }}
318313
cache: 'pip'
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Nightly test metrics
2+
3+
# Once a day, run the redis-py standalone test suite (master, default
4+
# client configuration, modules included) against a Redis server built
5+
# from redis' unstable branch, and report per-test durations to Grafana
6+
# for performance regression tracking.
7+
8+
on:
9+
schedule:
10+
- cron: '30 3 * * *' # nightly, staggered from the 01:00 CI run
11+
workflow_dispatch:
12+
13+
concurrency:
14+
group: nightly-metrics
15+
cancel-in-progress: false
16+
17+
permissions:
18+
contents: read # to fetch code (actions/checkout)
19+
20+
jobs:
21+
nightly-metrics:
22+
name: Redis unstable; test metrics
23+
runs-on: ubuntu-latest
24+
timeout-minutes: 60
25+
steps:
26+
# Secrets can't be used in `if:` conditions, so a first step checks
27+
# the token and later steps are gated on its output. Without the
28+
# secret the job prints a warning and skips everything.
29+
- name: Check for the metrics token
30+
id: gate
31+
env:
32+
OTEL_TOKEN: ${{ secrets.OTEL_CI_VISIBILITY_TOKEN }}
33+
run: |
34+
if [ -z "$OTEL_TOKEN" ]; then
35+
echo "::warning::The OTEL_CI_VISIBILITY_TOKEN secret is not set; skipping the nightly metrics run."
36+
echo "run=false" >> "$GITHUB_OUTPUT"
37+
else
38+
echo "run=true" >> "$GITHUB_OUTPUT"
39+
fi
40+
41+
- uses: actions/checkout@v7
42+
if: steps.gate.outputs.run == 'true'
43+
44+
- name: Resolve latest unstable Redis image
45+
id: unstable-image
46+
if: steps.gate.outputs.run == 'true'
47+
run: |
48+
set -euo pipefail
49+
# Pick the newest tag client-side instead of relying on the API's
50+
# ordering parameter.
51+
tag=$(curl -sf "https://hub.docker.com/v2/repositories/redislabs/client-libs-test/tags?page_size=100&name=unstable-" \
52+
| jq -r '.results | max_by(.last_updated) | .name // empty')
53+
if [ -z "$tag" ]; then
54+
echo "::error::Could not resolve an unstable client-libs-test image tag from Docker Hub"
55+
exit 1
56+
fi
57+
echo "Using image tag: $tag"
58+
echo "tag=$tag" >> "$GITHUB_OUTPUT"
59+
60+
- name: Run tests
61+
if: steps.gate.outputs.run == 'true'
62+
uses: ./.github/actions/run-tests
63+
with:
64+
python-version: '3.12'
65+
parser-backend: 'plain'
66+
# The server under test is the unstable image resolved above;
67+
# redis-version only drives run-tests' setup branching and the
68+
# artifact name, and it must be numeric.
69+
redis-version: '8.10'
70+
client-libs-test-image: ${{ steps.unstable-image.outputs.tag }}
71+
# One config, so each test reports exactly one duration per night.
72+
test-config: 'default-legacy_responses-standalone'
73+
74+
- name: Report test metrics
75+
# Report even when tests fail, but not when the run was cancelled.
76+
if: ${{ !cancelled() && steps.gate.outputs.run == 'true' }}
77+
uses: redis-developer/cae-otel-ci-visibility@v4
78+
with:
79+
junit-xml-folder: 'junit-results'
80+
otlp-endpoint: 'https://otlp-gateway-prod-us-central-0.grafana.net/otlp/v1/metrics'
81+
otlp-headers: 'Authorization=Basic ${{ secrets.OTEL_CI_VISIBILITY_TOKEN }}'
82+
server-version: 'unstable'

.github/workflows/pypi-publish.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
steps:
1616
- uses: actions/checkout@v7
1717
- name: install python
18-
uses: actions/setup-python@v6
18+
uses: actions/setup-python@v7
1919
with:
2020
python-version: "3.10"
2121
- run: pip install build twine

.github/workflows/spellcheck.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
- name: Checkout
1212
uses: actions/checkout@v7
1313
- name: Check Spelling
14-
uses: rojopolis/spellcheck-github-actions@0.62.0
14+
uses: rojopolis/spellcheck-github-actions@0.63.0
1515
with:
1616
config_path: .github/spellcheck-settings.yml
1717
task_name: Markdown

.github/workflows/stale-issues.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ jobs:
2222
permissions:
2323
issues: write # required by actions/stale to label/close issues
2424
pull-requests: write # required by actions/stale to label/close PRs
25-
actions: write # required by actions/stale@v10 for its cache state
25+
actions: write # required by actions/stale@v11 for its cache state
2626
steps:
2727
# First step: Handle regular issues (excluding needs-information)
2828
- name: Mark regular issues as stale
29-
uses: actions/stale@v10
29+
uses: actions/stale@v11
3030
with:
3131
repo-token: ${{ secrets.GITHUB_TOKEN }}
3232

@@ -70,7 +70,7 @@ jobs:
7070

7171
# Second step: Handle needs-information issues with accelerated timeline
7272
- name: Mark needs-information issues as stale
73-
uses: actions/stale@v10
73+
uses: actions/stale@v11
7474
with:
7575
repo-token: ${{ secrets.GITHUB_TOKEN }}
7676

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,66 @@ This is useful when:
221221

222222
For the complete failover configuration options and examples, see the [Multi-database client docs](https://redis.readthedocs.io/en/latest/multi_database.html).
223223

224+
### Bulk hash ingestion (HIMPORT)
225+
226+
Redis 8.10 adds the `HIMPORT` command family for loading many hashes that share
227+
the same set of field names: register the field names once with `himport_prepare`,
228+
then create each hash by sending only its values. Keys written this way are regular
229+
hashes — every hash command works on them.
230+
231+
``` python
232+
r.himport_prepare("users", ["name", "email", "age"])
233+
r.himport_set("user:1", "users", ["alice", "alice@example.com", "25"])
234+
r.himport_set("user:2", "users", ["bob", "bob@example.com", "30"])
235+
r.himport_discard("users") # => 1
236+
```
237+
238+
Values pair positionally with the prepared fields. Hash enumeration order
239+
(`HGETALL`, `HKEYS`) is not guaranteed to match the prepare order.
240+
241+
**Fieldsets are connection state.** A prepared fieldset lives in the server-side
242+
session of the physical connection that prepared it: it is invisible to other
243+
connections and destroyed by a disconnect or `RESET`. redis-py handles this for you —
244+
`himport_prepare` records the fieldset in a client-level registry, and the `PREPARE`
245+
is applied lazily on whatever pooled connection serves each `himport_set` (and
246+
re-applied automatically after a reconnect, `RESET`, or Sentinel/cluster failover).
247+
You declare each fieldset once per client with `himport_prepare`; there is no
248+
constructor argument for it.
249+
250+
For the highest ingestion throughput, send the `PREPARE` and its `SET`s in one
251+
pipeline — a single batch always executes on one connection:
252+
253+
``` python
254+
with r.pipeline(transaction=False) as pipe:
255+
pipe.himport_prepare("users", ["name", "email", "age"])
256+
for uid, row in rows:
257+
pipe.himport_set(f"user:{uid}", "users", row)
258+
pipe.execute()
259+
```
260+
261+
The automatic re-prepare applies to direct calls only, not to commands inside
262+
`pipeline`/`transaction` blocks: a batched `himport_set` relies on the single
263+
pre-flight `PREPARE` in that batch.
264+
265+
With `RedisCluster`, `himport_prepare` / `himport_discard` / `himport_discard_all`
266+
update the client's shared, cluster-wide registry and return immediately — like the
267+
standalone API, they perform no server I/O of their own. The server-side `PREPARE`
268+
(and, after a discard, `DISCARD`) is applied lazily on each node's connection the
269+
next time it serves an `himport_set`, and re-applied after reconnects or failover;
270+
`himport_set` itself routes by the key's hash slot. A discard is therefore not
271+
removed from every server session at once: each connection drops the fieldset on its
272+
next `himport_set` (or on disconnect). With Sentinel, call
273+
`himport_prepare` on the long-lived client returned by `master_for(...)`; the fieldset
274+
survives failover automatically. HIMPORT is not supported on the multi-database
275+
(Active-Active) client.
276+
277+
The async client mirrors this exactly:
278+
279+
``` python
280+
await r.himport_prepare("users", ["name", "email", "age"])
281+
await r.himport_set("user:1", "users", ["alice", "alice@example.com", "25"])
282+
```
283+
224284
---------------------------------------------
225285

226286
### Author

0 commit comments

Comments
 (0)