Skip to content

Commit 7f61990

Browse files
authored
feat: add resp3 support (#46)
* feat: add resp3 support * fixed: add support of upgrade/downgrade * fix: unable upgrade resp3 of error reply * fix: add backend resp3 impl
1 parent 73a879f commit 7f61990

10 files changed

Lines changed: 817 additions & 94 deletions

File tree

docker/config/integration.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ servers = [
88
]
99
password = "front-standalone-secret"
1010
backend_password = "backend-standalone-secret"
11+
backend_resp_version = "resp3"
1112

1213
[[clusters]]
1314
name = "cluster"
@@ -24,3 +25,4 @@ auth = { password = "front-cluster-secret", users = [
2425
{ username = "ops", password = "ops-secret" }
2526
] }
2627
backend_password = "backend-cluster-secret"
28+
backend_resp_version = "resp3"

docker/integration-test.sh

100644100755
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,26 @@ wait_for_cluster() {
4141

4242
wait_for_cluster
4343

44+
if ! hello_standalone="$(REDISCLI_AUTH="$STANDALONE_PASS" redis-cli -h aster-proxy -p 6380 --raw HELLO 3 AUTH default "$STANDALONE_PASS")"; then
45+
echo "HELLO 3 handshake against standalone proxy failed" >&2
46+
exit 1
47+
fi
48+
if ! printf "%s\n" "$hello_standalone" | awk 'BEGIN{found=0} {if(prev=="proto" && $0=="3"){found=1} prev=$0} END{exit(found?0:1)}'; then
49+
echo "HELLO 3 response from standalone proxy missing proto=3:" >&2
50+
printf "%s\n" "$hello_standalone" >&2
51+
exit 1
52+
fi
53+
54+
if ! hello_cluster="$(REDISCLI_AUTH="$CLUSTER_USER_PASS" redis-cli -h aster-proxy -p 6381 --user "$CLUSTER_USER" --raw HELLO 3 AUTH "$CLUSTER_USER" "$CLUSTER_USER_PASS")"; then
55+
echo "HELLO 3 handshake against cluster proxy failed" >&2
56+
exit 1
57+
fi
58+
if ! printf "%s\n" "$hello_cluster" | awk 'BEGIN{found=0} {if(prev=="proto" && $0=="3"){found=1} prev=$0} END{exit(found?0:1)}'; then
59+
echo "HELLO 3 response from cluster proxy missing proto=3:" >&2
60+
printf "%s\n" "$hello_cluster" >&2
61+
exit 1
62+
fi
63+
4464
noauth_output="$(redis-cli -h aster-proxy -p 6380 PING 2>&1 || true)"
4565
if echo "$noauth_output" | grep -q "PONG"; then
4666
echo "Expected standalone proxy to require authentication" >&2

src/auth/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,17 @@ impl BackendAuth {
100100
.expect("AUTH command is always valid")
101101
}
102102

103+
pub fn hello_credentials(&self) -> Option<(Bytes, Bytes)> {
104+
match self.parts.len() {
105+
2 => Some((
106+
Bytes::from_static(DEFAULT_USER.as_bytes()),
107+
self.parts[1].clone(),
108+
)),
109+
3 => Some((self.parts[1].clone(), self.parts[2].clone())),
110+
_ => None,
111+
}
112+
}
113+
103114
pub async fn apply_to_stream(
104115
&self,
105116
framed: &mut Framed<TcpStream, RespCodec>,

src/cluster/mod.rs

Lines changed: 152 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ use crate::hotkey::Hotkey;
2525
use crate::info::{InfoContext, ProxyMode};
2626
use crate::metrics;
2727
use crate::protocol::redis::{
28-
BlockingKind, MultiDispatch, RedisCommand, RespCodec, RespValue, SlotMap, SubCommand,
29-
SubResponse, SubscriptionKind, SLOT_COUNT,
28+
BlockingKind, MultiDispatch, RedisCommand, RespCodec, RespValue, RespVersion, SlotMap,
29+
SubCommand, SubResponse, SubscriptionKind, SLOT_COUNT,
3030
};
3131
use crate::slowlog::Slowlog;
3232
use crate::utils::{crc16, trim_hash_tag};
@@ -78,6 +78,7 @@ impl ClusterProxy {
7878
runtime.clone(),
7979
REQUEST_TIMEOUT_MS,
8080
backend_auth.clone(),
81+
config.backend_resp_version,
8182
));
8283
let pool = Arc::new(ConnectionPool::new(cluster.clone(), connector.clone()));
8384
let auth = config
@@ -143,17 +144,25 @@ impl ClusterProxy {
143144
let client_id = ClientId::new();
144145
let _guard = FrontConnectionGuard::new(&self.cluster);
145146

146-
let (mut sink, stream) = Framed::new(socket, RespCodec::default()).split();
147+
let framed = Framed::new(socket, RespCodec::default());
148+
let codec_handle = framed.codec().clone();
149+
let (mut sink, stream) = framed.split();
147150
let mut stream = stream.fuse();
148-
let mut pending: FuturesOrdered<BoxFuture<'static, RespValue>> = FuturesOrdered::new();
151+
let mut pending: FuturesOrdered<BoxFuture<'static, (RespValue, Option<RespVersion>)>> =
152+
FuturesOrdered::new();
153+
let mut _resp3_negotiated = codec_handle.version() == RespVersion::Resp3;
149154
let mut inflight = 0usize;
150155
let mut stream_closed = false;
151156
let mut auth_state = self.auth.as_ref().map(|auth| auth.new_session());
152157

153158
loop {
154159
tokio::select! {
155-
Some(resp) = pending.next(), if inflight > 0 => {
160+
Some((resp, version)) = pending.next(), if inflight > 0 => {
156161
inflight -= 1;
162+
if let Some(version) = version {
163+
codec_handle.set_version(version);
164+
_resp3_negotiated = version == RespVersion::Resp3;
165+
}
157166
sink.send(resp).await?;
158167
}
159168
frame_opt = stream.next(), if !stream_closed && inflight < PIPELINE_LIMIT => {
@@ -168,14 +177,13 @@ impl ClusterProxy {
168177
cmd = new_cmd;
169178
}
170179
AuthAction::Reply(resp) => {
171-
let fut = async move { resp };
180+
let fut = async move { (resp, None) };
172181
pending.push_back(Box::pin(fut));
173182
inflight += 1;
174183
continue;
175184
}
176185
}
177186
}
178-
179187
if matches!(cmd.as_subscription(), SubscriptionKind::Channel | SubscriptionKind::Pattern) {
180188
let kind_label = cmd.kind_label();
181189
if cmd.args().len() <= 1 {
@@ -230,8 +238,12 @@ impl ClusterProxy {
230238
}
231239
};
232240
while inflight > 0 {
233-
if let Some(resp) = pending.next().await {
241+
if let Some((resp, version)) = pending.next().await {
234242
inflight -= 1;
243+
if let Some(version) = version {
244+
codec_handle.set_version(version);
245+
_resp3_negotiated = version == RespVersion::Resp3;
246+
}
235247
sink.send(resp).await?;
236248
} else {
237249
inflight = 0;
@@ -291,7 +303,7 @@ impl ClusterProxy {
291303
kind_label,
292304
success,
293305
);
294-
let fut = async move { response };
306+
let fut = async move { (response, None) };
295307
pending.push_back(Box::pin(fut));
296308
inflight += 1;
297309
continue;
@@ -302,7 +314,7 @@ impl ClusterProxy {
302314
cmd.kind_label(),
303315
true,
304316
);
305-
let fut = async move { response };
317+
let fut = async move { (response, None) };
306318
pending.push_back(Box::pin(fut));
307319
inflight += 1;
308320
continue;
@@ -314,7 +326,7 @@ impl ClusterProxy {
314326
cmd.kind_label(),
315327
success,
316328
);
317-
let fut = async move { response };
329+
let fut = async move { (response, None) };
318330
pending.push_back(Box::pin(fut));
319331
inflight += 1;
320332
continue;
@@ -326,21 +338,31 @@ impl ClusterProxy {
326338
cmd.kind_label(),
327339
success,
328340
);
329-
let fut = async move { response };
341+
let fut = async move { (response, None) };
330342
pending.push_back(Box::pin(fut));
331343
inflight += 1;
332344
continue;
333345
}
346+
let requested_version = cmd.resp_version_request();
334347
let guard = self.prepare_dispatch(client_id, cmd);
335-
pending.push_back(Box::pin(guard));
348+
let fut = async move {
349+
let resp = guard.await;
350+
let version = if !resp.is_error() {
351+
requested_version
352+
} else {
353+
None
354+
};
355+
(resp, version)
356+
};
357+
pending.push_back(Box::pin(fut));
336358
inflight += 1;
337359
}
338360
Err(err) => {
339361
metrics::global_error_incr();
340362
metrics::front_error(self.cluster.as_ref(), "parse");
341363
metrics::front_command(self.cluster.as_ref(), "invalid", false);
342364
let message = Bytes::from(format!("ERR {err}"));
343-
let fut = async move { RespValue::Error(message) };
365+
let fut = async move { (RespValue::Error(message), None) };
344366
pending.push_back(Box::pin(fut));
345367
inflight += 1;
346368
}
@@ -364,7 +386,11 @@ impl ClusterProxy {
364386
}
365387
}
366388

367-
while let Some(resp) = pending.next().await {
389+
while let Some((resp, version)) = pending.next().await {
390+
if let Some(version) = version {
391+
codec_handle.set_version(version);
392+
_resp3_negotiated = version == RespVersion::Resp3;
393+
}
368394
sink.send(resp).await?;
369395
}
370396
sink.close().await?;
@@ -903,13 +929,15 @@ struct ClusterConnector {
903929
heartbeat_interval: Duration,
904930
reconnect_base_delay: Duration,
905931
max_reconnect_attempts: usize,
932+
backend_resp_version: RespVersion,
906933
}
907934

908935
impl ClusterConnector {
909936
fn new(
910937
runtime: Arc<ClusterRuntime>,
911938
default_timeout_ms: u64,
912939
backend_auth: Option<BackendAuth>,
940+
backend_resp_version: RespVersion,
913941
) -> Self {
914942
Self {
915943
runtime,
@@ -918,6 +946,7 @@ impl ClusterConnector {
918946
heartbeat_interval: Duration::from_secs(30),
919947
reconnect_base_delay: Duration::from_millis(50),
920948
max_reconnect_attempts: 3,
949+
backend_resp_version,
921950
}
922951
}
923952

@@ -950,11 +979,91 @@ impl ClusterConnector {
950979
Ok(framed)
951980
}
952981

982+
async fn negotiate_resp_version(
983+
&self,
984+
cluster: &str,
985+
node: &BackendNode,
986+
framed: &mut Framed<TcpStream, RespCodec>,
987+
) -> Result<RespVersion> {
988+
framed.codec_mut().set_version(RespVersion::Resp2);
989+
if self.backend_resp_version != RespVersion::Resp3 {
990+
return Ok(RespVersion::Resp2);
991+
}
992+
993+
let timeout_duration = self.current_timeout();
994+
let mut hello_parts = vec![
995+
RespValue::BulkString(Bytes::from_static(b"HELLO")),
996+
RespValue::BulkString(Bytes::from_static(b"3")),
997+
];
998+
if let Some(auth) = &self.backend_auth {
999+
if let Some((username, password)) = auth.hello_credentials() {
1000+
hello_parts.push(RespValue::BulkString(Bytes::from_static(b"AUTH")));
1001+
hello_parts.push(RespValue::BulkString(username));
1002+
hello_parts.push(RespValue::BulkString(password));
1003+
}
1004+
}
1005+
let hello = RespValue::Array(hello_parts);
1006+
1007+
match timeout(timeout_duration, framed.send(hello)).await {
1008+
Ok(Ok(())) => {}
1009+
Ok(Err(err)) => {
1010+
metrics::backend_error(cluster, node.as_str(), "resp3_handshake");
1011+
return Err(err.context(format!("failed to send RESP3 HELLO to {}", node.as_str())));
1012+
}
1013+
Err(_) => {
1014+
metrics::backend_error(cluster, node.as_str(), "resp3_handshake");
1015+
return Err(anyhow!(
1016+
"backend {} timed out sending RESP3 HELLO",
1017+
node.as_str()
1018+
));
1019+
}
1020+
}
1021+
1022+
let reply = match timeout(timeout_duration, framed.next()).await {
1023+
Ok(Some(Ok(value))) => value,
1024+
Ok(Some(Err(err))) => {
1025+
metrics::backend_error(cluster, node.as_str(), "resp3_handshake");
1026+
return Err(err.context(format!(
1027+
"failed to read RESP3 HELLO reply from {}",
1028+
node.as_str()
1029+
)));
1030+
}
1031+
Ok(None) => {
1032+
metrics::backend_error(cluster, node.as_str(), "resp3_handshake");
1033+
return Err(anyhow!(
1034+
"backend {} closed connection during RESP3 HELLO",
1035+
node.as_str()
1036+
));
1037+
}
1038+
Err(_) => {
1039+
metrics::backend_error(cluster, node.as_str(), "resp3_handshake");
1040+
return Err(anyhow!(
1041+
"backend {} timed out waiting for RESP3 HELLO reply",
1042+
node.as_str()
1043+
));
1044+
}
1045+
};
1046+
1047+
if reply.is_error() {
1048+
info!(
1049+
cluster = %cluster,
1050+
backend = %node.as_str(),
1051+
"backend rejected RESP3 HELLO; falling back to RESP2"
1052+
);
1053+
framed.codec_mut().set_version(RespVersion::Resp2);
1054+
return Ok(RespVersion::Resp2);
1055+
}
1056+
1057+
framed.codec_mut().set_version(RespVersion::Resp3);
1058+
Ok(RespVersion::Resp3)
1059+
}
1060+
9531061
async fn execute(
9541062
&self,
9551063
framed: &mut Framed<TcpStream, RespCodec>,
9561064
command: RedisCommand,
9571065
) -> Result<RespValue> {
1066+
let requested_version = command.resp_version_request();
9581067
let blocking = command.as_blocking();
9591068
if let Ok(name) = std::str::from_utf8(command.command_name()) {
9601069
if name.eq_ignore_ascii_case("blpop") || name.eq_ignore_ascii_case("brpop") {
@@ -966,7 +1075,7 @@ impl ClusterConnector {
9661075
.await
9671076
.context("timed out sending command")??;
9681077

969-
match blocking {
1078+
let response = match blocking {
9701079
BlockingKind::Queue { .. } | BlockingKind::Stream { .. } => match framed.next().await {
9711080
Some(Ok(value)) => Ok(value),
9721081
Some(Err(err)) => Err(err.into()),
@@ -978,7 +1087,14 @@ impl ClusterConnector {
9781087
Ok(None) => Err(anyhow!("backend closed connection")),
9791088
Err(_) => Err(anyhow!("timed out waiting for response")),
9801089
},
1090+
}?;
1091+
1092+
if let Some(version) = requested_version {
1093+
if !response.is_error() {
1094+
framed.codec_mut().set_version(version);
1095+
}
9811096
}
1097+
Ok(response)
9821098
}
9831099

9841100
async fn connect_with_retry(
@@ -990,15 +1106,33 @@ impl ClusterConnector {
9901106
for attempt in 0..self.max_reconnect_attempts {
9911107
let attempt_start = Instant::now();
9921108
match self.open_stream(node.as_str()).await {
993-
Ok(stream) => {
1109+
Ok(mut stream) => {
9941110
metrics::backend_probe_duration(
9951111
cluster,
9961112
node.as_str(),
9971113
"connect",
9981114
attempt_start.elapsed(),
9991115
);
10001116
metrics::backend_probe_result(cluster, node.as_str(), "connect", true);
1001-
return Ok(stream);
1117+
match self
1118+
.negotiate_resp_version(cluster, node, &mut stream)
1119+
.await
1120+
{
1121+
Ok(_) => return Ok(stream),
1122+
Err(err) => {
1123+
warn!(
1124+
cluster = %cluster,
1125+
backend = %node.as_str(),
1126+
attempt = attempt + 1,
1127+
error = %err,
1128+
"failed to negotiate RESP version with backend"
1129+
);
1130+
last_error = Some(err);
1131+
if attempt + 1 < self.max_reconnect_attempts {
1132+
sleep(self.reconnect_base_delay).await;
1133+
}
1134+
}
1135+
}
10021136
}
10031137
Err(err) => {
10041138
let elapsed = attempt_start.elapsed();

0 commit comments

Comments
 (0)