Skip to content

Commit 242ba44

Browse files
authored
Wire PPAF-enablement flag from database account payload from Gateway. (#45317)
* Wire PPAF-enablement flag from database account payload from Gateway. * Wire PPAF-enablement flag from database account payload from Gateway. * Wire PPAF-enablement flag from database account payload from Gateway. * Wire PPAF-enablement flag from database account payload from Gateway. * Fix javadoc * Refactoring * Refactoring
1 parent f28f11b commit 242ba44

File tree

11 files changed

+141
-68
lines changed

11 files changed

+141
-68
lines changed

sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/PerPartitionAutomaticFailoverE2ETests.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,7 @@ public Object[][] ppafTestConfigsWithWriteOps() {
680680
ONLY_GATEWAY_MODE
681681
},
682682
{
683-
"Test failover handling for CREATE when REQUEST_TIMEOUT / GATEWAY_ENDPOINT_READ_TIMEOUT is injected into first preferred region for a specific server partition.",
683+
"Test failover handling for CREATE when SERVICE_UNAVAILABLE / GATEWAY_ENDPOINT_UNAVAILABLE is injected into first preferred region for a specific server partition.",
684684
OperationType.Create,
685685
HttpConstants.StatusCodes.SERVICE_UNAVAILABLE,
686686
HttpConstants.SubStatusCodes.GATEWAY_ENDPOINT_UNAVAILABLE,
@@ -961,9 +961,9 @@ public void testPpafWithWriteFailoverWithEligibleErrorStatusCodes(
961961
System.setProperty("COSMOS.E2E_TIMEOUT_ERROR_HIT_THRESHOLD_FOR_PPAF", "2");
962962
}
963963

964-
CosmosClientBuilder cosmosClientBuilder = getClientBuilder()
965-
.perPartitionAutomaticFailoverEnabled(true)
966-
.preferredRegions(preferredRegions);
964+
System.setProperty("COSMOS.IS_PER_PARTITION_AUTOMATIC_FAILOVER_ENABLED", "true");
965+
966+
CosmosClientBuilder cosmosClientBuilder = getClientBuilder();
967967

968968
// todo: evaluate whether Batch operation needs op-level e2e timeout and availability strategy
969969
if (operationType.equals(OperationType.Batch) && shouldUseE2ETimeout) {
@@ -1044,6 +1044,7 @@ public void testPpafWithWriteFailoverWithEligibleErrorStatusCodes(
10441044
Assertions.fail("The test ran into an exception {}", e);
10451045
} finally {
10461046
System.clearProperty("COSMOS.E2E_TIMEOUT_ERROR_HIT_THRESHOLD_FOR_PPAF");
1047+
System.clearProperty("COSMOS.IS_PER_PARTITION_AUTOMATIC_FAILOVER_ENABLED");
10471048
safeClose(cosmosAsyncClientValueHolder.v);
10481049
}
10491050
}
@@ -1060,9 +1061,9 @@ public void testPpafWithWriteFailoverWithEligibleErrorStatusCodes(
10601061
System.setProperty("COSMOS.E2E_TIMEOUT_ERROR_HIT_THRESHOLD_FOR_PPAF", "2");
10611062
}
10621063

1063-
CosmosClientBuilder cosmosClientBuilder = getClientBuilder()
1064-
.perPartitionAutomaticFailoverEnabled(true)
1065-
.preferredRegions(preferredRegions);
1064+
System.setProperty("COSMOS.IS_PER_PARTITION_AUTOMATIC_FAILOVER_ENABLED", "true");
1065+
1066+
CosmosClientBuilder cosmosClientBuilder = getClientBuilder();
10661067

10671068
// todo: evaluate whether Batch operation needs op-level e2e timeout and availability strategy
10681069
if (operationType.equals(OperationType.Batch) && shouldUseE2ETimeout) {
@@ -1139,6 +1140,7 @@ public void testPpafWithWriteFailoverWithEligibleErrorStatusCodes(
11391140
Assertions.fail("The test ran into an exception {}", e);
11401141
} finally {
11411142
System.clearProperty("COSMOS.E2E_TIMEOUT_ERROR_HIT_THRESHOLD_FOR_PPAF");
1143+
System.clearProperty("COSMOS.IS_PER_PARTITION_AUTOMATIC_FAILOVER_ENABLED");
11421144
safeClose(cosmosAsyncClientValueHolder.v);
11431145
}
11441146
}

sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/RxDocumentClientImplTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,8 @@ public void readMany() {
249249
this.sessionRetryOptionsMock,
250250
this.containerProactiveInitConfigMock,
251251
this.defaultItemSerializer,
252-
false,
253-
false);
252+
false
253+
);
254254

255255
try {
256256
ReflectionUtils.setCollectionCache(rxDocumentClient, this.collectionCacheMock);

sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/RxDocumentClientUnderTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ public RxDocumentClientUnderTest(URI serviceEndpoint,
6262
null,
6363
null,
6464
null,
65-
false,
66-
false);
65+
false
66+
);
6767
init(null, null);
6868
}
6969

sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/SpyClientUnderTestFactory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ public SpyBaseClass(
6767
null,
6868
null,
6969
null,
70-
false,
71-
false);
70+
false
71+
);
7272
}
7373

7474
public abstract List<T> getCapturedRequests();

sdk/cosmos/azure-cosmos/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* Fixed an issue where child partition is getting overridden with null continuation token if a split happens during the first request of a parent partition. - See [PR 45363](https://github.com/Azure/azure-sdk-for-java/pull/45363)
1111

1212
#### Other Changes
13+
* Added a way to opt-in into Per-Partition Automatic Failover using enablement flag from the account metadata. - [PR 45317](https://github.com/Azure/azure-sdk-for-java/pull/45317)
1314

1415
### 4.69.0 (2025-05-14)
1516

sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosClientBuilder.java

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -934,25 +934,6 @@ void resetSessionCapturingType() {
934934
}
935935
}
936936

937-
void resetIsPerPartitionAutomaticFailoverEnabledAndEnforcePerPartitionCircuitBreakerIfNeeded() {
938-
String isPerPartitionAutomaticFailoverEnabledAsStr
939-
= Configs.isPerPartitionAutomaticFailoverEnabled();
940-
941-
if (!StringUtils.isEmpty(isPerPartitionAutomaticFailoverEnabledAsStr)) {
942-
this.isPerPartitionAutomaticFailoverEnabled = Boolean.parseBoolean(isPerPartitionAutomaticFailoverEnabledAsStr);
943-
}
944-
945-
if (this.isPerPartitionAutomaticFailoverEnabled) {
946-
947-
PartitionLevelCircuitBreakerConfig partitionLevelCircuitBreakerConfig = Configs.getPartitionLevelCircuitBreakerConfig();
948-
949-
if (partitionLevelCircuitBreakerConfig != null && !partitionLevelCircuitBreakerConfig.isPartitionLevelCircuitBreakerEnabled()) {
950-
logger.info("As Per-Partition Automatic Failover is enabled, Per-Partition Circuit Breaker is enabled too by default!");
951-
System.setProperty("COSMOS.PARTITION_LEVEL_CIRCUIT_BREAKER_CONFIG", "{\"isPartitionLevelCircuitBreakerEnabled\": true}");
952-
}
953-
}
954-
}
955-
956937
/**
957938
* Sets the {@link CosmosContainerProactiveInitConfig} which enable warming up of caches and connections
958939
* associated with containers obtained from {@link CosmosContainerProactiveInitConfig#getCosmosContainerIdentities()} to replicas
@@ -1243,7 +1224,6 @@ CosmosAsyncClient buildAsyncClient(boolean logStartupInfo) {
12431224
}
12441225

12451226
this.resetSessionCapturingType();
1246-
this.resetIsPerPartitionAutomaticFailoverEnabledAndEnforcePerPartitionCircuitBreakerIfNeeded();
12471227

12481228
validateConfig();
12491229
buildConnectionPolicy();
@@ -1285,7 +1265,6 @@ public CosmosClient buildClient() {
12851265
}
12861266

12871267
this.resetSessionCapturingType();
1288-
this.resetIsPerPartitionAutomaticFailoverEnabledAndEnforcePerPartitionCircuitBreakerIfNeeded();
12891268

12901269
validateConfig();
12911270
buildConnectionPolicy();

sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Constants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ public static final class Properties {
203203
public static final String THINCLIENT_WRITABLE_LOCATIONS = "thinClientWritableLocations";
204204
public static final String THINCLIENT_READABLE_LOCATIONS = "thinClientReadableLocations";
205205
public static final String DATABASE_ACCOUNT_ENDPOINT = "databaseAccountEndpoint";
206+
public static final String ENABLE_PER_PARTITION_FAILOVER_BEHAVIOR = "enablePerPartitionFailoverBehavior";
206207

207208
//Authorization
208209
public static final String MASTER_TOKEN = "master";

sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/DatabaseAccount.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,27 @@ public void setEnableMultipleWriteLocations(boolean value) {
282282
this.set(Constants.Properties.ENABLE_MULTIPLE_WRITE_LOCATIONS, value);
283283
}
284284

285+
/**
286+
* Returns true if the account supports per partition failover behavior,
287+
* false if enablePerPartitionFailoverBehavior evaluates to null or false.
288+
* <p>
289+
* If enablePerPartitionFailoverBehavior property does not exist in account metadata JSON payload, null is returned.
290+
*
291+
* @return true if the account supports per partition failover behavior, false otherwise.
292+
*/
293+
public Boolean isPerPartitionFailoverBehaviorEnabled() {
294+
295+
if (super.has(Constants.Properties.ENABLE_PER_PARTITION_FAILOVER_BEHAVIOR)) {
296+
return ObjectUtils.defaultIfNull(super.getBoolean(Constants.Properties.ENABLE_PER_PARTITION_FAILOVER_BEHAVIOR), false);
297+
}
298+
299+
return null;
300+
}
301+
302+
public void setIsPerPartitionFailoverBehaviorEnabled(boolean value) {
303+
this.set(Constants.Properties.ENABLE_PER_PARTITION_FAILOVER_BEHAVIOR, value);
304+
}
305+
285306
public void populatePropertyBag() {
286307
super.populatePropertyBag();
287308
if (this.consistencyPolicy != null) {

sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/RxDocumentClientImpl.java

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import com.azure.cosmos.implementation.patch.PatchUtil;
5050
import com.azure.cosmos.implementation.perPartitionAutomaticFailover.GlobalPartitionEndpointManagerForPerPartitionAutomaticFailover;
5151
import com.azure.cosmos.implementation.perPartitionCircuitBreaker.GlobalPartitionEndpointManagerForPerPartitionCircuitBreaker;
52+
import com.azure.cosmos.implementation.perPartitionCircuitBreaker.PartitionLevelCircuitBreakerConfig;
5253
import com.azure.cosmos.implementation.query.DocumentQueryExecutionContextFactory;
5354
import com.azure.cosmos.implementation.query.IDocumentQueryClient;
5455
import com.azure.cosmos.implementation.query.IDocumentQueryExecutionContext;
@@ -268,7 +269,7 @@ public class RxDocumentClientImpl implements AsyncDocumentClient, IAuthorization
268269
private final boolean isRegionScopedSessionCapturingEnabledOnClientOrSystemConfig;
269270
private List<CosmosOperationPolicy> operationPolicies;
270271
private final AtomicReference<CosmosAsyncClient> cachedCosmosAsyncClientSnapshot;
271-
private final CosmosEndToEndOperationLatencyPolicyConfig ppafEnforcedE2ELatencyPolicyConfigForReads;
272+
private CosmosEndToEndOperationLatencyPolicyConfig ppafEnforcedE2ELatencyPolicyConfigForReads;
272273

273274
public RxDocumentClientImpl(URI serviceEndpoint,
274275
String masterKeyOrResourceToken,
@@ -290,8 +291,7 @@ public RxDocumentClientImpl(URI serviceEndpoint,
290291
SessionRetryOptions sessionRetryOptions,
291292
CosmosContainerProactiveInitConfig containerProactiveInitConfig,
292293
CosmosItemSerializer defaultCustomSerializer,
293-
boolean isRegionScopedSessionCapturingEnabled,
294-
boolean isPerPartitionAutomaticFailoverEnabled) {
294+
boolean isRegionScopedSessionCapturingEnabled) {
295295
this(
296296
serviceEndpoint,
297297
masterKeyOrResourceToken,
@@ -313,8 +313,8 @@ public RxDocumentClientImpl(URI serviceEndpoint,
313313
sessionRetryOptions,
314314
containerProactiveInitConfig,
315315
defaultCustomSerializer,
316-
isRegionScopedSessionCapturingEnabled,
317-
isPerPartitionAutomaticFailoverEnabled);
316+
isRegionScopedSessionCapturingEnabled
317+
);
318318

319319
this.cosmosAuthorizationTokenResolver = cosmosAuthorizationTokenResolver;
320320
}
@@ -364,8 +364,8 @@ public RxDocumentClientImpl(URI serviceEndpoint,
364364
sessionRetryOptions,
365365
containerProactiveInitConfig,
366366
defaultCustomSerializer,
367-
isRegionScopedSessionCapturingEnabled,
368-
isPerPartitionAutomaticFailoverEnabled);
367+
isRegionScopedSessionCapturingEnabled
368+
);
369369

370370
this.cosmosAuthorizationTokenResolver = cosmosAuthorizationTokenResolver;
371371
this.operationPolicies = operationPolicies;
@@ -391,8 +391,7 @@ private RxDocumentClientImpl(URI serviceEndpoint,
391391
SessionRetryOptions sessionRetryOptions,
392392
CosmosContainerProactiveInitConfig containerProactiveInitConfig,
393393
CosmosItemSerializer defaultCustomSerializer,
394-
boolean isRegionScopedSessionCapturingEnabled,
395-
boolean isPerPartitionAutomaticFailoverEnabled) {
394+
boolean isRegionScopedSessionCapturingEnabled) {
396395
this(
397396
serviceEndpoint,
398397
masterKeyOrResourceToken,
@@ -413,8 +412,8 @@ private RxDocumentClientImpl(URI serviceEndpoint,
413412
sessionRetryOptions,
414413
containerProactiveInitConfig,
415414
defaultCustomSerializer,
416-
isRegionScopedSessionCapturingEnabled,
417-
isPerPartitionAutomaticFailoverEnabled);
415+
isRegionScopedSessionCapturingEnabled
416+
);
418417

419418
if (permissionFeed != null && permissionFeed.size() > 0) {
420419
this.resourceTokensMap = new HashMap<>();
@@ -477,8 +476,7 @@ private RxDocumentClientImpl(URI serviceEndpoint,
477476
SessionRetryOptions sessionRetryOptions,
478477
CosmosContainerProactiveInitConfig containerProactiveInitConfig,
479478
CosmosItemSerializer defaultCustomSerializer,
480-
boolean isRegionScopedSessionCapturingEnabled,
481-
boolean isPerPartitionAutomaticFailoverEnabled) {
479+
boolean isRegionScopedSessionCapturingEnabled) {
482480

483481
assert(clientTelemetryConfig != null);
484482
activeClientsCnt.incrementAndGet();
@@ -583,14 +581,13 @@ private RxDocumentClientImpl(URI serviceEndpoint,
583581

584582
this.globalPartitionEndpointManagerForPerPartitionCircuitBreaker
585583
= new GlobalPartitionEndpointManagerForPerPartitionCircuitBreaker(this.globalEndpointManager);
584+
585+
// enablement of PPAF is revaluated in RxDocumentClientImpl#init
586586
this.globalPartitionEndpointManagerForPerPartitionAutomaticFailover
587-
= new GlobalPartitionEndpointManagerForPerPartitionAutomaticFailover(this.globalEndpointManager, isPerPartitionAutomaticFailoverEnabled);
587+
= new GlobalPartitionEndpointManagerForPerPartitionAutomaticFailover(this.globalEndpointManager, false);
588588

589-
this.globalPartitionEndpointManagerForPerPartitionCircuitBreaker.init();
590589
this.cachedCosmosAsyncClientSnapshot = new AtomicReference<>();
591590

592-
this.diagnosticsClientConfig.withPartitionLevelCircuitBreakerConfig(this.globalPartitionEndpointManagerForPerPartitionCircuitBreaker.getCircuitBreakerConfig());
593-
594591
this.retryPolicy = new RetryPolicy(
595592
this,
596593
this.globalEndpointManager,
@@ -602,10 +599,6 @@ private RxDocumentClientImpl(URI serviceEndpoint,
602599
this.queryPlanCache = new ConcurrentHashMap<>();
603600
this.apiType = apiType;
604601
this.clientTelemetryConfig = clientTelemetryConfig;
605-
this.ppafEnforcedE2ELatencyPolicyConfigForReads = evaluatePpafEnforcedE2eLatencyPolicyCfgForReads(
606-
this.globalPartitionEndpointManagerForPerPartitionAutomaticFailover,
607-
this.connectionPolicy
608-
);
609602
} catch (RuntimeException e) {
610603
logger.error("unexpected failure in initializing client.", e);
611604
close();
@@ -795,7 +788,7 @@ public void init(CosmosClientMetadataCachesSnapshot metadataCachesSnapshot, Func
795788
&& readConsistencyStrategy != ReadConsistencyStrategy.SESSION
796789
&& !sessionCapturingOverrideEnabled);
797790
this.sessionContainer.setDisableSessionCapturing(updatedDisableSessionCapturing);
798-
791+
this.initializePerPartitionFailover(databaseAccountSnapshot);
799792
this.addUserAgentSuffix(this.userAgentContainer, EnumSet.allOf(UserAgentFeatureFlags.class));
800793
} catch (Exception e) {
801794
logger.error("unexpected failure in initializing client.", e);
@@ -7181,7 +7174,7 @@ private static boolean isNonTransientResultForHedging(int statusCode, int subSta
71817174
return false;
71827175
}
71837176

7184-
private static CosmosEndToEndOperationLatencyPolicyConfig evaluatePpafEnforcedE2eLatencyPolicyCfgForReads(
7177+
private CosmosEndToEndOperationLatencyPolicyConfig evaluatePpafEnforcedE2eLatencyPolicyCfgForReads(
71857178
GlobalPartitionEndpointManagerForPerPartitionAutomaticFailover globalPartitionEndpointManagerForPerPartitionAutomaticFailover,
71867179
ConnectionPolicy connectionPolicy) {
71877180

@@ -7191,6 +7184,9 @@ private static CosmosEndToEndOperationLatencyPolicyConfig evaluatePpafEnforcedE2
71917184

71927185
if (Configs.isReadAvailabilityStrategyEnabledWithPpaf()) {
71937186

7187+
logger.warn("Availability strategy for reads, queries, read all and read many" +
7188+
" is enabled when PerPartitionAutomaticFailover is enabled.");
7189+
71947190
if (connectionPolicy.getConnectionMode() == ConnectionMode.DIRECT) {
71957191
Duration networkRequestTimeout = connectionPolicy.getTcpNetworkRequestTimeout();
71967192

@@ -7618,6 +7614,57 @@ private void addCancelledGatewayModeDiagnosticsIntoCosmosException(CosmosExcepti
76187614
}
76197615
}
76207616

7617+
// this is a one time call, so we can afford to synchronize as the benefit is now all PPAF and PPCB related dependencies are visible
7618+
// if initializePerPartitionFailover has been invoked prior
7619+
private synchronized void initializePerPartitionFailover(DatabaseAccount databaseAccountSnapshot) {
7620+
initializePerPartitionAutomaticFailover(databaseAccountSnapshot);
7621+
initializePerPartitionCircuitBreaker();
7622+
enableAvailabilityStrategyForReads();
7623+
7624+
checkNotNull(this.globalPartitionEndpointManagerForPerPartitionAutomaticFailover, "Argument 'globalPartitionEndpointManagerForPerPartitionAutomaticFailover' cannot be null.");
7625+
checkNotNull(this.globalPartitionEndpointManagerForPerPartitionCircuitBreaker, "Argument 'globalPartitionEndpointManagerForPerPartitionCircuitBreaker' cannot be null.");
7626+
7627+
this.diagnosticsClientConfig.withPartitionLevelCircuitBreakerConfig(this.globalPartitionEndpointManagerForPerPartitionCircuitBreaker.getCircuitBreakerConfig());
7628+
}
7629+
7630+
private void initializePerPartitionAutomaticFailover(DatabaseAccount databaseAccountSnapshot) {
7631+
7632+
Boolean isPerPartitionAutomaticFailoverEnabledAsMandatedByService
7633+
= databaseAccountSnapshot.isPerPartitionFailoverBehaviorEnabled();
7634+
7635+
if (isPerPartitionAutomaticFailoverEnabledAsMandatedByService != null) {
7636+
this.globalPartitionEndpointManagerForPerPartitionAutomaticFailover.resetPerPartitionAutomaticFailoverEnabled(isPerPartitionAutomaticFailoverEnabledAsMandatedByService);
7637+
} else {
7638+
boolean isPerPartitionAutomaticFailoverOptedIntoByClient
7639+
= Configs.isPerPartitionAutomaticFailoverEnabled().equalsIgnoreCase("true");
7640+
this.globalPartitionEndpointManagerForPerPartitionAutomaticFailover.resetPerPartitionAutomaticFailoverEnabled(isPerPartitionAutomaticFailoverOptedIntoByClient);
7641+
}
7642+
}
7643+
7644+
private void initializePerPartitionCircuitBreaker() {
7645+
if (this.globalPartitionEndpointManagerForPerPartitionAutomaticFailover.isPerPartitionAutomaticFailoverEnabled()) {
7646+
7647+
PartitionLevelCircuitBreakerConfig partitionLevelCircuitBreakerConfig = Configs.getPartitionLevelCircuitBreakerConfig();
7648+
7649+
if (partitionLevelCircuitBreakerConfig != null && !partitionLevelCircuitBreakerConfig.isPartitionLevelCircuitBreakerEnabled()) {
7650+
logger.warn("Per-Partition Circuit Breaker is enabled by default when Per-Partition Automatic Failover is enabled.");
7651+
System.setProperty("COSMOS.PARTITION_LEVEL_CIRCUIT_BREAKER_CONFIG", "{\"isPartitionLevelCircuitBreakerEnabled\": true}");
7652+
}
7653+
}
7654+
7655+
this.globalPartitionEndpointManagerForPerPartitionCircuitBreaker.resetCircuitBreakerConfig();
7656+
this.globalPartitionEndpointManagerForPerPartitionCircuitBreaker.init();
7657+
}
7658+
7659+
private void enableAvailabilityStrategyForReads() {
7660+
if (this.globalPartitionEndpointManagerForPerPartitionAutomaticFailover.isPerPartitionAutomaticFailoverEnabled()) {
7661+
this.ppafEnforcedE2ELatencyPolicyConfigForReads = this.evaluatePpafEnforcedE2eLatencyPolicyCfgForReads(
7662+
this.globalPartitionEndpointManagerForPerPartitionAutomaticFailover,
7663+
this.connectionPolicy
7664+
);
7665+
}
7666+
}
7667+
76217668
private boolean useThinClient() {
76227669
return Configs.isThinClientEnabled() && this.connectionPolicy.getConnectionMode() == ConnectionMode.GATEWAY;
76237670
}

0 commit comments

Comments
 (0)