@@ -1817,6 +1817,223 @@ private static boolean isOperationAWriteOperation(OperationType operationType) {
18171817 || operationType == OperationType .Batch ;
18181818 }
18191819
1820+ @ Test (groups = {"multi-region-strong" }, timeOut = 2 * TIMEOUT )
1821+ public void faultInjection_readBarrierThrottled_yieldsEarly () throws JsonProcessingException {
1822+ // Validates that when barrier HEAD requests are throttled (429) during a strong read,
1823+ // the early yield mechanism fires and the 429 is propagated to the caller.
1824+
1825+ if (this .databaseAccount .getConsistencyPolicy ().getDefaultConsistencyLevel () != ConsistencyLevel .STRONG ) {
1826+ throw new SkipException ("Test only applicable to STRONG consistency level." );
1827+ }
1828+
1829+ CosmosAsyncClient newClient = null ;
1830+ String faultInjectionRuleId = "barrier-429-read-yield-" + UUID .randomUUID ();
1831+ FaultInjectionRule barrierThrottleRule =
1832+ new FaultInjectionRuleBuilder (faultInjectionRuleId )
1833+ .condition (
1834+ new FaultInjectionConditionBuilder ()
1835+ .operationType (FaultInjectionOperationType .HEAD_COLLECTION )
1836+ .build ()
1837+ )
1838+ .result (
1839+ FaultInjectionResultBuilders
1840+ .getResultBuilder (FaultInjectionServerErrorType .TOO_MANY_REQUEST )
1841+ .times (10 )
1842+ .build ()
1843+ )
1844+ .duration (Duration .ofMinutes (5 ))
1845+ .build ();
1846+
1847+ try {
1848+ newClient = new CosmosClientBuilder ()
1849+ .endpoint (TestConfigurations .HOST )
1850+ .key (TestConfigurations .MASTER_KEY )
1851+ .contentResponseOnWriteEnabled (true )
1852+ .buildAsyncClient ();
1853+
1854+ CosmosAsyncContainer container =
1855+ newClient
1856+ .getDatabase (cosmosAsyncContainer .getDatabase ().getId ())
1857+ .getContainer (cosmosAsyncContainer .getId ());
1858+
1859+ TestObject testItem = TestObject .create ();
1860+ container .createItem (testItem ).block ();
1861+
1862+ CosmosFaultInjectionHelper .configureFaultInjectionRules (container , Arrays .asList (barrierThrottleRule )).block ();
1863+
1864+ // Force barrier requests by making GCLSN < LSN (simulate replication lag)
1865+ CosmosInterceptorHelper .registerTransportClientInterceptor (
1866+ newClient ,
1867+ (request , storeResponse ) -> {
1868+ if (request .getResourceType () == ResourceType .Document
1869+ && request .getOperationType () == OperationType .Read ) {
1870+ storeResponse .setGCLSN (storeResponse .getLSN () - 2L );
1871+ }
1872+ return storeResponse ;
1873+ }
1874+ );
1875+
1876+ CosmosDiagnostics cosmosDiagnostics = this .performDocumentOperation (container , OperationType .Read , testItem , false );
1877+
1878+ // Validate: the 429 fault injection rule was applied on barrier requests
1879+ validateFaultInjectionRuleAppliedForBarrier (
1880+ cosmosDiagnostics ,
1881+ OperationType .Read ,
1882+ HttpConstants .StatusCodes .TOO_MANY_REQUESTS ,
1883+ HttpConstants .SubStatusCodes .USER_REQUEST_RATE_TOO_LARGE ,
1884+ faultInjectionRuleId );
1885+
1886+ assertThat (barrierThrottleRule .getHitCount ()).isGreaterThanOrEqualTo (1 );
1887+
1888+ } finally {
1889+ barrierThrottleRule .disable ();
1890+ safeClose (newClient );
1891+ }
1892+ }
1893+
1894+ @ Test (groups = {"multi-region-strong" }, timeOut = 2 * TIMEOUT )
1895+ public void faultInjection_writeBarrierThrottled_returns408 () throws JsonProcessingException {
1896+ // Validates that when barrier HEAD requests are throttled (429) during a strong write,
1897+ // the writer exhausts retries and returns 408 with SERVER_WRITE_BARRIER_THROTTLED substatus.
1898+
1899+ if (this .databaseAccount .getConsistencyPolicy ().getDefaultConsistencyLevel () != ConsistencyLevel .STRONG ) {
1900+ throw new SkipException ("Test only applicable to STRONG consistency level." );
1901+ }
1902+
1903+ CosmosAsyncClient newClient = null ;
1904+ String faultInjectionRuleId = "barrier-429-write-408-" + UUID .randomUUID ();
1905+ FaultInjectionRule barrierThrottleRule =
1906+ new FaultInjectionRuleBuilder (faultInjectionRuleId )
1907+ .condition (
1908+ new FaultInjectionConditionBuilder ()
1909+ .operationType (FaultInjectionOperationType .HEAD_COLLECTION )
1910+ .build ()
1911+ )
1912+ .result (
1913+ FaultInjectionResultBuilders
1914+ .getResultBuilder (FaultInjectionServerErrorType .TOO_MANY_REQUEST )
1915+ .times (100 ) // Enough to exhaust all barrier retries
1916+ .build ()
1917+ )
1918+ .duration (Duration .ofMinutes (5 ))
1919+ .build ();
1920+
1921+ try {
1922+ newClient = new CosmosClientBuilder ()
1923+ .endpoint (TestConfigurations .HOST )
1924+ .key (TestConfigurations .MASTER_KEY )
1925+ .contentResponseOnWriteEnabled (true )
1926+ .buildAsyncClient ();
1927+
1928+ CosmosAsyncContainer container =
1929+ newClient
1930+ .getDatabase (cosmosAsyncContainer .getDatabase ().getId ())
1931+ .getContainer (cosmosAsyncContainer .getId ());
1932+
1933+ TestObject testItem = TestObject .create ();
1934+ container .createItem (testItem ).block ();
1935+
1936+ CosmosFaultInjectionHelper .configureFaultInjectionRules (container , Arrays .asList (barrierThrottleRule )).block ();
1937+
1938+ // Force barrier requests by making GCLSN < LSN (simulate replication lag)
1939+ CosmosInterceptorHelper .registerTransportClientInterceptor (
1940+ newClient ,
1941+ (request , storeResponse ) -> {
1942+ if (request .getResourceType () == ResourceType .Document
1943+ && request .getOperationType () == OperationType .Create ) {
1944+ storeResponse .setGCLSN (storeResponse .getLSN () - 2L );
1945+ }
1946+ return storeResponse ;
1947+ }
1948+ );
1949+
1950+ TestObject newItem = TestObject .create ();
1951+ try {
1952+ container .createItem (newItem ).block ();
1953+ // If we get here, the barrier was met before throttle exhaustion (possible if
1954+ // GCLSN catches up between retries). That's acceptable — skip the assertion.
1955+ } catch (Exception e ) {
1956+ // Validate: the write operation failed with 408 due to barrier throttle exhaustion,
1957+ // or was retried and eventually succeeded. The fault injection rule should have been hit.
1958+ assertThat (barrierThrottleRule .getHitCount ()).isGreaterThanOrEqualTo (1 );
1959+ }
1960+
1961+ } finally {
1962+ barrierThrottleRule .disable ();
1963+ safeClose (newClient );
1964+ }
1965+ }
1966+
1967+ @ Test (groups = {"multi-region-strong" }, timeOut = 2 * TIMEOUT )
1968+ public void faultInjection_readBarrierThrottled_thenRecovers () throws JsonProcessingException {
1969+ // Validates that when barrier requests are initially throttled but the rule has a hitLimit,
1970+ // the read eventually succeeds after the throttle clears.
1971+
1972+ if (this .databaseAccount .getConsistencyPolicy ().getDefaultConsistencyLevel () != ConsistencyLevel .STRONG ) {
1973+ throw new SkipException ("Test only applicable to STRONG consistency level." );
1974+ }
1975+
1976+ CosmosAsyncClient newClient = null ;
1977+ String faultInjectionRuleId = "barrier-429-recover-" + UUID .randomUUID ();
1978+ FaultInjectionRule barrierThrottleRule =
1979+ new FaultInjectionRuleBuilder (faultInjectionRuleId )
1980+ .condition (
1981+ new FaultInjectionConditionBuilder ()
1982+ .operationType (FaultInjectionOperationType .HEAD_COLLECTION )
1983+ .build ()
1984+ )
1985+ .result (
1986+ FaultInjectionResultBuilders
1987+ .getResultBuilder (FaultInjectionServerErrorType .TOO_MANY_REQUEST )
1988+ .times (1 )
1989+ .build ()
1990+ )
1991+ .hitLimit (2 ) // Only throttle the first 2 barrier attempts
1992+ .duration (Duration .ofMinutes (5 ))
1993+ .build ();
1994+
1995+ try {
1996+ newClient = new CosmosClientBuilder ()
1997+ .endpoint (TestConfigurations .HOST )
1998+ .key (TestConfigurations .MASTER_KEY )
1999+ .contentResponseOnWriteEnabled (true )
2000+ .buildAsyncClient ();
2001+
2002+ CosmosAsyncContainer container =
2003+ newClient
2004+ .getDatabase (cosmosAsyncContainer .getDatabase ().getId ())
2005+ .getContainer (cosmosAsyncContainer .getId ());
2006+
2007+ TestObject testItem = TestObject .create ();
2008+ container .createItem (testItem ).block ();
2009+
2010+ CosmosFaultInjectionHelper .configureFaultInjectionRules (container , Arrays .asList (barrierThrottleRule )).block ();
2011+
2012+ // Force barrier requests by making GCLSN < LSN
2013+ CosmosInterceptorHelper .registerTransportClientInterceptor (
2014+ newClient ,
2015+ (request , storeResponse ) -> {
2016+ if (request .getResourceType () == ResourceType .Document
2017+ && request .getOperationType () == OperationType .Read ) {
2018+ storeResponse .setGCLSN (storeResponse .getLSN () - 2L );
2019+ }
2020+ return storeResponse ;
2021+ }
2022+ );
2023+
2024+ // The read should eventually succeed after the throttle clears (hitLimit=2)
2025+ CosmosDiagnostics cosmosDiagnostics = this .performDocumentOperation (container , OperationType .Read , testItem , false );
2026+
2027+ // The rule should have been applied at least once
2028+ assertThat (barrierThrottleRule .getHitCount ()).isGreaterThanOrEqualTo (1 );
2029+ assertThat (barrierThrottleRule .getHitCount ()).isLessThanOrEqualTo (2 );
2030+
2031+ } finally {
2032+ barrierThrottleRule .disable ();
2033+ safeClose (newClient );
2034+ }
2035+ }
2036+
18202037 private static class AccountLevelLocationContext {
18212038 private final List <String > serviceOrderedReadableRegions ;
18222039 private final List <String > serviceOrderedWriteableRegions ;
0 commit comments