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}