001package org.jgroups.protocols; 002 003 004import java.text.SimpleDateFormat; 005import java.util.Collection; 006import java.util.Date; 007import java.util.LinkedList; 008import java.util.List; 009 010import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; 011import com.amazonaws.client.builder.AwsClientBuilder; 012import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; 013import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; 014import com.amazonaws.services.dynamodbv2.document.*; 015import com.amazonaws.services.dynamodbv2.document.spec.ScanSpec; 016import com.amazonaws.services.dynamodbv2.document.utils.NameMap; 017import com.amazonaws.services.dynamodbv2.document.utils.ValueMap; 018import com.amazonaws.services.dynamodbv2.model.*; 019import org.jgroups.Address; 020import org.jgroups.annotations.Property; 021import org.jgroups.conf.ClassConfigurator; 022import org.jgroups.util.Responses; 023 024 025/** 026 * <p>JGroups discovery protocol using a shared Amazon Web Services (JWS) 027 * DynamoDB table.</p> 028 * 029 * @author Vladimir Dzhuvinov 030 */ 031public class DYNAMODB_PING extends FILE_PING { 032 033 034 /** 035 * The DYNAMODB_PING magic number. 036 */ 037 protected static final short JGROUPS_PROTOCOL_MAGIC_NUMBER = 1050; 038 039 040 @Property(description="The DynamoDB region to use.", exposeAsManagedAttribute=false) 041 protected String region_name; 042 043 044 @Property(description="The DynamoDB endpoint to use (optional, overrides region).", exposeAsManagedAttribute=false) 045 protected String endpoint; 046 047 048 @Property(description="The DynamoDB table to use (defaults to 'jgroups_ping').", exposeAsManagedAttribute=false) 049 protected String table_name = "jgroups_ping"; 050 051 052 @Property(description="Checks if the DynamoDB table exists and creates a new one if missing.", exposeAsManagedAttribute=false) 053 protected boolean check_if_table_exists = true; 054 055 056 @Property(description="Optional prefix to be applied to the cluster name when stored in the DynamoDB ping table.", exposeAsManagedAttribute=false) 057 protected String cluster_name_prefix = ""; 058 059 060 @Property(description="Causes additional debugging information to be stored for each DynamoDB ping item.", exposeAsManagedAttribute=false) 061 protected boolean store_debug_info = false; 062 063 064 /** 065 * The DynamoDB ping table. 066 */ 067 protected Table table; 068 069 070 static { 071 registerProtocolWithJGroups(JGROUPS_PROTOCOL_MAGIC_NUMBER); 072 } 073 074 075 /** 076 * Registers the DYNAMODB_PING protocol with JGroups. 077 * 078 * @param magicNumber The magic number to use. Should be greater than 079 * 1024. 080 */ 081 private static void registerProtocolWithJGroups(final short magicNumber) { 082 083 if (ClassConfigurator.getProtocolId(DYNAMODB_PING.class) == 0) { 084 ClassConfigurator.addProtocol(magicNumber, DYNAMODB_PING.class); 085 } 086 } 087 088 089 @Override 090 public void init() throws Exception { 091 092 super.init(); 093 094 AmazonDynamoDBClientBuilder builder = AmazonDynamoDBClientBuilder 095 .standard() 096 .withCredentials(DefaultAWSCredentialsProviderChain.getInstance()); 097 098 if (endpoint != null && ! endpoint.trim().isEmpty()) { 099 // Override region if the endpoint is set 100 builder = builder.withEndpointConfiguration( 101 new AwsClientBuilder.EndpointConfiguration( 102 endpoint, 103 null // inferred from endpoint 104 )); 105 log.info("set DynamoDB endpoint to %s", endpoint); 106 } else { 107 builder.withRegion(region_name); 108 } 109 110 AmazonDynamoDB client = builder.build(); 111 112 log.info("using DynamoDB in region %s with table %s", region_name, table_name); 113 114 if (check_if_table_exists) { 115 createTableIfMissing(client); 116 } 117 118 table = new DynamoDB(client).getTable(table_name); 119 120 try { 121 table.waitForActive(); 122 } catch (InterruptedException e) { 123 throw new RuntimeException(e); 124 } 125 } 126 127 128 /** 129 * Creates a new DynamoDB ping table if missing. 130 * 131 * @param client The DynamoDB client. 132 */ 133 protected void createTableIfMissing(final AmazonDynamoDB client) { 134 135 try { 136 new DynamoDB(client).createTable(composeCreateTableRequest()); 137 log.info("created DynamoDB table %s", table_name); 138 } catch (ResourceInUseException e) { 139 log.info("found DynamoDB table %s", table_name); 140 } 141 } 142 143 144 /** 145 * Composes a create table request for the DynamoDB ping table. 146 * 147 * @return The create table request. 148 */ 149 protected CreateTableRequest composeCreateTableRequest() { 150 151 Collection<KeySchemaElement> keyAttributes = new LinkedList<>(); 152 keyAttributes.add(new KeySchemaElement("own_address", KeyType.HASH)); 153 keyAttributes.add(new KeySchemaElement("cluster", KeyType.RANGE)); 154 155 Collection<AttributeDefinition> attributeDefinitions = new LinkedList<>(); 156 attributeDefinitions.add(new AttributeDefinition("own_address", ScalarAttributeType.S)); 157 attributeDefinitions.add(new AttributeDefinition("cluster", ScalarAttributeType.S)); 158 159 return new CreateTableRequest() 160 .withTableName(table_name) 161 .withKeySchema(keyAttributes) 162 .withAttributeDefinitions(attributeDefinitions) 163 .withProvisionedThroughput(new ProvisionedThroughput(1L, 1L)); 164 } 165 166 167 @Override 168 public void stop() { 169 170 super.stop(); 171 172 if (is_coord) 173 removeAll(cluster_name); 174 } 175 176 177 @Override 178 protected void createRootDir() { 179 // ignore, table has to exist 180 } 181 182 183 @Override 184 protected void readAll(final List<Address> members, final String clusterName, final Responses responses) { 185 186 if (clusterName == null) 187 return; 188 189 if (log.isTraceEnabled()) 190 log.trace("getting entries for cluster %s ...", clusterName); 191 192 ItemCollection<ScanOutcome> outcome; 193 194 try { 195 outcome = table.scan(composeScanSpec(clusterName)); 196 } catch (Exception e) { 197 log.error(String.format("failed to get member list for cluster %s", clusterName), e); 198 return; 199 } 200 201 if (log.isTraceEnabled()) 202 log.trace("retrieved %d items for cluster %s", outcome.getAccumulatedItemCount(), clusterName); 203 204 for (Item item : outcome) { 205 try { 206 final PingData pingData = toPingData(item); 207 208 if (log.isTraceEnabled()) 209 log.trace("processing DynamoDB item [%s -> %s]", item.getString("own_address"), pingData); 210 211 if (pingData == null || (members != null && ! members.contains(pingData.getAddress()))) 212 continue; 213 214 responses.addResponse(pingData, pingData.isCoord()); 215 216 if (log.isTraceEnabled()) 217 log.trace("added member %s [members: %s]", pingData, members); 218 219 if (local_addr != null && ! local_addr.equals(pingData.getAddress())) { 220 221 addDiscoveryResponseToCaches( 222 pingData.getAddress(), 223 pingData.getLogicalName(), 224 pingData.getPhysicalAddr()); 225 226 if (log.isTraceEnabled()) 227 log.trace("added possible member %s [local address: %s]", pingData, local_addr); 228 } 229 230 } catch (Exception e) { 231 log.error("error processing ping data for cluster %s [item: %s]", clusterName, item); 232 } 233 } 234 } 235 236 237 /** 238 * Composes the final stored cluster name. 239 * 240 * @return The cluster name. 241 */ 242 protected String composeStoredClusterName(final String clusterName) { 243 244 return cluster_name_prefix != null ? cluster_name_prefix + clusterName : clusterName; 245 } 246 247 248 /** 249 * Composes the DynamoDB primary key for the specified JGroups address 250 * an cluster name. 251 * 252 * @param ownAddress The own address. 253 * @param clusterName The cluster name. 254 * 255 * @return The primary key. 256 */ 257 protected PrimaryKey composePrimaryKey(final String ownAddress, final String clusterName) { 258 259 return new PrimaryKey( 260 "own_address", ownAddress, // hash key 261 "cluster", composeStoredClusterName(clusterName) // range key 262 ); 263 } 264 265 266 /** 267 * Composes the DynamoDB scan spec for the specified cluster name. 268 * 269 * @param clusterName The cluster name. 270 * 271 * @return The scan spec. 272 */ 273 protected ScanSpec composeScanSpec(final String clusterName) { 274 275 return new ScanSpec() 276 .withFilterExpression("#k_cluster = :v_cluster") 277 .withNameMap(new NameMap().with("#k_cluster", "cluster")) 278 .withValueMap(new ValueMap().withString(":v_cluster", composeStoredClusterName(clusterName))); 279 } 280 281 282 /** 283 * Formats the specified date in ISO format. 284 * 285 * @param date The date. 286 * 287 * @return The formatted date. 288 */ 289 static String formatISODate(final Date date) { 290 291 return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); 292 } 293 294 295 /** 296 * Converts the specified ping data to a DynamoDB item. 297 * 298 * @param data The ping data. 299 * @param clusterName The cluster mame. 300 * 301 * @return The DynamoDB item. 302 */ 303 protected Item toItem(final PingData data, final String clusterName) { 304 305 Item item = new Item() 306 .withPrimaryKey(composePrimaryKey(addressAsString(data.getAddress()), clusterName)) 307 .withBinary("ping_data", serializeWithoutView(data)); 308 309 if (store_debug_info) { 310 item 311 .withString("logical_name", data.getLogicalName()) 312 .withString("physical_address", addressAsString(data.getPhysicalAddr())) 313 .withBoolean("server", data.isServer()) 314 .withBoolean("coordinator", data.isCoord()) 315 .withString("timestamp", formatISODate(new Date())); 316 } 317 318 return item; 319 } 320 321 322 /** 323 * Converts the specified DynamoDB item to ping data. 324 * 325 * @param item The DynamoDB item. 326 * 327 * @return The ping data. 328 * 329 * @throws Exception If conversion failed. 330 */ 331 protected PingData toPingData(final Item item) 332 throws Exception { 333 334 return deserialize(item.getBinary("ping_data")); 335 } 336 337 338 @Override 339 protected void write(final List<PingData> list, final String clusterName) { 340 341 for (PingData data: list) { 342 putIntoTable(data, clusterName); 343 } 344 } 345 346 347 /** 348 * Puts the specified ping data into the DynamoDB table. 349 * 350 * @param data The ping data. 351 * @param clusterName The cluster name. 352 */ 353 protected synchronized void putIntoTable(final PingData data, final String clusterName) { 354 355 try { 356 table.putItem(toItem(data, clusterName)); 357 } catch (Exception e) { 358 log.error("put error: " + e.getMessage(), e); 359 } 360 } 361 362 363 @Override 364 protected void remove(final String clusterName, final Address address) { 365 try { 366 table.deleteItem(composePrimaryKey(addressAsString(address), clusterName)); 367 } catch (Exception e) { 368 log.error("delete error: " + e.getMessage(), e); 369 } 370 } 371 372 373 @Override 374 protected void removeAll(final String clusterName) { 375 376 if (clusterName == null) 377 return; 378 379 ItemCollection<ScanOutcome> outcome = table.scan(composeScanSpec(clusterName)); 380 381 for (Item item: outcome) { 382 try { 383 remove(clusterName, toPingData(item).getAddress()); 384 } catch (Exception e) { 385 log.error("delete all error: " + e.getMessage(), e); 386 } 387 } 388 } 389}