1 /* 2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"). 5 * You may not use this file except in compliance with the License. 6 * A copy of the License is located at 7 * 8 * http://aws.amazon.com/apache2.0 9 * 10 * or in the "license" file accompanying this file. This file is distributed 11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 * express or implied. See the License for the specific language governing 13 * permissions and limitations under the License. 14 */ 15 16 package utils.test.util; 17 18 import org.slf4j.Logger; 19 import org.slf4j.LoggerFactory; 20 import software.amazon.awssdk.core.exception.SdkClientException; 21 import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 22 import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; 23 import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; 24 import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest; 25 import software.amazon.awssdk.services.dynamodb.model.ResourceInUseException; 26 import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; 27 import software.amazon.awssdk.services.dynamodb.model.TableDescription; 28 import software.amazon.awssdk.services.dynamodb.model.TableStatus; 29 30 /** 31 * Utility methods for working with DynamoDB tables. 32 * 33 * <pre class="brush: java"> 34 * // ... create DynamoDB table ... 35 * try { 36 * waitUntilActive(dynamoDB, myTableName()); 37 * } catch (SdkClientException e) { 38 * // table didn't become active 39 * } 40 * // ... start making calls to table ... 41 * </pre> 42 */ 43 public class TableUtils { 44 45 private static final int DEFAULT_WAIT_TIMEOUT = 20 * 60 * 1000; 46 private static final int DEFAULT_WAIT_INTERVAL = 10 * 1000; 47 /** 48 * The logging utility. 49 */ 50 private static final Logger log = LoggerFactory.getLogger(TableUtils.class); 51 52 /** 53 * Waits up to 10 minutes for a specified DynamoDB table to resolve, 54 * indicating that it exists. If the table doesn't return a result after 55 * this time, a SdkClientException is thrown. 56 * 57 * @param dynamo 58 * The DynamoDB client to use to make requests. 59 * @param tableName 60 * The name of the table being resolved. 61 * 62 * @throws SdkClientException 63 * If the specified table does not resolve before this method 64 * times out and stops polling. 65 * @throws InterruptedException 66 * If the thread is interrupted while waiting for the table to 67 * resolve. 68 */ waitUntilExists(final DynamoDbClient dynamo, final String tableName)69 public static void waitUntilExists(final DynamoDbClient dynamo, final String tableName) 70 throws InterruptedException { 71 waitUntilExists(dynamo, tableName, DEFAULT_WAIT_TIMEOUT, DEFAULT_WAIT_INTERVAL); 72 } 73 74 /** 75 * Waits up to a specified amount of time for a specified DynamoDB table to 76 * resolve, indicating that it exists. If the table doesn't return a result 77 * after this time, a SdkClientException is thrown. 78 * 79 * @param dynamo 80 * The DynamoDB client to use to make requests. 81 * @param tableName 82 * The name of the table being resolved. 83 * @param timeout 84 * The maximum number of milliseconds to wait. 85 * @param interval 86 * The poll interval in milliseconds. 87 * 88 * @throws SdkClientException 89 * If the specified table does not resolve before this method 90 * times out and stops polling. 91 * @throws InterruptedException 92 * If the thread is interrupted while waiting for the table to 93 * resolve. 94 */ waitUntilExists(final DynamoDbClient dynamo, final String tableName, final int timeout, final int interval)95 public static void waitUntilExists(final DynamoDbClient dynamo, final String tableName, final int timeout, 96 final int interval) throws InterruptedException { 97 TableDescription table = waitForTableDescription(dynamo, tableName, null, timeout, interval); 98 99 if (table == null) { 100 throw SdkClientException.builder().message("Table " + tableName + " never returned a result").build(); 101 } 102 } 103 104 /** 105 * Waits up to 10 minutes for a specified DynamoDB table to move into the 106 * <code>ACTIVE</code> state. If the table does not exist or does not 107 * transition to the <code>ACTIVE</code> state after this time, then 108 * SdkClientException is thrown. 109 * 110 * @param dynamo 111 * The DynamoDB client to use to make requests. 112 * @param tableName 113 * The name of the table whose status is being checked. 114 * 115 * @throws TableNeverTransitionedToStateException 116 * If the specified table does not exist or does not transition 117 * into the <code>ACTIVE</code> state before this method times 118 * out and stops polling. 119 * @throws InterruptedException 120 * If the thread is interrupted while waiting for the table to 121 * transition into the <code>ACTIVE</code> state. 122 */ waitUntilActive(final DynamoDbClient dynamo, final String tableName)123 public static void waitUntilActive(final DynamoDbClient dynamo, final String tableName) 124 throws InterruptedException, TableNeverTransitionedToStateException { 125 waitUntilActive(dynamo, tableName, DEFAULT_WAIT_TIMEOUT, DEFAULT_WAIT_INTERVAL); 126 } 127 128 /** 129 * Waits up to a specified amount of time for a specified DynamoDB table to 130 * move into the <code>ACTIVE</code> state. If the table does not exist or 131 * does not transition to the <code>ACTIVE</code> state after this time, 132 * then a SdkClientException is thrown. 133 * 134 * @param dynamo 135 * The DynamoDB client to use to make requests. 136 * @param tableName 137 * The name of the table whose status is being checked. 138 * @param timeout 139 * The maximum number of milliseconds to wait. 140 * @param interval 141 * The poll interval in milliseconds. 142 * 143 * @throws TableNeverTransitionedToStateException 144 * If the specified table does not exist or does not transition 145 * into the <code>ACTIVE</code> state before this method times 146 * out and stops polling. 147 * @throws InterruptedException 148 * If the thread is interrupted while waiting for the table to 149 * transition into the <code>ACTIVE</code> state. 150 */ waitUntilActive(final DynamoDbClient dynamo, final String tableName, final int timeout, final int interval)151 public static void waitUntilActive(final DynamoDbClient dynamo, final String tableName, final int timeout, 152 final int interval) throws InterruptedException, TableNeverTransitionedToStateException { 153 TableDescription table = waitForTableDescription(dynamo, tableName, TableStatus.ACTIVE, timeout, interval); 154 155 if (table == null || !table.tableStatus().equals(TableStatus.ACTIVE)) { 156 throw new TableNeverTransitionedToStateException(tableName, TableStatus.ACTIVE); 157 } 158 } 159 160 /** 161 * Wait for the table to reach the desired status and returns the table 162 * description 163 * 164 * @param dynamo 165 * Dynamo client to use 166 * @param tableName 167 * Table name to poll status of 168 * @param desiredStatus 169 * Desired {@link TableStatus} to wait for. If null this method 170 * simply waits until DescribeTable returns something non-null 171 * (i.e. any status) 172 * @param timeout 173 * Timeout in milliseconds to continue to poll for desired status 174 * @param interval 175 * Time to wait in milliseconds between poll attempts 176 * @return Null if DescribeTables never returns a result, otherwise the 177 * result of the last poll attempt (which may or may not have the 178 * desired state) 179 * @throws {@link 180 * IllegalArgumentException} If timeout or interval is invalid 181 */ waitForTableDescription(final DynamoDbClient dynamo, final String tableName, TableStatus desiredStatus, final int timeout, final int interval)182 private static TableDescription waitForTableDescription(final DynamoDbClient dynamo, final String tableName, 183 TableStatus desiredStatus, final int timeout, final int interval) 184 throws InterruptedException, IllegalArgumentException { 185 if (timeout < 0) { 186 throw new IllegalArgumentException("Timeout must be >= 0"); 187 } 188 if (interval <= 0 || interval >= timeout) { 189 throw new IllegalArgumentException("Interval must be > 0 and < timeout"); 190 } 191 long startTime = System.currentTimeMillis(); 192 long endTime = startTime + timeout; 193 194 TableDescription table = null; 195 while (System.currentTimeMillis() < endTime) { 196 try { 197 table = dynamo.describeTable(DescribeTableRequest.builder().tableName(tableName).build()).table(); 198 if (desiredStatus == null || table.tableStatus().equals(desiredStatus)) { 199 return table; 200 201 } 202 } catch (ResourceNotFoundException rnfe) { 203 // ResourceNotFound means the table doesn't exist yet, 204 // so ignore this error and just keep polling. 205 } 206 207 Thread.sleep(interval); 208 } 209 return table; 210 } 211 212 /** 213 * Creates the table and ignores any errors if it already exists. 214 * @param dynamo The Dynamo client to use. 215 * @param createTableRequest The create table request. 216 * @return True if created, false otherwise. 217 */ createTableIfNotExists(final DynamoDbClient dynamo, final CreateTableRequest createTableRequest)218 public static boolean createTableIfNotExists(final DynamoDbClient dynamo, final CreateTableRequest createTableRequest) { 219 try { 220 dynamo.createTable(createTableRequest); 221 return true; 222 } catch (final ResourceInUseException e) { 223 if (log.isTraceEnabled()) { 224 log.trace("Table " + createTableRequest.tableName() + " already exists", e); 225 } 226 } 227 return false; 228 } 229 230 /** 231 * Deletes the table and ignores any errors if it doesn't exist. 232 * @param dynamo The Dynamo client to use. 233 * @param deleteTableRequest The delete table request. 234 * @return True if deleted, false otherwise. 235 */ deleteTableIfExists(final DynamoDbClient dynamo, final DeleteTableRequest deleteTableRequest)236 public static boolean deleteTableIfExists(final DynamoDbClient dynamo, final DeleteTableRequest deleteTableRequest) { 237 try { 238 dynamo.deleteTable(deleteTableRequest); 239 return true; 240 } catch (final ResourceNotFoundException e) { 241 if (log.isTraceEnabled()) { 242 log.trace("Table " + deleteTableRequest.tableName() + " does not exist", e); 243 } 244 } 245 return false; 246 } 247 248 /** 249 * Thrown by {@link TableUtils} when a table never reaches a desired state 250 */ 251 public static class TableNeverTransitionedToStateException extends SdkClientException { 252 253 private static final long serialVersionUID = 8920567021104846647L; 254 TableNeverTransitionedToStateException(String tableName, TableStatus desiredStatus)255 public TableNeverTransitionedToStateException(String tableName, TableStatus desiredStatus) { 256 super(SdkClientException.builder() 257 .message("Table " + tableName + " never transitioned to desired state of " + 258 desiredStatus.toString())); 259 } 260 261 } 262 263 } 264