/* * Copyright 2013-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.cloud.aws.jdbc.retry; import com.amazonaws.services.rds.AmazonRDS; import com.amazonaws.services.rds.model.DBInstance; import com.amazonaws.services.rds.model.DBInstanceNotFoundException; import com.amazonaws.services.rds.model.DescribeDBInstancesRequest; import com.amazonaws.services.rds.model.DescribeDBInstancesResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.aws.core.env.ResourceIdResolver; import org.springframework.retry.RetryContext; import org.springframework.retry.RetryPolicy; import org.springframework.retry.context.RetryContextSupport; import org.springframework.util.Assert; /** * {@link RetryPolicy} implementation that checks if it is useful to retry an operation based on the database instance * status. This class retrieves that database state and verifies through the {@link InstanceStatus} enum operation if * it is useful to retry the operation. This class does not retrieve the status if there is no Throwable registered to * avoid any performance implication during normal operations. * <p>This class should not be used alone because this would lead into a infinite retry, because this class does not * limit the amount of retries. Consider using this class together with the {@link SqlRetryPolicy} which limits the * maximum number of retries.</p> * * @author Agim Emruli * @since 1.0 */ public class DatabaseInstanceStatusRetryPolicy implements RetryPolicy { /** * DB-Instance attribute name used inside the {@link RetryContext} to store the database instance name during the * retries. */ private static final String DB_INSTANCE_ATTRIBUTE_NAME = "DbInstanceIdentifier"; private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseInstanceStatusRetryPolicy.class); /** * Database instance identifier which should be checked. */ private final String dbInstanceIdentifier; /** * {@link AmazonRDS} client used to query the Amazon RDS service */ private final AmazonRDS amazonRDS; private ResourceIdResolver resourceIdResolver; /** * Constructs this strategy implementation with it default and mandatory collaborators. * * @param amazonRDS * - used to query the Amazon RDS service, must not be null * @param dbInstanceIdentifier * - database instance for which this class should check the state. */ public DatabaseInstanceStatusRetryPolicy(AmazonRDS amazonRDS, String dbInstanceIdentifier) { Assert.notNull(amazonRDS, "amazonRDS must not be null."); this.amazonRDS = amazonRDS; this.dbInstanceIdentifier = dbInstanceIdentifier; } /** * Configures an option {@link org.springframework.cloud.aws.core.env.ResourceIdResolver} to resolve logical name to physical name * * @param resourceIdResolver * - the resourceIdResolver to be used, may be null */ public void setResourceIdResolver(ResourceIdResolver resourceIdResolver) { this.resourceIdResolver = resourceIdResolver; } /** * Implementation that checks if there is an exception registered through {@link #registerThrowable(org.springframework.retry.RetryContext, * Throwable)}. Returns <code>true</code> if there is no exception registered at all and verifies the database instance status if * there is one registered. * * @param context * - the retry context which may contain a registered exception * @return <code>true</code> if there is no exception registered or if there is a retry useful which is verified by the {@link * InstanceStatus} enum. */ @Override public boolean canRetry(RetryContext context) { //noinspection ThrowableResultOfMethodCallIgnored return (context.getLastThrowable() == null || isDatabaseAvailable(context)); } @Override public RetryContext open(RetryContext parent) { RetryContextSupport context = new RetryContextSupport(parent); context.setAttribute(DB_INSTANCE_ATTRIBUTE_NAME, getDbInstanceIdentifier()); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Starting RetryContext for database instance with identifier {}", getDbInstanceIdentifier()); } return context; } @Override public void close(RetryContext context) { context.removeAttribute(DB_INSTANCE_ATTRIBUTE_NAME); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Closing RetryContext for database instance with identifier {}", getDbInstanceIdentifier()); } } @Override public void registerThrowable(RetryContext context, Throwable throwable) { ((RetryContextSupport) context).registerThrowable(throwable); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Registered Throwable of Type {} for RetryContext", throwable.getClass().getName()); } } private boolean isDatabaseAvailable(RetryContext context) { DescribeDBInstancesResult describeDBInstancesResult; try { describeDBInstancesResult = this.amazonRDS.describeDBInstances(new DescribeDBInstancesRequest().withDBInstanceIdentifier((String) context.getAttribute(DB_INSTANCE_ATTRIBUTE_NAME))); } catch (DBInstanceNotFoundException e) { LOGGER.warn("Database Instance with name {} has been removed or is not configured correctly, no retry possible", getDbInstanceIdentifier()); //Database has been deleted while operating, hence we can not retry return false; } if (describeDBInstancesResult.getDBInstances().size() == 1) { DBInstance dbInstance = describeDBInstancesResult.getDBInstances().get(0); InstanceStatus instanceStatus = InstanceStatus.fromDatabaseStatus(dbInstance.getDBInstanceStatus()); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Status of database to be retried is {}", instanceStatus); } return instanceStatus.isRetryable(); } else { throw new IllegalStateException("Multiple databases found for same identifier, this is likely an incompatibility with the Amazon SDK"); } } private String getDbInstanceIdentifier() { return this.resourceIdResolver != null ? this.resourceIdResolver.resolveToPhysicalResourceId(this.dbInstanceIdentifier) : this.dbInstanceIdentifier; } }