Skip to content

Commit 0797a67

Browse files
authored
CORE: Persist Client Name Across Reconnections for CLIENT SETNAME Command (#4841)
* persist client name across reconnections for CLIENT SETNAME command - Add client name update methods to Client, StandaloneClient, and cluster clients - Intercept CLIENT SETNAME commands to update stored connection info - Ensure client name persists through automatic reconnections - Add comprehensive tests for client name persistence behavior - Support both standalone and cluster connection modes Signed-off-by: affonsov <[email protected]> * added changelog Signed-off-by: affonsov <[email protected]> * clean up test Signed-off-by: affonsov <[email protected]> * refactored fucntion to not return value clean up the test Signed-off-by: affonsov <[email protected]> * fix lint Signed-off-by: affonsov <[email protected]> * fix lint Signed-off-by: affonsov <[email protected]> --------- Signed-off-by: affonsov <[email protected]>
1 parent 8843b6f commit 0797a67

File tree

7 files changed

+286
-0
lines changed

7 files changed

+286
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
* CORE: Fix SELECT Command Database Persistence Across Reconnections ([#4764](https://github.com/valkey-io/valkey-glide/issues/#4764))
2121
* Rust: Updates the `install-rust-and-protoc` action to explicitly include the `rustfmt` and `clippy` components. ([#4816](https://github.com/valkey-io/valkey-glide/issues/4816))
22+
* CORE: Persist Client Name Across Reconnections for CLIENT SETNAME Command. ([#4841](https://github.com/valkey-io/valkey-glide/pull/4841))
2223

2324
## 2.1
2425

glide-core/redis-rs/redis/src/client.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,11 @@ impl Client {
583583
pub fn update_database(&mut self, database_id: i64) {
584584
self.connection_info.redis.db = database_id;
585585
}
586+
587+
/// Updates the client_name in connection_info.
588+
pub fn update_client_name(&mut self, client_name: Option<String>) {
589+
self.connection_info.redis.client_name = client_name;
590+
}
586591
}
587592

588593
#[cfg(feature = "aio")]

glide-core/redis-rs/redis/src/cluster_async/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,15 @@ where
338338
.await
339339
}
340340

341+
/// Update the name used for all cluster connections
342+
pub async fn update_connection_client_name(
343+
&mut self,
344+
client_name: Option<String>,
345+
) -> RedisResult<Value> {
346+
self.route_operation_request(Operation::UpdateConnectionClientName(client_name))
347+
.await
348+
}
349+
341350
/// Get the username used to authenticate with all cluster servers
342351
pub async fn get_username(&mut self) -> RedisResult<Value> {
343352
self.route_operation_request(Operation::GetUsername).await
@@ -662,6 +671,7 @@ enum CmdArg<C> {
662671
enum Operation {
663672
UpdateConnectionPassword(Option<String>),
664673
UpdateConnectionDatabase(i64),
674+
UpdateConnectionClientName(Option<String>),
665675
GetUsername,
666676
}
667677

@@ -2604,6 +2614,11 @@ where
26042614
.expect(MUTEX_WRITE_ERR);
26052615
Ok(Response::Single(Value::Okay))
26062616
}
2617+
Operation::UpdateConnectionClientName(client_name) => {
2618+
core.set_cluster_param(|params| params.client_name = client_name)
2619+
.expect(MUTEX_WRITE_ERR);
2620+
Ok(Response::Single(Value::Okay))
2621+
}
26072622
Operation::GetUsername => {
26082623
let username = match core
26092624
.get_cluster_param(|params| params.username.clone())

glide-core/src/client/mod.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,58 @@ impl Client {
399399
}
400400
}
401401

402+
/// Checks if the given command is a CLIENT SETNAME command.
403+
/// Returns true if the command is "CLIENT SETNAME", false otherwise.
404+
fn is_client_set_name_command(&self, cmd: &Cmd) -> bool {
405+
// Check if the command is "CLIENT SETNAME"
406+
cmd.command()
407+
.is_some_and(|bytes| bytes == b"CLIENT SETNAME")
408+
}
409+
410+
/// Extracts the client name from a CLIENT SETNAME command.
411+
/// Parses the client name argument from the CLIENT SETNAME command.
412+
/// Returns None if the argument is missing or invalid.
413+
fn extract_client_name_from_client_set_name(&self, cmd: &Cmd) -> Option<String> {
414+
// For redis::cmd("CLIENT").arg("SETNAME").arg("name")
415+
// the client name is at arg_idx(2) (after "SETNAME")
416+
cmd.arg_idx(2).and_then(|name_bytes| {
417+
std::str::from_utf8(name_bytes)
418+
.ok()
419+
.map(|name_str| name_str.to_string())
420+
})
421+
}
422+
423+
/// Handles CLIENT SETNAME command processing after successful execution.
424+
/// Updates connection name state for standalone, cluster, and lazy clients.
425+
async fn handle_client_set_name_command(&mut self, cmd: &Cmd) -> RedisResult<()> {
426+
// Extract client name from the CLIENT SETNAME command
427+
let client_name = self.extract_client_name_from_client_set_name(cmd);
428+
429+
// Update client name state for all client types
430+
self.update_stored_client_name(client_name).await?;
431+
Ok(())
432+
}
433+
434+
/// Updates the stored client name for different client types.
435+
/// Handles standalone, cluster, and lazy clients appropriately.
436+
/// Ensures thread-safe updates using existing synchronization mechanisms.
437+
async fn update_stored_client_name(&self, client_name: Option<String>) -> RedisResult<()> {
438+
let mut guard = self.internal_client.write().await;
439+
match &mut *guard {
440+
ClientWrapper::Standalone(client) => {
441+
client.update_connection_client_name(client_name).await?;
442+
Ok(())
443+
}
444+
ClientWrapper::Cluster { client } => {
445+
// Update cluster connection database configuration
446+
client.update_connection_client_name(client_name).await?;
447+
Ok(())
448+
}
449+
ClientWrapper::Lazy(_) => {
450+
unreachable!("Lazy client should have been initialized")
451+
}
452+
}
453+
}
402454
async fn get_or_initialize_client(&self) -> RedisResult<ClientWrapper> {
403455
{
404456
let guard = self.internal_client.read().await;
@@ -496,6 +548,12 @@ impl Client {
496548
})
497549
.await?;
498550

551+
// Intercept CLIENT SETNAME commands after regular processing
552+
// Only handle CLIENT SETNAME commands if they executed successfully (no error)
553+
if self.is_client_set_name_command(cmd) {
554+
self.handle_client_set_name_command(cmd).await?;
555+
}
556+
499557
// Intercept SELECT commands after regular processing
500558
// Only handle SELECT commands if they executed successfully (no error)
501559
if self.is_select_command(cmd) {
@@ -1522,4 +1580,54 @@ mod tests {
15221580
inflight_requests_allowed: Arc::new(AtomicIsize::new(1000)),
15231581
}
15241582
}
1583+
1584+
#[test]
1585+
fn test_is_client_set_name_command() {
1586+
// Create a mock client for testing
1587+
let client = create_test_client();
1588+
1589+
// Test valid CLIENT SETNAME command
1590+
let mut cmd = Cmd::new();
1591+
cmd.arg("CLIENT").arg("SETNAME").arg("test_client");
1592+
assert!(client.is_client_set_name_command(&cmd));
1593+
1594+
// Test CLIENT SETNAME with different case (should work due to case-insensitive comparison)
1595+
let mut cmd = Cmd::new();
1596+
cmd.arg("client").arg("setname").arg("test_client");
1597+
assert!(client.is_client_set_name_command(&cmd));
1598+
1599+
// Test CLIENT command without SETNAME
1600+
let mut cmd = Cmd::new();
1601+
cmd.arg("CLIENT").arg("INFO");
1602+
assert!(!client.is_client_set_name_command(&cmd));
1603+
1604+
// Test non-CLIENT command
1605+
let mut cmd = Cmd::new();
1606+
cmd.arg("SET").arg("key").arg("value");
1607+
assert!(!client.is_client_set_name_command(&cmd));
1608+
1609+
// Test CLIENT SETNAME without client name argument
1610+
let mut cmd = Cmd::new();
1611+
cmd.arg("CLIENT").arg("SETNAME");
1612+
assert!(client.is_client_set_name_command(&cmd));
1613+
1614+
// Test CLIENT only
1615+
let mut cmd = Cmd::new();
1616+
cmd.arg("CLIENT");
1617+
assert!(!client.is_client_set_name_command(&cmd));
1618+
}
1619+
1620+
#[test]
1621+
fn test_extract_client_name_from_client_set_name() {
1622+
// Test detection of valid CLIENT SETNAME commands
1623+
let client = create_test_client();
1624+
1625+
// Test uppercase CLIENT SETNAME command
1626+
let mut cmd = Cmd::new();
1627+
cmd.arg("CLIENT").arg("SETNAME").arg("test_name");
1628+
assert_eq!(
1629+
client.extract_client_name_from_client_set_name(&cmd),
1630+
Some("test_name".to_string())
1631+
);
1632+
}
15251633
}

glide-core/src/client/reconnecting_connection.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,17 @@ impl ReconnectingConnection {
416416
client.update_database(new_database_id);
417417
}
418418

419+
/// Updates the client name that's saved inside connection_info, that will be used in case of disconnection from the server.
420+
pub(crate) fn update_connection_client_name(&self, new_client_name: Option<String>) {
421+
let mut client = self
422+
.inner
423+
.backend
424+
.connection_info
425+
.write()
426+
.expect(WRITE_LOCK_ERR);
427+
client.update_client_name(new_client_name);
428+
}
429+
419430
/// Returns the username if one was configured during client creation. Otherwise, returns None.
420431
pub(crate) fn get_username(&self) -> Option<String> {
421432
let client = self.inner.backend.get_backend_client();

glide-core/src/client/standalone_client.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,18 @@ impl StandaloneClient {
632632
Ok(Value::Okay)
633633
}
634634

635+
/// Update the client_name used to create the connection.
636+
pub async fn update_connection_client_name(
637+
&self,
638+
new_client_name: Option<String>,
639+
) -> RedisResult<Value> {
640+
for node in self.inner.nodes.iter() {
641+
node.update_connection_client_name(new_client_name.clone());
642+
}
643+
644+
Ok(Value::Okay)
645+
}
646+
635647
/// Retrieve the username used to authenticate with the server.
636648
pub fn get_username(&self) -> Option<String> {
637649
// All nodes in the client should have the same username configured, thus any connection would work here.

glide-core/tests/test_client.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,4 +1667,138 @@ pub(crate) mod shared_client_tests {
16671667
}
16681668
});
16691669
}
1670+
1671+
#[rstest]
1672+
#[serial_test::serial]
1673+
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
1674+
/// Test that verifies the client maintains the correct client name after an automatic reconnection
1675+
/// when the client name was changed using the CLIENT SETNAME command.
1676+
/// This test:
1677+
/// 1. Creates a client with a name "1stName"
1678+
/// 2. Uses CLIENT SETNAME command to change to "2ndName"
1679+
/// 3. Verifies the connection is now with the "2ndName"
1680+
/// 4. Simulates a connection drop by killing the connection
1681+
/// 5. Sends another command which either:
1682+
/// - Fails due to the dropped connection, then retries and verifies reconnection has the name "2ndName" set
1683+
/// - Succeeds with a new client ID (indicating reconnection) and verifies still has the name "2ndName" set
1684+
/// This ensures that client name via CLIENT SETNAME command persists across reconnections.
1685+
fn test_client_set_name_command_persistence_after_reconnection(
1686+
#[values(false, true)] use_cluster: bool,
1687+
) {
1688+
block_on_all(async move {
1689+
let mut test_basics = setup_test_basics(
1690+
use_cluster,
1691+
TestConfiguration {
1692+
client_name: Some("1stName".to_string()),
1693+
shared_server: true,
1694+
..Default::default()
1695+
},
1696+
)
1697+
.await;
1698+
1699+
let mut client_info_cmd = redis::Cmd::new();
1700+
client_info_cmd.arg("CLIENT").arg("INFO");
1701+
1702+
let mut client_setname_cmd = redis::Cmd::new();
1703+
client_setname_cmd
1704+
.arg("Client")
1705+
.arg("SETNAME")
1706+
.arg("2ndName");
1707+
1708+
// Verify initial connection client name
1709+
let initial_client_info_response = test_basics
1710+
.client
1711+
.send_command(&client_info_cmd, None)
1712+
.await
1713+
.unwrap();
1714+
1715+
let initial_client_info = match initial_client_info_response {
1716+
Value::BulkString(bytes) => String::from_utf8_lossy(&bytes).to_string(),
1717+
Value::VerbatimString { text, .. } => text,
1718+
_ => panic!(
1719+
"Unexpected CLIENT INFO response type: {:?}",
1720+
initial_client_info_response
1721+
),
1722+
};
1723+
assert!(initial_client_info.contains("name=1stName"));
1724+
1725+
// Extract initial client ID
1726+
let initial_client_id = utilities::extract_client_id(&initial_client_info)
1727+
.expect("Failed to extract initial client ID");
1728+
1729+
// Execute CLIENT SETNAME command to change to 2ndName
1730+
let client_setname_result = test_basics
1731+
.client
1732+
.send_command(&client_setname_cmd, None)
1733+
.await
1734+
.unwrap();
1735+
assert_eq!(client_setname_result, Value::Okay);
1736+
1737+
// Verify we're now on 2ndName
1738+
let post_client_setname_client_info_response = test_basics
1739+
.client
1740+
.send_command(&client_info_cmd, None)
1741+
.await
1742+
.unwrap();
1743+
1744+
let post_client_setname_client_info = match post_client_setname_client_info_response {
1745+
Value::BulkString(bytes) => String::from_utf8_lossy(&bytes).to_string(),
1746+
Value::VerbatimString { text, .. } => text,
1747+
_ => panic!(
1748+
"Unexpected CLIENT INFO response type: {:?}",
1749+
post_client_setname_client_info_response
1750+
),
1751+
};
1752+
assert!(post_client_setname_client_info.contains("name=2ndName"));
1753+
1754+
// Kill the connection to simulate a network drop
1755+
kill_connection(&mut test_basics.client).await;
1756+
1757+
// Try to send another command - this should trigger reconnection
1758+
let res = test_basics
1759+
.client
1760+
.send_command(&client_info_cmd, None)
1761+
.await;
1762+
match res {
1763+
Err(err) => {
1764+
// Connection was dropped as expected
1765+
assert!(
1766+
err.is_connection_dropped() || err.is_timeout(),
1767+
"Expected connection dropped or timeout error, got: {err:?}",
1768+
);
1769+
// Retry and verify we're still on name 2ndName after reconnection
1770+
let client_info = repeat_try_create(|| async {
1771+
let mut client = test_basics.client.clone();
1772+
let response = client.send_command(&client_info_cmd, None).await.ok()?;
1773+
match response {
1774+
Value::BulkString(bytes) => {
1775+
Some(String::from_utf8_lossy(&bytes).to_string())
1776+
}
1777+
Value::VerbatimString { text, .. } => Some(text),
1778+
_ => None,
1779+
}
1780+
})
1781+
.await;
1782+
assert!(client_info.contains("name=2ndName"));
1783+
}
1784+
Ok(response) => {
1785+
// Command succeeded, extract new client ID and compare
1786+
let new_client_info = match response {
1787+
Value::BulkString(bytes) => String::from_utf8_lossy(&bytes).to_string(),
1788+
Value::VerbatimString { text, .. } => text,
1789+
_ => panic!("Unexpected CLIENT INFO response type: {:?}", response),
1790+
};
1791+
let new_client_id = utilities::extract_client_id(&new_client_info)
1792+
.expect("Failed to extract new client ID");
1793+
assert_ne!(
1794+
initial_client_id, new_client_id,
1795+
"Client ID should change after reconnection if command succeeds"
1796+
);
1797+
// Check that the client name is still 2ndName (from CLIENT SETNAME command)
1798+
println!("{}", new_client_info);
1799+
assert!(new_client_info.contains("name=2ndName"));
1800+
}
1801+
}
1802+
});
1803+
}
16701804
}

0 commit comments

Comments
 (0)