diff --git a/ci/pod/docker-compose.common.yml b/ci/pod/docker-compose.common.yml index 50d8004c53d1..0d387e2fe2ec 100644 --- a/ci/pod/docker-compose.common.yml +++ b/ci/pod/docker-compose.common.yml @@ -35,6 +35,16 @@ services: - "6379:6379" - "6380:6380" + ## Valkey + # No TLS/ACL config is needed here (unlike apisix_redis) because the Valkey + # tests only cover the plaintext, non-auth path. Add a command/volumes block + # mirroring apisix_redis if TLS or ACL coverage is introduced later. + apisix_valkey: + image: valkey/valkey:8 + restart: unless-stopped + ports: + - "6381:6379" + ## Etcd etcd_old: image: bitnamilegacy/etcd:3.3.8 diff --git a/docs/en/latest/plugins/ai-cache.md b/docs/en/latest/plugins/ai-cache.md index 907b8d177719..6552e47f3499 100644 --- a/docs/en/latest/plugins/ai-cache.md +++ b/docs/en/latest/plugins/ai-cache.md @@ -63,6 +63,25 @@ Even with `cache_key.share_across_routes` enabled, the cache key identifies the ::: +## Valkey Compatibility + +[Valkey](https://valkey.io/) works as a drop-in replacement for the exact (L1) cache layer. Because Valkey implements the Redis serialization protocol (RESP), no code or schema changes are required — configure the same `redis_host`, `redis_port`, `redis_password`, and other Redis attributes to point to your Valkey deployment. + +The semantic (L2) layer is an exception: it requires the RediSearch module (available in [Redis Stack](https://redis.io/docs/stack/)), which Valkey does not provide. Use Valkey only when `layers` is omitted or contains only `"exact"`. + +For example, to back the exact (L1) cache with a Valkey instance: + +```json +{ + "layers": ["exact"], + "redis_host": "valkey.example.com", + "redis_port": 6379, + "redis_password": "secret", + "redis_database": 0, + "redis_timeout": 1000 +} +``` + ## Attributes | Name | Type | Required | Default | Valid values | Description | diff --git a/docs/en/latest/plugins/ai-rate-limiting.md b/docs/en/latest/plugins/ai-rate-limiting.md index acf1367a9bb1..664227475c60 100644 --- a/docs/en/latest/plugins/ai-rate-limiting.md +++ b/docs/en/latest/plugins/ai-rate-limiting.md @@ -40,6 +40,23 @@ import TabItem from '@theme/TabItem'; The `ai-rate-limiting` Plugin enforces token-based rate limiting for requests sent to LLM services. It helps manage API usage by controlling the number of tokens consumed within a specified time frame, ensuring fair resource allocation and preventing excessive load on the service. It is often used with [`ai-proxy`](./ai-proxy.md) or [`ai-proxy-multi`](./ai-proxy-multi.md) Plugin. +## Valkey Compatibility + +[Valkey](https://valkey.io/) is fully compatible with this Plugin. Because Valkey implements the Redis serialization protocol (RESP), you can use a Valkey instance as a drop-in replacement wherever a Redis instance is configured. No code or schema changes are required — configure the same `redis_host`, `redis_port`, `redis_password`, and other Redis attributes to point to your Valkey deployment. + +For example, to point the `redis` policy at a Valkey instance: + +```json +{ + "policy": "redis", + "redis_host": "valkey.example.com", + "redis_port": 6379, + "redis_password": "secret", + "redis_database": 0, + "redis_timeout": 1000 +} +``` + ## Attributes | Name | Type | Required | Default | Valid values | Description | diff --git a/docs/en/latest/plugins/graphql-limit-count.md b/docs/en/latest/plugins/graphql-limit-count.md index 2633131ca6a5..76af1434f11f 100644 --- a/docs/en/latest/plugins/graphql-limit-count.md +++ b/docs/en/latest/plugins/graphql-limit-count.md @@ -48,6 +48,23 @@ You may see the following rate limiting headers in the response: - `X-RateLimit-Remaining`: the remaining quota - `X-RateLimit-Reset`: number of seconds left for the counter to reset +## Valkey Compatibility + +[Valkey](https://valkey.io/) is fully compatible with this Plugin. Because Valkey implements the Redis serialization protocol (RESP), you can use a Valkey instance as a drop-in replacement wherever a Redis instance is configured. No code or schema changes are required — configure the same `redis_host`, `redis_port`, `redis_password`, and other Redis attributes to point to your Valkey deployment. + +For example, to point the `redis` policy at a Valkey instance: + +```json +{ + "policy": "redis", + "redis_host": "valkey.example.com", + "redis_port": 6379, + "redis_password": "secret", + "redis_database": 0, + "redis_timeout": 1000 +} +``` + ## Attributes This Plugin shares the same schema as the [limit-count](./limit-count.md) Plugin. Refer to that page for the full attribute reference. Key attributes are listed below. diff --git a/docs/en/latest/plugins/limit-conn.md b/docs/en/latest/plugins/limit-conn.md index a92ecf2db36e..73c27df7d292 100644 --- a/docs/en/latest/plugins/limit-conn.md +++ b/docs/en/latest/plugins/limit-conn.md @@ -37,6 +37,23 @@ import TabItem from '@theme/TabItem'; The `limit-conn` Plugin limits the rate of requests by the number of concurrent connections. Requests exceeding the threshold will be delayed or rejected based on the configuration, ensuring controlled resource usage and preventing overload. +## Valkey Compatibility + +[Valkey](https://valkey.io/) is fully compatible with this Plugin. Because Valkey implements the Redis serialization protocol (RESP), you can use a Valkey instance as a drop-in replacement wherever a Redis instance is configured. No code or schema changes are required — configure the same `redis_host`, `redis_port`, `redis_password`, and other Redis attributes to point to your Valkey deployment. + +For example, to point the `redis` policy at a Valkey instance: + +```json +{ + "policy": "redis", + "redis_host": "valkey.example.com", + "redis_port": 6379, + "redis_password": "secret", + "redis_database": 0, + "redis_timeout": 1000 +} +``` + ## Attributes | Name | Type | Required | Default | Valid values | Description | diff --git a/docs/en/latest/plugins/limit-count.md b/docs/en/latest/plugins/limit-count.md index 38f1d4f59310..30063cb2fb29 100644 --- a/docs/en/latest/plugins/limit-count.md +++ b/docs/en/latest/plugins/limit-count.md @@ -43,6 +43,23 @@ You may see the following rate limiting headers in the response: * `X-RateLimit-Remaining`: the remaining quota * `X-RateLimit-Reset`: number of seconds left for the counter to reset +## Valkey Compatibility + +[Valkey](https://valkey.io/) is fully compatible with this Plugin. Because Valkey implements the Redis serialization protocol (RESP), you can use a Valkey instance as a drop-in replacement wherever a Redis instance is configured. No code or schema changes are required — configure the same `redis_host`, `redis_port`, `redis_password`, and other Redis attributes to point to your Valkey deployment. + +For example, to point the `redis` policy at a Valkey instance: + +```json +{ + "policy": "redis", + "redis_host": "valkey.example.com", + "redis_port": 6379, + "redis_password": "secret", + "redis_database": 0, + "redis_timeout": 1000 +} +``` + ## Attributes | Name | Type | Required | Default | Valid values | Description | diff --git a/docs/en/latest/plugins/limit-req.md b/docs/en/latest/plugins/limit-req.md index 59c65d8e547f..e6c119b8ffb7 100644 --- a/docs/en/latest/plugins/limit-req.md +++ b/docs/en/latest/plugins/limit-req.md @@ -45,6 +45,23 @@ The `limit-req` Plugin supports two modes of rate limiting: * **Local rate limiting**: Limits are enforced independently on each gateway instance. Each instance maintains its own counters, so the effective limit is roughly (limit × number of instances) when traffic is spread across instances. This is the default when no `policy` is set or when `policy` is `local`. * **Redis-based rate limiting**: Limits are shared across all gateway instances through Redis. All instances share the same quota, so the configured limit applies to all gateway instances. +## Valkey Compatibility + +[Valkey](https://valkey.io/) is fully compatible with this Plugin. Because Valkey implements the Redis serialization protocol (RESP), you can use a Valkey instance as a drop-in replacement wherever a Redis instance is configured. No code or schema changes are required — configure the same `redis_host`, `redis_port`, `redis_password`, and other Redis attributes to point to your Valkey deployment. + +For example, to point the `redis` policy at a Valkey instance: + +```json +{ + "policy": "redis", + "redis_host": "valkey.example.com", + "redis_port": 6379, + "redis_password": "secret", + "redis_database": 0, + "redis_timeout": 1000 +} +``` + ## Attributes | Name | Type | Required | Default | Valid values | Description | diff --git a/docs/en/latest/plugins/openid-connect.md b/docs/en/latest/plugins/openid-connect.md index 5bb74efd3fad..3a8e3316056f 100644 --- a/docs/en/latest/plugins/openid-connect.md +++ b/docs/en/latest/plugins/openid-connect.md @@ -38,6 +38,27 @@ import TabItem from '@theme/TabItem'; The `openid-connect` Plugin supports the integration with [OpenID Connect (OIDC)](https://openid.net/connect/) identity providers, such as Keycloak, Auth0, Microsoft Entra ID, Google, Okta, and more. It allows APISIX to authenticate clients and obtain their information from the identity provider before allowing or denying their access to upstream protected resources. +## Valkey Compatibility + +[Valkey](https://valkey.io/) is fully compatible with this Plugin's Redis session storage. Because Valkey implements the Redis serialization protocol (RESP), you can use a Valkey instance as a drop-in replacement wherever a Redis instance is configured. No code or schema changes are required — when `session.storage` is `redis`, point `session.redis.host`, `session.redis.port`, `session.redis.password`, and the other `session.redis.*` attributes at your Valkey deployment. + +For example, to store sessions in a Valkey instance: + +```json +{ + "session": { + "secret": "your-session-secret-min-16-chars", + "storage": "redis", + "redis": { + "host": "valkey.example.com", + "port": 6379, + "password": "secret", + "database": 0 + } + } +} +``` + ## Attributes | Name | Type | Required | Default | Valid values | Description | diff --git a/t/lib/test_redis.lua b/t/lib/test_redis.lua index 7f097a4608c9..70cfdbe316bb 100644 --- a/t/lib/test_redis.lua +++ b/t/lib/test_redis.lua @@ -25,7 +25,7 @@ local _M = {} -- 6479 is the sentinel master used by the redis-sentinel policies; leftover -- counters there survive CI re-runs if it is not flushed -local DEFAULT_PORTS = {6379, 6479, 5000, 5001, 5002, 5003, 5004, 5005, 5006} +local DEFAULT_PORTS = {6379, 6381, 6479, 5000, 5001, 5002, 5003, 5004, 5005, 5006} local DEFAULT_HOST = "127.0.0.1" local function log_warn(...) diff --git a/t/plugin/limit-count-valkey.t b/t/plugin/limit-count-valkey.t new file mode 100644 index 000000000000..f941bbf0867e --- /dev/null +++ b/t/plugin/limit-count-valkey.t @@ -0,0 +1,294 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +BEGIN { + if ($ENV{TEST_NGINX_CHECK_LEAK}) { + $SkipReason = "unavailable for the hup tests"; + + } else { + $ENV{TEST_NGINX_USE_HUP} = 1; + undef $ENV{TEST_NGINX_USE_STAP}; + } +} + +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +# Tests are order-dependent: route-setup tests precede their assertion tests +# (TESTs 2/3 depend on TEST 1, TEST 6 on TEST 5, TEST 8 on TEST 7, TEST 10 on TEST 9), +# so shuffling would break them. +no_shuffle(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->error_log && !$block->no_error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } + + my $extra_init_worker_by_lua = $block->extra_init_worker_by_lua // ""; + $extra_init_worker_by_lua .= <<_EOC_; + require("lib.test_redis").flush_port("127.0.0.1", 6381) +_EOC_ + + $block->set_value("extra_init_worker_by_lua", $extra_init_worker_by_lua); +}); + +run_tests; + +__DATA__ + +=== TEST 1: set route with Valkey (redis policy, port 6381) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + "policy": "redis", + "redis_host": "127.0.0.1", + "redis_port": 6381, + "redis_timeout": 1001 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: up the limit (Valkey) +--- pipelined_requests eval +["GET /hello", "GET /hello", "GET /hello"] +--- error_code eval +[200, 200, 503] + + + +=== TEST 3: up the limit with different URIs (Valkey) +--- pipelined_requests eval +["GET /hello1", "GET /hello", "GET /hello2", "GET /hello", "GET /hello"] +--- error_code eval +[404, 200, 404, 200, 503] + + + +=== TEST 4: set route with invalid redis host (Valkey) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + "policy": "redis" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.print(body) + } + } +--- error_code: 400 +--- response_body +{"error_msg":"failed to check the configuration of plugin limit-count err: then clause did not match"} + + + +=== TEST 5: set route with allow_degradation (Valkey) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + "policy": "redis", + "redis_host": "127.0.0.1", + "redis_port": 6381, + "redis_timeout": 1001, + "allow_degradation": true + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 6: up the limit with allow_degradation set (Valkey) +--- pipelined_requests eval +["GET /hello", "GET /hello", "GET /hello"] +--- error_code eval +[200, 200, 503] + + + +=== TEST 7: set route with group (Valkey) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + "policy": "redis", + "redis_host": "127.0.0.1", + "redis_port": 6381, + "redis_timeout": 1001, + "group": "valkey-test-group" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 8: up the limit with group (Valkey) +--- pipelined_requests eval +["GET /hello", "GET /hello", "GET /hello"] +--- error_code eval +[200, 200, 503] + + + +=== TEST 9: flush Valkey script cache +--- config + location /t { + content_by_lua_block { + local redis = require("resty.redis") + local red = redis:new() + red:set_timeout(1000) + local ok, err = red:connect("127.0.0.1", 6381) + if not ok then + ngx.say("failed to connect: ", err) + return + end + local ok, err = red:script("FLUSH") + if not ok then + ngx.say("failed to flush scripts: ", err) + return + end + ok, err = red:flushall() + if not ok then + ngx.say("failed to flushall: ", err) + return + end + red:set_keepalive(10000, 100) + ngx.say("done") + } + } +--- response_body +done + + + +=== TEST 10: evalsha NOSCRIPT fallback after SCRIPT FLUSH (Valkey) +--- request +GET /hello +--- error_code: 200 +--- grep_error_log eval +qr/redis evalsha failed:.*Falling back to eval/ +--- grep_error_log_out +redis evalsha failed: NOSCRIPT No matching script. Please use EVAL.. Falling back to eval