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                CreateTableRequest ctr = requestFactory.resolveCreateTableRequest();
330                
331                try {
332                        table = new DynamoDB(client)
333                                .createTable(
334                                        ctr.withProvisionedThroughput(config.getProvisionedThroughput())
335                                );
336                        
337                        Loggers.MAIN_LOG.info("[DS0129] DynamoDB store: Created table {} for cache {}", table.getTableName(), getCacheName());
338                        
339                } catch (ResourceInUseException e) {
340                        // table exists
341                        Loggers.MAIN_LOG.info("[DS0133] DynamoDB store: Table {} for cache {} exists", table.getTableName(), getCacheName());
342                } catch (Exception e) {
343                        Loggers.MAIN_LOG.fatal("[DS0103] DynamoDB store: Couldn't create table {} with {}: {}: {}", table.getTableName(), ctr, e.getMessage(), e);
344                        throw new PersistenceException(e.getMessage(), e);
345                }
346                
347                try {
348                        table.waitForActive();
349                } catch (InterruptedException e) {
350                        throw new PersistenceException("Interrupted while awaiting DynamoDB table " + table.getTableName() + " to become active: " + e.getMessage(), e);
351                }
352                
353                Loggers.MAIN_LOG.info("[DS0141] DynamoDB store: Table properties: " + table.getDescription());
354                
355                
356                ContinuousBackupsStatus contBackupStatus = null;
357                
358                try {
359                        // Not available on localhost DynamoDB
360                        DescribeContinuousBackupsResult result = client.describeContinuousBackups(new DescribeContinuousBackupsRequest().withTableName(table.getTableName()));
361                        contBackupStatus = ContinuousBackupsStatus.fromValue(result.getContinuousBackupsDescription().getContinuousBackupsStatus());
362                        Loggers.MAIN_LOG.info("[DS0143] DynamoDB store: Continuous backup status for table {}: {}", table.getTableName(), contBackupStatus);
363                        Loggers.MAIN_LOG.info("[DS0144] DynamoDB store: Point in time recovery status for table {}: {}",
364                                table.getTableName(),
365                                result.getContinuousBackupsDescription().getPointInTimeRecoveryDescription().getPointInTimeRecoveryStatus());
366                } catch (Exception e) {
367                        Loggers.MAIN_LOG.error("[DS0145] DynamoDB store: Couldn't obtain continuous backup status for table {}: {}", table.getTableName(), e.getMessage());
368                }
369                
370                if (ContinuousBackupsStatus.DISABLED.equals(contBackupStatus) && config.isEnableContinuousBackups()) {
371                        
372                        try {
373                                client.updateContinuousBackups(
374                                        new UpdateContinuousBackupsRequest()
375                                                .withTableName(table.getTableName())
376                                                .withPointInTimeRecoverySpecification(
377                                                        new PointInTimeRecoverySpecification()
378                                                                .withPointInTimeRecoveryEnabled(true)));
379                                
380                        } catch (Exception e) {
381                                String msg = "Couldn't set up continuous backups for table " + table.getTableName() + ": " + e.getMessage();
382                                Loggers.MAIN_LOG.fatal("[DS0104] DynamoDB store: {}", msg);
383                                throw new PersistenceException(msg, e);
384                        }
385                }
386                
387                TimeToLiveStatus currentTTLStatus = null;
388                
389                try {
390                        // Not available on localhost DynamoDB
391                        DescribeTimeToLiveResult result = client.describeTimeToLive(new DescribeTimeToLiveRequest().withTableName(table.getTableName()));
392                        currentTTLStatus = TimeToLiveStatus.fromValue(result.getTimeToLiveDescription().getTimeToLiveStatus());
393                        Loggers.MAIN_LOG.info("[DS0161] DynamoDB store: TTL for table {}: status={} attribute={}",
394                                table.getTableName(),
395                                currentTTLStatus,
396                                result.getTimeToLiveDescription().getAttributeName());
397                } catch (Exception e) {
398                        Loggers.MAIN_LOG.error("[DS0162] DynamoDB store: Couldn't obtain TTL status for table {}: {}", table.getTableName(), e.getMessage());
399                }
400                
401                if (TimeToLiveStatus.DISABLED.equals(currentTTLStatus) && config.isEnableTTL() && requestFactory.getItemTransformer().getTTLAttributeName() != null) {
402                        try {
403                                client.updateTimeToLive(
404                                        new UpdateTimeToLiveRequest()
405                                                .withTableName(table.getTableName())
406                                                .withTimeToLiveSpecification(
407                                                        new TimeToLiveSpecification()
408                                                                .withAttributeName(requestFactory.getItemTransformer().getTTLAttributeName())
409                                                                .withEnabled(true)));
410                        } catch (Exception e) {
411                                String msg = "Couldn't set up TTL for table " + table.getTableName() + ": " + e.getMessage();
412                                Loggers.MAIN_LOG.fatal("[DS0160] DynamoDB store: {}", msg);
413                                throw new PersistenceException(msg, e);
414                        }
415                }
416                
417                reaper = new ExpiredEntryReaper<>(marshalledEntryFactory, table, requestFactory, config.getPurgeLimit(), meters.purgeTimer);
418                
419                Loggers.MAIN_LOG.info("[DS0104] Started DynamoDB external store connector for cache {} with table {}", getCacheName(), table.getTableName());
420        }
421
422
423        @Override
424        public void stop() {
425
426                super.stop();
427                
428                if (client != null) {
429                        client.shutdown();
430                }
431                
432                Loggers.MAIN_LOG.info("[DS0105] Stopped DynamoDB store connector for cache {}",  getCacheName());
433        }
434
435
436        @Override
437        public boolean contains(final Object key) {
438
439                // This method will be invoked by the PersistenceManager to determine if the loader contains the specified key.
440                // The implementation should be as fast as possible, e.g. it should strive to transfer the least amount of data possible
441                // from the external storage to perform the check. Also, if possible, make sure the field is indexed on the external storage
442                // so that its existence can be determined as quickly as possible.
443                //
444                // Note that keys will be in the cache's native format, which means that if the cache is being used by a remoting protocol
445                // such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].
446
447                Loggers.DYNAMODB_LOG.trace("[DS0106] DynamoDB store: Checking {} cache key {}", getCacheName(), key);
448                
449                Timer.Context timerCtx = meters.getTimer.time();
450                
451                try {
452                        return table.getItem(requestFactory.resolveGetItemSpec(key)) != null;
453                        
454                } catch (Exception e) {
455                        Loggers.DYNAMODB_LOG.error("[DS0107] {}: {}", e.getMessage(), e);
456                        throw new PersistenceException(e.getMessage(), e);
457                } finally {
458                        timerCtx.stop();
459                }
460        }
461
462
463        @Override
464        public MarshalledEntry<K,V> load(final Object key) {
465
466                // Fetches an entry from the storage using the specified key. The CacheLoader should retrieve from the external storage all
467                // of the data that is needed to reconstruct the entry in memory, i.e. the value and optionally the metadata. This method
468                // needs to return a MarshalledEntry which can be constructed as follows:
469                //
470                // ctx.getMarshalledEntryFactory().new MarshalledEntry(key, value, metadata);
471                //
472                // If the entry does not exist or has expired, this method should return null.
473                // If an error occurs while retrieving data from the external storage, this method should throw a PersistenceException
474                //
475                // 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
476                // such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].
477                // 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
478                // classes and the marshaller used to encode them.
479
480                Loggers.DYNAMODB_LOG.trace("[DS0108] DynamoDB store: Loading {} cache entry with key {}", getCacheName(), key);
481                
482                Item item;
483                
484                Timer.Context timerCtx = meters.getTimer.time();
485                
486                GetItemSpec getItemSpec = requestFactory.resolveGetItemSpec(key);
487                
488                try {
489                        item = table.getItem(getItemSpec);
490                        
491                } catch (Exception e) {
492                        Loggers.DYNAMODB_LOG.error("[DS0109] {}, {}", e.getMessage(), e);
493                        throw new PersistenceException(e.getMessage(), e);
494                } finally {
495                        timerCtx.stop();
496                }
497                
498                if (item == null) {
499                        // Not found
500                        Loggers.DYNAMODB_LOG.trace("[DS0110] DynamoDB store: Item with key {} not found", key);
501                        return null;
502                }
503                
504                Loggers.DYNAMODB_LOG.trace("[DS0111] DynamoDB store: Retrieved {} cache item: {}", getCacheName(), item);
505                
506                try {
507                        item = itemHMAC.verify(item);
508                } catch (InvalidHMACException e) {
509                        meters.invalidItemHmacCounter.inc();
510                        Loggers.DYNAMODB_LOG.error("[DS0131] DynamoDB store: Invalid item HMAC in {}: {}", getCacheName(), e.getMessage());
511                        throw new PersistenceException(e.getMessage(), e);
512                } catch (NoSuchAlgorithmException | InvalidKeyException e) {
513                        throw new PersistenceException(e.getMessage(), e);
514                }
515                
516                // Transform DynamoDB entry to Infinispan entry
517                InfinispanEntry<K,V> infinispanEntry = itemTransformer.toInfinispanEntry(item);
518                
519                if (infinispanEntry.isExpired()) {
520                        Loggers.DYNAMODB_LOG.trace("[DS0114] DynamoDB store: Item with key {} expired", key);
521                        return null;
522                }
523
524                return marshalledEntryFactory.newMarshalledEntry(
525                        infinispanEntry.getKey(),
526                        infinispanEntry.getValue(),
527                        infinispanEntry.getMetadata());
528        }
529
530
531        @Override
532        public boolean delete(final Object key) {
533
534                // The CacheWriter should remove from the external storage the entry identified by the specified key.
535                // Note that keys will be in the cache's native format, which means that if the cache is being used by a remoting protocol
536                // such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].
537
538                Loggers.DYNAMODB_LOG.trace("[DS0112] DynamoDB store: Deleting {} cache entry with key {}", getCacheName(), key);
539                
540                final boolean deleted;
541                
542                Timer.Context timerCtx = meters.deleteTimer.time();
543                
544                try {
545                        deleted = table.deleteItem(requestFactory.resolveDeleteItemSpec(key)).getItem() != null;
546                        
547                } catch (Exception e) {
548                        Loggers.DYNAMODB_LOG.error("[DS0113] {}, {}", e.getMessage(), e);
549                        throw new PersistenceException(e.getMessage(), e);
550                } finally {
551                        timerCtx.stop();
552                }
553                
554                if (deleted) {
555                        Loggers.DYNAMODB_LOG.trace("[DS0116] DynamoDB store: Deleted {} cache item with key {}", getCacheName(), key);
556                }
557                
558                return deleted;
559        }
560
561
562        @Override
563        public void write(final MarshalledEntry<? extends K, ? extends V> marshalledEntry) {
564
565                // The CacheWriter should write the specified entry to the external storage.
566                //
567                // The PersistenceManager uses MarshalledEntry as the default format so that CacheWriters can efficiently store data coming
568                // from a remote node, thus avoiding any additional transformation steps.
569                //
570                // 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
571                // such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].
572
573                Loggers.DYNAMODB_LOG.trace("[DS0115] DynamoDB store: Writing {} cache entry {}", getCacheName(), marshalledEntry);
574                
575                Timer.Context timerCtx = meters.putTimer.time();
576                
577                try {
578                        Item item = requestFactory.resolveItem(
579                                new InfinispanEntry<>(
580                                        marshalledEntry.getKey(),
581                                        marshalledEntry.getValue(),
582                                        marshalledEntry.getMetadata()));
583                        
584                        item = itemHMAC.apply(item);
585                        
586                        Loggers.DYNAMODB_LOG.trace("[DS0132] DynamoDB store: Writing {} cache item: {}", getCacheName(), item);
587                        
588                        table.putItem(item);
589                        
590                } catch (Exception e) {
591                        Loggers.DYNAMODB_LOG.error("[DS0117] {}: {}", e.getMessage(), e);
592                        throw new PersistenceException(e.getMessage(), e);
593                } finally {
594                        timerCtx.stop();
595                }
596        }
597
598        @Override
599        public Publisher<MarshalledEntry<K, V>> publishEntries(final Predicate<? super K> filter, final boolean fetchValue, final boolean fetchMetadata) {
600                
601                Loggers.DYNAMODB_LOG.trace("[DS0118] DynamoDB store: Processing key filter for {} cache: fetchValue={} fetchMetadata={}",
602                                getCacheName(), fetchValue, fetchMetadata);
603                
604                final Instant now = Instant.now();
605                
606                return Flowable.using(meters.processTimer::time,
607                                ignore -> Flowable.fromIterable(requestFactory.getAllItems(table))
608                                                .map(itemTransformer::toInfinispanEntry)
609                                                .filter(infinispanEntry -> filter == null || filter.test(infinispanEntry.getKey()))
610                                                .filter(infinispanEntry -> ! infinispanEntry.isExpired(now))
611                                                .map(infinispanEntry -> marshalledEntryFactory.newMarshalledEntry(
612                                                                infinispanEntry.getKey(),
613                                                                infinispanEntry.getValue(),
614                                                                infinispanEntry.getMetadata()))
615                                                .doOnError(e -> Loggers.DYNAMODB_LOG.error("[DS0119] {}: {}", e.getMessage(), e)),
616                                Timer.Context::stop);
617        }
618
619        @Override
620        public int size() {
621
622                // Infinispan code analysis on 8.2 and 9.4.11 shows that this method is never called in practice, and
623                // is not wired to the data / cache container API
624                
625                // TODO inaccurate when a range key is applied!
626
627                Loggers.DYNAMODB_LOG.trace("[DS0120] DynamoDB store: Counting {} cache items", getCacheName());
628
629                final int count;
630                
631                try {
632                        count = table.describe().getItemCount().intValue();
633                        
634                } catch (Exception e) {
635                        Loggers.DYNAMODB_LOG.error("[DS0121] {}: {}", e.getMessage(), e);
636                        throw new PersistenceException(e.getMessage(), e);
637                }
638
639                Loggers.DYNAMODB_LOG.trace("[DS0122] DynamoDB store: Reported approximately {} {} items", count, getCacheName());
640
641                return count;
642        }
643
644
645        @Override
646        public void clear() {
647
648                Loggers.DYNAMODB_LOG.trace("[DS0123] DynamoDB store: Clearing {} items", getCacheName());
649                
650                if (requestFactory.getRangeKeyResolvedName() != null) {
651                        throw new PersistenceException("DynamoDB clear operation not supported with applied range key");
652                }
653
654                try {
655                        DeleteTableResult result = table.delete();
656                        
657                        int numDeleted = result.getTableDescription().getItemCount().intValue();
658                        
659                        Loggers.DYNAMODB_LOG.info("[DS0125] DynamoDB store: Cleared {} {} items", numDeleted, table.getTableName());
660                        
661                        table.waitForDelete();
662                        
663                        client.createTable(requestFactory.resolveCreateTableRequest());
664                        
665                        table.waitForActive();
666                        
667                } catch (Exception e) {
668                        Loggers.DYNAMODB_LOG.error("[DS0124] {}: {}", e.getMessage(), e);
669                        throw new PersistenceException(e.getMessage(), e);
670                }
671        }
672
673
674        @Override
675        public void purge(final Executor executor, final PurgeListener<? super K> purgeListener) {
676
677                Loggers.DYNAMODB_LOG.trace("[DS0126] DynamoDB store: Purging {} cache entries", getCacheName());
678                
679                try {
680                        executor.execute(() -> reaper.purge(purgeListener));
681                        
682                } catch (Exception e) {
683                        Loggers.DYNAMODB_LOG.error("[DS0127] {}: {}", e.getMessage(), e);
684                        throw new PersistenceException("Purge exception: " + e.getMessage(), e);
685                }
686        }
687        
688        
689        @Override
690        public void purge(final Executor executor, final ExpirationPurgeListener<K,V> purgeListener) {
691                
692                Loggers.DYNAMODB_LOG.trace("[DS0150] DynamoDB store: Purging {} cache entries", getCacheName());
693                
694                try {
695                        executor.execute(() -> reaper.purgeExtended(purgeListener));
696                        
697                } catch (Exception e) {
698                        Loggers.DYNAMODB_LOG.error("[DS0151] {}: {}", e.getMessage(), e);
699                        throw new PersistenceException("Purge exception: " + e.getMessage(), e);
700                }
701        }
702}