001package com.nimbusds.infinispan.persistence.dynamodb;
002
003
004import java.security.InvalidKeyException;
005import java.security.NoSuchAlgorithmException;
006import java.time.Instant;
007import java.util.concurrent.Executor;
008import java.util.function.Predicate;
009
010import com.amazonaws.ClientConfiguration;
011import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
012import com.amazonaws.client.builder.AwsClientBuilder;
013import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
014import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
015import com.amazonaws.services.dynamodbv2.document.DynamoDB;
016import com.amazonaws.services.dynamodbv2.document.Index;
017import com.amazonaws.services.dynamodbv2.document.Item;
018import com.amazonaws.services.dynamodbv2.document.Table;
019import com.amazonaws.services.dynamodbv2.document.spec.GetItemSpec;
020import com.amazonaws.services.dynamodbv2.model.*;
021import com.codahale.metrics.Timer;
022import io.reactivex.Flowable;
023import net.jcip.annotations.ThreadSafe;
024import org.infinispan.commons.configuration.ConfiguredBy;
025import org.infinispan.commons.persistence.Store;
026import org.infinispan.marshall.core.MarshalledEntry;
027import org.infinispan.marshall.core.MarshalledEntryFactory;
028import org.infinispan.persistence.spi.InitializationContext;
029import org.infinispan.persistence.spi.PersistenceException;
030import org.kohsuke.MetaInfServices;
031import org.reactivestreams.Publisher;
032
033import com.nimbusds.infinispan.persistence.common.InfinispanEntry;
034import com.nimbusds.infinispan.persistence.common.InfinispanStore;
035import com.nimbusds.infinispan.persistence.common.query.QueryExecutor;
036import com.nimbusds.infinispan.persistence.dynamodb.config.DynamoDBStoreConfiguration;
037import com.nimbusds.infinispan.persistence.dynamodb.logging.Loggers;
038import com.nimbusds.infinispan.persistence.dynamodb.query.DynamoDBQueryExecutor;
039import com.nimbusds.infinispan.persistence.dynamodb.query.DynamoDBQueryExecutorInitContext;
040
041
042/**
043 * AWS DynamoDB store for Infinispan 9.3+ caches and maps.
044 */
045@ThreadSafe
046@MetaInfServices
047@ConfiguredBy(DynamoDBStoreConfiguration.class)
048@Store(shared = true)
049public class DynamoDBStore<K,V> extends InfinispanStore<K,V> {
050
051
052        /**
053         * The DynamoDB configuration.
054         */
055        private DynamoDBStoreConfiguration config;
056        
057        
058        /**
059         * The DynamoDB HTTP client.
060         */
061        private AmazonDynamoDB client;
062        
063        
064        /**
065         * The DynamoDB table API client.
066         */
067        private Table table;
068
069
070        /**
071         * The DynamoDB item transformer (to / from Infinispan entries).
072         */
073        private DynamoDBItemTransformer<K,V> itemTransformer;
074        
075        
076        /**
077         * The optional DynamoDB query executor.
078         */
079        private DynamoDBQueryExecutor<K,V> queryExecutor;
080        
081        
082        /**
083         * The DynamoDB request factory.
084         */
085        private RequestFactory<K,V> requestFactory;
086        
087        
088        /**
089         * Optional item HMAC SHA-256 security.
090         */
091        private ItemHMAC itemHMAC;
092
093
094        /**
095         * The marshalled Infinispan entry factory.
096         */
097        private MarshalledEntryFactory<K,V> marshalledEntryFactory;
098
099
100        /**
101         * Purges expired entries found in the DynamoDB store, as indicated by
102         * their persisted metadata (optional, may be ignored / not stored).
103         */
104        private ExpiredEntryReaper<K,V> reaper;
105        
106        
107        /**
108         * DynamoDB meters.
109         */
110        private DynamoDBMeters meters;
111
112
113        /**
114         * Loads a DynamoDB item transformer with the specified class name.
115         *
116         * @param clazz The class. Must not be {@code null}.
117         *
118         * @return The DynamoDB entry transformer.
119         */
120        private DynamoDBItemTransformer<K,V> loadItemTransformerClass(final Class<?> clazz) {
121
122                try {
123                        @SuppressWarnings( "unchecked" )
124                        Class<DynamoDBItemTransformer<K,V>> genClazz = (Class<DynamoDBItemTransformer<K,V>>)clazz;
125                        return genClazz.newInstance();
126                } catch (Exception e) {
127                        throw new PersistenceException("Couldn't load DynamoDB item transformer class: " + e.getMessage(), e);
128                }
129        }
130        
131        
132        /**
133         * Loads a DynamoDB query executor with the specified class name.
134         *
135         * @param clazz The class. Must not be {@code null}.
136         *
137         * @return The DynamoDB query executor.
138         */
139        private DynamoDBQueryExecutor<K,V> loadQueryExecutorClass(final Class<?> clazz) {
140                
141                try {
142                        @SuppressWarnings( "unchecked" )
143                        Class<DynamoDBQueryExecutor<K,V>> genClazz = (Class<DynamoDBQueryExecutor<K,V>>)clazz;
144                        return genClazz.newInstance();
145                } catch (Exception e) {
146                        throw new PersistenceException("Couldn't load DynamoDB query executor class: " + e.getMessage(), e);
147                }
148        }
149        
150        
151        /**
152         * Returns the DynamoDB store configuration.
153         *
154         * @return The DynamoDB store configuration, {@code null} if not
155         *         initialised.
156         */
157        public DynamoDBStoreConfiguration getConfiguration() {
158                return config;
159        }
160        
161        
162        /**
163         * Returns the underlying DynamoDB table.
164         *
165         * @return The DynamoDB table, {@code null} if not initialised.
166         */
167        public Table getTable() {
168                return table;
169        }
170        
171        
172        /**
173         * Returns the configured DynamoDB item transformer.
174         *
175         * @return The DynamoDB item transformer, {@code null} if not
176         *         initialised.
177         */
178        public DynamoDBItemTransformer<K, V> getItemTransformer() {
179                return itemTransformer;
180        }
181        
182        
183        @Override
184        public QueryExecutor<K, V> getQueryExecutor() {
185                return queryExecutor;
186        }
187        
188        
189        /**
190         * Returns the DynamoDB timers.
191         *
192         * @return The timers, {@code null} if not initialised.
193         */
194        public DynamoDBMeters getMeters() {
195                return meters;
196        }
197        
198        
199        @Override
200        @SuppressWarnings("unchecked")
201        public void init(final InitializationContext ctx) {
202
203                // This method will be invoked by the PersistenceManager during initialization. The InitializationContext
204                // contains:
205                // - this CacheLoader's configuration
206                // - the cache to which this loader is applied. Your loader might want to use the cache's name to construct
207                //   cache-specific identifiers
208                // - the StreamingMarshaller that needs to be used to marshall/unmarshall the entries
209                // - a TimeService which the loader can use to determine expired entries
210                // - a ByteBufferFactory which needs to be used to construct ByteBuffers
211                // - a MarshalledEntryFactory which needs to be used to construct entries from the data retrieved by the loader
212
213                super.init(ctx);
214                
215                this.config = ctx.getConfiguration();
216                
217                // Log the configuration
218                Loggers.MAIN_LOG.info("[DS0100] DynamoDB store: Infinispan cache store configuration for {}:", getCacheName());
219                config.log();
220                
221                // Create a DynamoDB client
222                AmazonDynamoDBClientBuilder builder = AmazonDynamoDBClientBuilder
223                        .standard()
224                        .withCredentials(DefaultAWSCredentialsProviderChain.getInstance());
225                
226                if (config.getEndpoint() != null && ! config.getEndpoint().trim().isEmpty()) {
227                        // If endpoint is set, override region
228                        builder = builder.withEndpointConfiguration(
229                                new AwsClientBuilder.EndpointConfiguration(
230                                        config.getEndpoint(),
231                                        null // inferred from endpoint
232                                )
233                        );
234                } else {
235                        builder.withRegion(config.getRegion());
236                }
237                
238                if (config.getHTTPProxyHost() != null) {
239                        ClientConfiguration clientConfig = new ClientConfiguration().withProxyHost(config.getHTTPProxyHost());
240                        if (config.getHTTPProxyPort() > -1) {
241                                clientConfig.withProxyPort(config.getHTTPProxyPort());
242                        }
243                        builder = builder.withClientConfiguration(clientConfig);
244                }
245                
246                try {
247                        itemHMAC = new ItemHMAC(config.getHMACSHA256Key());
248                } catch (InvalidKeyException e) {
249                        throw new PersistenceException(e.getMessage(), e);
250                }
251                
252                client = builder.build();
253                
254                Loggers.MAIN_LOG.info("[DS0140] DynamoDB store: Expiration thread wake up interval for cache {}: {}", getCacheName(),
255                        ctx.getCache().getCacheConfiguration().expiration().wakeUpInterval());
256                
257                // Load and initialise the DynamoDB item transformer
258                Loggers.MAIN_LOG.debug("[DS0101] Loading DynamoDB item transformer class {} for cache {}...",
259                        config.getItemTransformerClass(),
260                        getCacheName());
261                
262                itemTransformer = loadItemTransformerClass(config.getItemTransformerClass());
263                itemTransformer.init(() -> config.isEnableTTL());
264        
265                requestFactory = new RequestFactory<>(
266                        itemTransformer,
267                        config.getIndexedAttributes(),
268                        config.isConsistentReads(),
269                        config.getProvisionedThroughput(),
270                        config.isTableWithEncryptionAtRest(),
271                        config.getTablePrefix(),
272                        config.getApplyRangeKey(),
273                        config.getRangeKeyValue(),
274                        config.isEnableStream());
275                
276                table = new DynamoDB(client).getTable(requestFactory.getTableName());
277                
278                // Load and initialise the optional query executor
279                if (config.getQueryExecutorClass() != null) {
280                        Loggers.MAIN_LOG.debug("[DS0130] Loading optional DynamoDB query executor class {} for cache {}...",
281                                config.getQueryExecutorClass(),
282                                getCacheName());
283                        
284                        queryExecutor = loadQueryExecutorClass(config.getQueryExecutorClass());
285                        
286                        queryExecutor.init(new DynamoDBQueryExecutorInitContext<K, V>() {
287                                @Override
288                                public DynamoDBItemTransformer<K,V> getDynamoDBItemTransformer() {
289                                        return itemTransformer;
290                                }
291                                
292                                @Override
293                                public Table getDynamoDBTable() {
294                                        return table;
295                                }
296                                
297                                @Override
298                                public Index getDynamoDBIndex(final String attributeName) {
299                                        if (config.getIndexedAttributes() != null && config.getIndexedAttributes().contains(attributeName)) {
300                                                return table.getIndex(requestFactory.getGSIName(attributeName));
301                                        } else {
302                                                return null;
303                                        }
304                                }
305                        });
306                }
307
308                marshalledEntryFactory = (MarshalledEntryFactory<K,V>)ctx.getMarshalledEntryFactory();
309
310                final String metricsPrefix = ctx.getCache().getName() + ".";
311                if (config.getMetricRegistry() == null)
312                        meters = new DynamoDBMeters(metricsPrefix);
313                else
314                        meters = new DynamoDBMeters(metricsPrefix, config.getMetricRegistry());
315
316                Loggers.MAIN_LOG.info("[DS0102] Initialized DynamoDB store for cache {} with table {}",
317                        getCacheName(),
318                        table.getTableName());
319        }
320        
321        
322        @Override
323        public void start() {
324
325                // This method will be invoked by the PersistenceManager to start the CacheLoader. At this stage configuration
326                // is complete and the loader can perform operations such as opening a connection to the external storage,
327                // initialize internal data structures, etc.
328                
329                try {
330                        table = new DynamoDB(client).createTable(requestFactory
331                                .resolveCreateTableRequest()
332                                .withProvisionedThroughput(
333                                        config.getProvisionedThroughput()));
334                        
335                        Loggers.MAIN_LOG.info("[DS0129] DynamoDB store: Created table {} for cache {}", table.getTableName(), getCacheName());
336                        
337                } catch (ResourceInUseException e) {
338                        // table exists
339                } catch (Exception e) {
340                        Loggers.MAIN_LOG.fatal("[DS0103] DynamoDB store: Couldn't create table {}: {}: {}", table.getTableName(), e.getMessage(), e);
341                        throw new PersistenceException(e.getMessage(), e);
342                }
343                
344                try {
345                        table.waitForActive();
346                } catch (InterruptedException e) {
347                        throw new PersistenceException("Interrupted while awaiting DynamoDB table " + table.getTableName() + " to become active: " + e.getMessage(), e);
348                }
349                
350                Loggers.MAIN_LOG.info("[DS0141] DynamoDB store: Table properties: " + table.getDescription());
351                
352                
353                ContinuousBackupsStatus contBackupStatus = null;
354                
355                try {
356                        // Not available on localhost DynamoDB
357                        DescribeContinuousBackupsResult result = client.describeContinuousBackups(new DescribeContinuousBackupsRequest().withTableName(table.getTableName()));
358                        contBackupStatus = ContinuousBackupsStatus.fromValue(result.getContinuousBackupsDescription().getContinuousBackupsStatus());
359                        Loggers.MAIN_LOG.info("[DS0143] DynamoDB store: Continuous backup status for table {}: {}", table.getTableName(), contBackupStatus);
360                        Loggers.MAIN_LOG.info("[DS0144] DynamoDB store: Point in time recovery status for table {}: {}",
361                                table.getTableName(),
362                                result.getContinuousBackupsDescription().getPointInTimeRecoveryDescription().getPointInTimeRecoveryStatus());
363                } catch (Exception e) {
364                        Loggers.MAIN_LOG.error("[DS0145] DynamoDB store: Couldn't obtain continuous backup status for table {}: {}", table.getTableName(), e.getMessage());
365                }
366                
367                if (ContinuousBackupsStatus.DISABLED.equals(contBackupStatus) && config.isEnableContinuousBackups()) {
368                        
369                        try {
370                                client.updateContinuousBackups(
371                                        new UpdateContinuousBackupsRequest()
372                                                .withTableName(table.getTableName())
373                                                .withPointInTimeRecoverySpecification(
374                                                        new PointInTimeRecoverySpecification()
375                                                                .withPointInTimeRecoveryEnabled(true)));
376                                
377                        } catch (Exception e) {
378                                String msg = "Couldn't set up continuous backups for table " + table.getTableName() + ": " + e.getMessage();
379                                Loggers.MAIN_LOG.fatal("[DS0104] DynamoDB store: {}", msg);
380                                throw new PersistenceException(msg, e);
381                        }
382                }
383                
384                TimeToLiveStatus currentTTLStatus = null;
385                
386                try {
387                        // Not available on localhost DynamoDB
388                        DescribeTimeToLiveResult result = client.describeTimeToLive(new DescribeTimeToLiveRequest().withTableName(table.getTableName()));
389                        currentTTLStatus = TimeToLiveStatus.fromValue(result.getTimeToLiveDescription().getTimeToLiveStatus());
390                        Loggers.MAIN_LOG.info("[DS0161] DynamoDB store: TTL for table {}: status={} attribute={}",
391                                table.getTableName(),
392                                currentTTLStatus,
393                                result.getTimeToLiveDescription().getAttributeName());
394                } catch (Exception e) {
395                        Loggers.MAIN_LOG.error("[DS0162] DynamoDB store: Couldn't obtain TTL status for table {}: {}", table.getTableName(), e.getMessage());
396                }
397                
398                if (TimeToLiveStatus.DISABLED.equals(currentTTLStatus) && config.isEnableTTL() && requestFactory.getItemTransformer().getTTLAttributeName() != null) {
399                        try {
400                                client.updateTimeToLive(
401                                        new UpdateTimeToLiveRequest()
402                                                .withTableName(table.getTableName())
403                                                .withTimeToLiveSpecification(
404                                                        new TimeToLiveSpecification()
405                                                                .withAttributeName(requestFactory.getItemTransformer().getTTLAttributeName())
406                                                                .withEnabled(true)));
407                        } catch (Exception e) {
408                                String msg = "Couldn't set up TTL for table " + table.getTableName() + ": " + e.getMessage();
409                                Loggers.MAIN_LOG.fatal("[DS0160] DynamoDB store: {}", msg);
410                                throw new PersistenceException(msg, e);
411                        }
412                }
413                
414                reaper = new ExpiredEntryReaper<>(marshalledEntryFactory, table, requestFactory, config.getPurgeLimit(), meters.purgeTimer);
415                
416                Loggers.MAIN_LOG.info("[DS0104] Started DynamoDB external store connector for cache {} with table {}", getCacheName(), table.getTableName());
417        }
418
419
420        @Override
421        public void stop() {
422
423                super.stop();
424                
425                if (client != null) {
426                        client.shutdown();
427                }
428                
429                Loggers.MAIN_LOG.info("[DS0105] Stopped DynamoDB store connector for cache {}",  getCacheName());
430        }
431
432
433        @Override
434        public boolean contains(final Object key) {
435
436                // This method will be invoked by the PersistenceManager to determine if the loader contains the specified key.
437                // The implementation should be as fast as possible, e.g. it should strive to transfer the least amount of data possible
438                // from the external storage to perform the check. Also, if possible, make sure the field is indexed on the external storage
439                // so that its existence can be determined as quickly as possible.
440                //
441                // Note that keys will be in the cache's native format, which means that if the cache is being used by a remoting protocol
442                // such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].
443
444                Loggers.DYNAMODB_LOG.trace("[DS0106] DynamoDB store: Checking {} cache key {}", getCacheName(), key);
445                
446                Timer.Context timerCtx = meters.getTimer.time();
447                
448                try {
449                        return table.getItem(requestFactory.resolveGetItemSpec(key)) != null;
450                        
451                } catch (Exception e) {
452                        Loggers.DYNAMODB_LOG.error("[DS0107] {}: {}", e.getMessage(), e);
453                        throw new PersistenceException(e.getMessage(), e);
454                } finally {
455                        timerCtx.stop();
456                }
457        }
458
459
460        @Override
461        public MarshalledEntry<K,V> load(final Object key) {
462
463                // Fetches an entry from the storage using the specified key. The CacheLoader should retrieve from the external storage all
464                // of the data that is needed to reconstruct the entry in memory, i.e. the value and optionally the metadata. This method
465                // needs to return a MarshalledEntry which can be constructed as follows:
466                //
467                // ctx.getMarshalledEntryFactory().new MarshalledEntry(key, value, metadata);
468                //
469                // If the entry does not exist or has expired, this method should return null.
470                // If an error occurs while retrieving data from the external storage, this method should throw a PersistenceException
471                //
472                // Note that keys and values will be in the cache's native format, which means that if the cache is being used by a remoting protocol
473                // such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].
474                // If the loader needs to have knowledge of the key/value data beyond their binary representation, then it needs access to the key's and value's
475                // classes and the marshaller used to encode them.
476
477                Loggers.DYNAMODB_LOG.trace("[DS0108] DynamoDB store: Loading {} cache entry with key {}", getCacheName(), key);
478                
479                Item item;
480                
481                Timer.Context timerCtx = meters.getTimer.time();
482                
483                GetItemSpec getItemSpec = requestFactory.resolveGetItemSpec(key);
484                
485                try {
486                        item = table.getItem(getItemSpec);
487                        
488                } catch (Exception e) {
489                        Loggers.DYNAMODB_LOG.error("[DS0109] {}, {}", e.getMessage(), e);
490                        throw new PersistenceException(e.getMessage(), e);
491                } finally {
492                        timerCtx.stop();
493                }
494                
495                if (item == null) {
496                        // Not found
497                        Loggers.DYNAMODB_LOG.trace("[DS0110] DynamoDB store: Item with key {} not found", key);
498                        return null;
499                }
500                
501                if (Loggers.DYNAMODB_LOG.isTraceEnabled()) {
502                        Loggers.DYNAMODB_LOG.trace("[DS0111] DynamoDB store: Retrieved item: {}", item);
503                }
504                
505                try {
506                        item = itemHMAC.verify(item);
507                } catch (InvalidHMACException e) {
508                        meters.invalidItemHmacCounter.inc();
509                        Loggers.DYNAMODB_LOG.error("[DS0131] DynamoDB store: Invalid item HMAC in {} for {}", getCacheName(), item.toJSON());
510                        throw new PersistenceException(e.getMessage(), e);
511                } catch (NoSuchAlgorithmException | InvalidKeyException e) {
512                        throw new PersistenceException(e.getMessage(), e);
513                }
514                
515                // Transform DynamoDB entry to Infinispan entry
516                InfinispanEntry<K,V> infinispanEntry = itemTransformer.toInfinispanEntry(item);
517                
518                if (infinispanEntry.isExpired()) {
519                        Loggers.DYNAMODB_LOG.trace("[DS0114] DynamoDB store: Item with key {} expired", key);
520                        return null;
521                }
522
523                return marshalledEntryFactory.newMarshalledEntry(
524                        infinispanEntry.getKey(),
525                        infinispanEntry.getValue(),
526                        infinispanEntry.getMetadata());
527        }
528
529
530        @Override
531        public boolean delete(final Object key) {
532
533                // The CacheWriter should remove from the external storage the entry identified by the specified key.
534                // Note that keys will be in the cache's native format, which means that if the cache is being used by a remoting protocol
535                // such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].
536
537                Loggers.DYNAMODB_LOG.trace("[DS0112] DynamoDB store: Deleting {} cache entry with key {}", getCacheName(), key);
538                
539                final boolean deleted;
540                
541                Timer.Context timerCtx = meters.deleteTimer.time();
542                
543                try {
544                        deleted = table.deleteItem(requestFactory.resolveDeleteItemSpec(key)).getItem() != null;
545                        
546                } catch (Exception e) {
547                        Loggers.DYNAMODB_LOG.error("[DS0113] {}, {}", e.getMessage(), e);
548                        throw new PersistenceException(e.getMessage(), e);
549                } finally {
550                        timerCtx.stop();
551                }
552                
553                if (deleted) {
554                        Loggers.DYNAMODB_LOG.trace("[DS0116] DynamoDB store: Deleted item with key {}", key);
555                }
556                
557                return deleted;
558        }
559
560
561        @Override
562        public void write(final MarshalledEntry<? extends K, ? extends V> marshalledEntry) {
563
564                // The CacheWriter should write the specified entry to the external storage.
565                //
566                // The PersistenceManager uses MarshalledEntry as the default format so that CacheWriters can efficiently store data coming
567                // from a remote node, thus avoiding any additional transformation steps.
568                //
569                // Note that keys and values will be in the cache's native format, which means that if the cache is being used by a remoting protocol
570                // such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].
571
572                Loggers.DYNAMODB_LOG.trace("[DS0115] DynamoDB store: Writing {} cache entry {}", getCacheName(), marshalledEntry);
573                
574                Timer.Context timerCtx = meters.putTimer.time();
575                
576                try {
577                        Item item = requestFactory.resolveItem(
578                                new InfinispanEntry<>(
579                                        marshalledEntry.getKey(),
580                                        marshalledEntry.getValue(),
581                                        marshalledEntry.getMetadata()));
582                        
583                        item = itemHMAC.apply(item);
584                        
585                        table.putItem(item);
586                        
587                } catch (Exception e) {
588                        Loggers.DYNAMODB_LOG.error("[DS0117] {}: {}", e.getMessage(), e);
589                        throw new PersistenceException(e.getMessage(), e);
590                } finally {
591                        timerCtx.stop();
592                }
593        }
594
595        @Override
596        public Publisher<MarshalledEntry<K, V>> publishEntries(Predicate<? super K> filter, boolean fetchValue, boolean fetchMetadata) {
597                
598                Loggers.DYNAMODB_LOG.trace("[DS0118] DynamoDB store: Processing key filter for {} cache: fetchValue={} fetchMetadata={}",
599                                getCacheName(), fetchValue, fetchMetadata);
600                
601                final Instant now = Instant.now();
602                
603                return Flowable.using(meters.processTimer::time,
604                                ignore -> Flowable.fromIterable(requestFactory.getAllItems(table))
605                                                .map(itemTransformer::toInfinispanEntry)
606                                                .filter(infinispanEntry -> filter == null || filter.test(infinispanEntry.getKey()))
607                                                .filter(infinispanEntry -> ! infinispanEntry.isExpired(now))
608                                                .map(infinispanEntry -> marshalledEntryFactory.newMarshalledEntry(
609                                                                infinispanEntry.getKey(),
610                                                                infinispanEntry.getValue(),
611                                                                infinispanEntry.getMetadata()))
612                                                .doOnError(e -> Loggers.DYNAMODB_LOG.error("[DS0119] {}: {}", e.getMessage(), e)),
613                                Timer.Context::stop);
614        }
615
616        @Override
617        public int size() {
618
619                // Infinispan code analysis on 8.2 and 9.4.11 shows that this method is never called in practice, and
620                // is not wired to the data / cache container API
621                
622                // TODO inaccurate when a range key is applied!
623
624                Loggers.DYNAMODB_LOG.trace("[DS0120] DynamoDB store: Counting {} records", getCacheName());
625
626                final int count;
627                
628                try {
629                        count = table.describe().getItemCount().intValue();
630                        
631                } catch (Exception e) {
632                        Loggers.DYNAMODB_LOG.error("[DS0121] {}: {}", e.getMessage(), e);
633                        throw new PersistenceException(e.getMessage(), e);
634                }
635
636                Loggers.DYNAMODB_LOG.trace("[DS0122] DynamoDB store: Reported approximately {} {} items", count, getCacheName());
637
638                return count;
639        }
640
641
642        @Override
643        public void clear() {
644
645                Loggers.DYNAMODB_LOG.trace("[DS0123] DynamoDB store: Clearing {} items", getCacheName());
646                
647                if (requestFactory.getRangeKeyResolvedName() != null) {
648                        throw new PersistenceException("DynamoDB clear operation not supported with applied range key");
649                }
650
651                try {
652                        DeleteTableResult result = table.delete();
653                        
654                        int numDeleted = result.getTableDescription().getItemCount().intValue();
655                        
656                        Loggers.DYNAMODB_LOG.info("[DS0125] DynamoDB store: Cleared {} {} items", numDeleted, table.getTableName());
657                        
658                        table.waitForDelete();
659                        
660                        client.createTable(requestFactory.resolveCreateTableRequest());
661                        
662                        table.waitForActive();
663                        
664                } catch (Exception e) {
665                        Loggers.DYNAMODB_LOG.error("[DS0124] {}: {}", e.getMessage(), e);
666                        throw new PersistenceException(e.getMessage(), e);
667                }
668        }
669
670
671        @Override
672        public void purge(final Executor executor, final PurgeListener<? super K> purgeListener) {
673
674                Loggers.DYNAMODB_LOG.trace("[DS0126] DynamoDB store: Purging {} cache entries", getCacheName());
675                
676                try {
677                        executor.execute(() -> reaper.purge(purgeListener));
678                        
679                } catch (Exception e) {
680                        Loggers.DYNAMODB_LOG.error("[DS0127] {}: {}", e.getMessage(), e);
681                        throw new PersistenceException("Purge exception: " + e.getMessage(), e);
682                }
683        }
684        
685        
686        @Override
687        public void purge(final Executor executor, final ExpirationPurgeListener<K,V> purgeListener) {
688                
689                Loggers.DYNAMODB_LOG.trace("[DS0150] DynamoDB store: Purging {} cache entries", getCacheName());
690                
691                try {
692                        executor.execute(() -> reaper.purgeExtended(purgeListener));
693                        
694                } catch (Exception e) {
695                        Loggers.DYNAMODB_LOG.error("[DS0151] {}: {}", e.getMessage(), e);
696                        throw new PersistenceException("Purge exception: " + e.getMessage(), e);
697                }
698        }
699}