@@ -21,6 +21,7 @@ import {
2121 GlideString ,
2222 ListDirection ,
2323 ProtocolVersion ,
24+ ReadFrom ,
2425 RequestError ,
2526 Script ,
2627 Transaction ,
@@ -1500,7 +1501,253 @@ describe("GlideClient", () => {
15001501 }
15011502 } ,
15021503 ) ;
1504+ describe ( "GlideClient - AZAffinity Read Strategy Test" , ( ) => {
1505+ it . each ( [ ProtocolVersion . RESP2 , ProtocolVersion . RESP3 ] ) (
1506+ "should route GET commands to all replicas with the same AZ using protocol %p" ,
1507+ async ( protocol ) => {
1508+ const az = "us-east-1a" ;
1509+ const GET_CALLS = 3 ;
1510+ const get_cmdstat = `cmdstat_get:calls=${ GET_CALLS } ` ;
15031511
1512+ let client_for_config_set ;
1513+ let client_for_testing_az ;
1514+
1515+ try {
1516+ // Stage 1: Configure nodes
1517+ client_for_config_set = await GlideClient . createClient (
1518+ getClientConfigurationOption (
1519+ cluster . getAddresses ( ) ,
1520+ protocol ,
1521+ ) ,
1522+ ) ;
1523+
1524+ // Skip test if version is below 8.0.0
1525+ if ( cluster . checkIfServerVersionLessThan ( "8.0.0" ) ) {
1526+ console . log (
1527+ "Skipping test: requires Valkey 8.0.0 or higher" ,
1528+ ) ;
1529+ return ;
1530+ }
1531+
1532+ await client_for_config_set . customCommand ( [
1533+ "CONFIG" ,
1534+ "RESETSTAT" ,
1535+ ] ) ;
1536+ await client_for_config_set . customCommand ( [
1537+ "CONFIG" ,
1538+ "SET" ,
1539+ "availability-zone" ,
1540+ az ,
1541+ ] ) ;
1542+
1543+ // Stage 2: Create AZ affinity client and verify configuration
1544+ client_for_testing_az = await GlideClient . createClient (
1545+ getClientConfigurationOption (
1546+ cluster . getAddresses ( ) ,
1547+ protocol ,
1548+ {
1549+ readFrom : "AZAffinity" as ReadFrom ,
1550+ clientAz : az ,
1551+ } ,
1552+ ) ,
1553+ ) ;
1554+
1555+ const azs = await client_for_testing_az . customCommand ( [
1556+ "CONFIG" ,
1557+ "GET" ,
1558+ "availability-zone" ,
1559+ ] ) ;
1560+
1561+ if ( Array . isArray ( azs ) && azs . length > 0 ) {
1562+ const configItem = azs [ 0 ] as {
1563+ key : string ;
1564+ value : string ;
1565+ } ;
1566+
1567+ if (
1568+ configItem &&
1569+ configItem . key === "availability-zone"
1570+ ) {
1571+ expect ( configItem . value ) . toBe ( az ) ;
1572+ } else {
1573+ throw new Error ( "Invalid config item format" ) ;
1574+ }
1575+ } else {
1576+ throw new Error (
1577+ "Unexpected response format from CONFIG GET command" ,
1578+ ) ;
1579+ }
1580+
1581+ // Stage 3: Set test data and perform GET operations
1582+ await client_for_testing_az . set ( "foo" , "testvalue" ) ;
1583+
1584+ for ( let i = 0 ; i < GET_CALLS ; i ++ ) {
1585+ await client_for_testing_az . get ( "foo" ) ;
1586+ }
1587+
1588+ // Stage 4: Verify GET commands were routed correctly
1589+ const info_result =
1590+ await client_for_testing_az . customCommand ( [
1591+ "INFO" ,
1592+ "COMMANDSTATS" ,
1593+ ] ) ;
1594+
1595+ if (
1596+ typeof info_result === "string" &&
1597+ info_result . includes ( get_cmdstat )
1598+ ) {
1599+ expect ( info_result ) . toContain ( get_cmdstat ) ;
1600+ } else {
1601+ throw new Error (
1602+ "Unexpected response format from INFO command in standalone mode" ,
1603+ ) ;
1604+ }
1605+ } finally {
1606+ // Cleanup
1607+ await client_for_config_set ?. close ( ) ;
1608+ await client_for_testing_az ?. close ( ) ;
1609+ }
1610+ } ,
1611+ ) ;
1612+ } ) ;
1613+ describe ( "GlideClient - AZAffinity Routing to 1 replica" , ( ) => {
1614+ it . each ( [ ProtocolVersion . RESP2 , ProtocolVersion . RESP3 ] ) (
1615+ "should route commands to single replica with AZ using protocol %p" ,
1616+ async ( protocol ) => {
1617+ const az = "us-east-1a" ;
1618+ const GET_CALLS = 3 ;
1619+ const get_cmdstat = `calls=${ GET_CALLS } ` ;
1620+
1621+ let client_for_config_set ;
1622+ let client_for_testing_az ;
1623+
1624+ try {
1625+ // Stage 1: Configure nodes
1626+ client_for_config_set = await GlideClient . createClient (
1627+ getClientConfigurationOption (
1628+ cluster . getAddresses ( ) ,
1629+ protocol ,
1630+ ) ,
1631+ ) ;
1632+
1633+ // Skip test if version is below 8.0.0
1634+ if ( cluster . checkIfServerVersionLessThan ( "8.0.0" ) ) {
1635+ console . log (
1636+ "Skipping test: requires Valkey 8.0.0 or higher" ,
1637+ ) ;
1638+ return ;
1639+ }
1640+
1641+ await client_for_config_set . customCommand ( [
1642+ "CONFIG" ,
1643+ "SET" ,
1644+ "availability-zone" ,
1645+ az ,
1646+ ] ) ;
1647+
1648+ await client_for_config_set . customCommand ( [
1649+ "CONFIG" ,
1650+ "RESETSTAT" ,
1651+ ] ) ;
1652+
1653+ // Stage 2: Create AZ affinity client and verify configuration
1654+ client_for_testing_az = await GlideClient . createClient (
1655+ getClientConfigurationOption (
1656+ cluster . getAddresses ( ) ,
1657+ protocol ,
1658+ {
1659+ readFrom : "AZAffinity" ,
1660+ clientAz : az ,
1661+ } ,
1662+ ) ,
1663+ ) ;
1664+ await client_for_testing_az . set ( "foo" , "testvalue" ) ;
1665+
1666+ for ( let i = 0 ; i < GET_CALLS ; i ++ ) {
1667+ await client_for_testing_az . get ( "foo" ) ;
1668+ }
1669+
1670+ // Stage 4: Verify GET commands were routed correctly
1671+ const info_result =
1672+ await client_for_testing_az . customCommand ( [
1673+ "INFO" ,
1674+ "ALL" ,
1675+ ] ) ;
1676+
1677+ if ( typeof info_result === "string" ) {
1678+ expect ( info_result ) . toContain ( get_cmdstat ) ;
1679+ expect ( info_result ) . toContain (
1680+ `availability_zone:${ az } ` ,
1681+ ) ;
1682+ } else {
1683+ throw new Error (
1684+ "Unexpected response format from INFO COMMANDSTATS command" ,
1685+ ) ;
1686+ }
1687+ } finally {
1688+ await client_for_config_set ?. close ( ) ;
1689+ await client_for_testing_az ?. close ( ) ;
1690+ }
1691+ } ,
1692+ ) ;
1693+ } ) ;
1694+ describe ( "GlideClient - AZAffinity with Non-existing AZ" , ( ) => {
1695+ it . each ( [ ProtocolVersion . RESP2 , ProtocolVersion . RESP3 ] ) (
1696+ "should route commands to a replica when AZ does not exist using protocol %p" ,
1697+ async ( protocol ) => {
1698+ const GET_CALLS = 4 ;
1699+ const get_cmdstat = `cmdstat_get:calls=${ GET_CALLS } ` ;
1700+
1701+ let client_for_testing_az ;
1702+
1703+ try {
1704+ if ( cluster . checkIfServerVersionLessThan ( "8.0.0" ) ) {
1705+ console . log (
1706+ "Skipping test: requires Valkey 8.0.0 or higher" ,
1707+ ) ;
1708+ return ;
1709+ }
1710+
1711+ client_for_testing_az = await GlideClient . createClient (
1712+ getClientConfigurationOption (
1713+ cluster . getAddresses ( ) ,
1714+ protocol ,
1715+ {
1716+ readFrom : "AZAffinity" ,
1717+ clientAz : "non-existing-az" ,
1718+ requestTimeout : 2000 ,
1719+ } ,
1720+ ) ,
1721+ ) ;
1722+
1723+ await client_for_testing_az . customCommand ( [
1724+ "CONFIG" ,
1725+ "RESETSTAT" ,
1726+ ] ) ;
1727+
1728+ for ( let i = 0 ; i < GET_CALLS ; i ++ ) {
1729+ await client_for_testing_az . get ( "foo" ) ;
1730+ }
1731+
1732+ const info_result =
1733+ await client_for_testing_az . customCommand ( [
1734+ "INFO" ,
1735+ "COMMANDSTATS" ,
1736+ ] ) ;
1737+
1738+ if ( typeof info_result === "string" ) {
1739+ expect ( info_result ) . toContain ( get_cmdstat ) ;
1740+ } else {
1741+ throw new Error (
1742+ "Unexpected response format from INFO command" ,
1743+ ) ;
1744+ }
1745+ } finally {
1746+ await client_for_testing_az ?. close ( ) ;
1747+ }
1748+ } ,
1749+ ) ;
1750+ } ) ;
15041751 runBaseTests ( {
15051752 init : async ( protocol , configOverrides ) => {
15061753 const config = getClientConfigurationOption (
0 commit comments