Added
-
ClusterConfig::read_preferenceroutes read-only commands to the replicas.
A cluster client sent every keyed command to the shard's master, replicas being
connected only to serve the broadcast policies, so a read-heavy deployment could
not spread its reads at all.ReadPreference::PreferReplica— spelled
?read_preference=prefer_replicain a URI — sends the commands the server reports
asreadonlyto the replicas of their shard, in round-robin, and puts those
connections inREADONLYmode. Writes, blocking commands, transactions and
redirections stay on the master, as does everything when a shard has no reachable
replica. The default,ReadPreference::Master, keeps the previous behaviour: a
replica lags behind its master, so reading from one trades consistency for
throughput and has to be asked for. -
Config::credentials_providerauthenticates with credentials resolved at every
handshake.Config::passwordis fixed once and for all, so a client of a managed
Redis whose password is a short-lived token (ElastiCache IAM, Memorystore IAM, Entra
ID, Vault) reconnects for one token lifetime and then fails authentication for good.
ACredentialsProvider— implemented for any closure returning
Result<Credentials>— is asked again on each reconnection. It takes precedence over
username/passwordand has no URI representation.SentinelConfig::credentials_provider
does the same for the Sentinel instances themselves; the two are independent, a
Sentinel being a different server with its own ACLs.
Changed
-
The manifest declares
license = "MIT"instead oflicense-file. With
license-file,cargo metadatareportslicense: null, so every tool that
keys on the SPDX identifier — license scanners, dependency audits, policy
checks — sees the crate as carrying no license.LICENSEis the verbatim MIT
text and is still packaged, so the terms are unchanged. -
A double no longer decodes as an integer unless the conversion is exact.
Every integer width converted aDoublereply with a saturating cast, so a
ZSCOREof3.9read asi64gave3,1e300gavei64::MAXandNaN
gave0— a plausible number for a value the server never sent. A double now
deserializes to an integer only when it is finite, has no fractional part and
fits the target; anything else fails withClientError::CannotParseInteger,
as an out-of-range integer reply already did. Read such a reply asf64to
keep the fractional value. A negative reply read asu128no longer wraps
either. Both deserializers — RESP andresp::Value— are covered. -
An array of more than one element no longer decodes as a single integer.
Every integer width (i8…i128,u8…u128) unwrapped an array reply to its
first element and discarded the rest, so a mistyped response shape produced a
plausible number instead of an error. Only a one-element array still unwraps;
a longer one fails withClientError::CannotParseInteger. -
A connection URI now rejects a query parameter it does not understand. An
unknown key (?commandtimeout=5000,?reconnection=constant) and a value that
does not parse (?command_timeout=5s) were both dropped silently, leaving the
default in place — a mistypedcommand_timeoutmeant no timeout at all. Both
now fail withClientError::InvalidUri, whose message names the parameter.
Code relying on a URI with an unknown parameter being accepted must drop it.
The documentedreconnectionparameter never existed and has been removed from
the list;max_command_attempts, which is parsed, has been added to it. -
keep_alivenow defaults to 30 seconds instead ofNone. Paired with the
defaultcommand_timeoutof 0 (no timeout), the previous default left a
half-open connection — one silently dropped by a NAT, a firewall or a load
balancer — detected by nothing: no timeout, no keepalive, no socket error, so
every awaiting caller parked forever andon_reconnectnever fired. Set
keep_alivetoNone, orkeep_alive=0in a URL, to restore the old
behaviour. -
Dependencies updated to their latest releases, including two major bumps:
rand0.9 → 0.10 andatoi2.0 → 3.1. Both are internal; the public API is
unchanged.serial_teststays on 3.x — its 4.0 requires Rust 1.93, above the
crate's 1.88 MSRV, which is itself unchanged.
Fixed
-
A Sentinel client rediscovers its master when the one it holds is demoted.
A failover turns the master into a replica without closing the connections it
already serves, so nothing in the transport says the topology moved: the client
kept writing to a replica, reading stale data and collectingREADONLYon every
write, for as long as the socket held — which is forever with the default absence
ofcommand_timeoutand TCP keepalive. AREADONLYreceived on a Sentinel
connection now triggers a reconnection, and a Sentinel reconnection polls the
sentinels for the master again and accepts a node only onceROLEconfirms it.
The caller who issued the refused write still receives theREADONLYitself; the
commands in flight follow the usual reconnection rules. Standalone and cluster
connections are unchanged — neither has a master to look up. -
TRYAGAINandCLUSTERDOWNare now retried instead of reaching the
caller. Both were parsed and never consulted, so a routine resharding — a
multi-key command whose keys straddle a slot in migration — or a failover
produced an application-visible error the driver is meant to absorb. A cluster
command answeredTRYAGAINis now replayed after 25 ms, and one answered
CLUSTERDOWNafter 250 ms and a topology reload, through the same retry path
asASK/MOVEDand under the samemax_command_attemptscap. -
Dropping a command future does not cancel the command, and this is now
documented. The message is already queued when the future is awaited, so it
is sent and executed by the server; only the reply is discarded. Every
tokio::time::timeout,select!, aborted task andcommand_timeouttherefore
leaves a non-idempotent command applied, which nothing said. A new
Cancellation and timeoutssection in theclientmodule states the contract
and what to do about it;Client::sendandConfig::command_timeoutpoint at
it. -
The silent
nilcoercion is now documented as a trap instead of a convenience.
Anilreply decodes as the neutral value of the response type —0for every
integer width,0.0for floats,falseforbool,""forString— so a
missing counter reads as zero with no error. The behaviour is unchanged, but the
respmodule,StringCommands::getandClient::sendnow state it and point at
Option<R>, which is honoured before any conversion and yieldsNoneonnil.
Thegetexample presented theStringcase as a convenience. -
The generic-command API documentation no longer teaches a Cluster misroute.
TheMSET/MGETexamples in the crate documentation and inClient::sendadded
their keys witharg, which does not mark an argument as a key: the command
carried no slot and was sent to a random node. They now usekeyand same-slot
hash tags, and the rule is stated in thecmd,argandkeydocumentation. -
keep_aliveandno_delayare now applied to TLS connections. Both were set
only on the plain TCP path: a TLS connection ran with Nagle's algorithm enabled
(up to 40 ms added to a small command) and without TCP keepalive, so a half-open
socket was detected by nothing. Both paths now share a single socket-setup step
applied to theTcpStreambefore the TLS handshake. -
Vector-set commands are usable in pipelines and transactions again.
VectorSetCommandswas implemented for&Pipelineand&Transactioninstead of
&mut, so.queue()and.forget()did not resolve on any of them and the whole
family was unreachable in batch mode. -
Building without any runtime feature now fails with a message that says so.
default-features = falsewithouttokio-runtimeleft every function of the
runtime layer without a body, and the user got ninecannot find … in this scope
errors pointing at internal items. Acompile_error!now names the missing
feature and how to enable it. -
Enabling
rustlsornative-tlson its own is now a clear compile error. The
backend-only features gate the TLS configuration types, while the connection code
reading them lives behindtokio-rustls/tokio-native-tls. Enabled alone they
produced sixcannot find … in this scopeerrors on internal stream types. A
compile_error!now names the runtime feature to enable instead. -
CI builds the feature combinations that were unbuildable. No job compiled the
crate without a runtime feature, which is why that break went unnoticed. The
feature matrix gainspool,json,client-cacheand the two TLS runtimes each
on their own, and a new job asserts that the four rejected configurations — no
runtime, both TLS runtimes, and each backend-only feature alone — fail with their
owncompile_error!rather than with a cascade of internal errors. -
The
actix_long_polling_pubsubandaxum_long_polling_pubsubexamples compile
again. They passedlpop's count as ausizewhere the signature takes au32.