Jelajahi Sumber

improve localdb iteration performance

Jason Rivard 5 tahun lalu
induk
melakukan
7f672008e6
21 mengubah file dengan 135 tambahan dan 115 penghapusan
  1. 1 2
      server/src/main/java/password/pwm/PwmEnvironment.java
  2. 1 2
      server/src/main/java/password/pwm/http/ContextManager.java
  3. 4 0
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerLocalDBServlet.java
  4. 6 5
      server/src/main/java/password/pwm/svc/intruder/DataStoreRecordStore.java
  5. 2 2
      server/src/main/java/password/pwm/svc/node/DatabaseNodeDataService.java
  6. 3 2
      server/src/main/java/password/pwm/svc/report/UserCacheService.java
  7. 3 2
      server/src/main/java/password/pwm/svc/token/DataStoreTokenMachine.java
  8. 5 3
      server/src/main/java/password/pwm/svc/wordlist/SharedHistoryManager.java
  9. 3 1
      server/src/main/java/password/pwm/util/DataStore.java
  10. 2 1
      server/src/main/java/password/pwm/util/db/DatabaseAccessor.java
  11. 10 7
      server/src/main/java/password/pwm/util/db/DatabaseAccessorImpl.java
  12. 3 1
      server/src/main/java/password/pwm/util/db/DatabaseDataStore.java
  13. 13 25
      server/src/main/java/password/pwm/util/localdb/AbstractJDBCLocalDB.java
  14. 2 2
      server/src/main/java/password/pwm/util/localdb/LocalDB.java
  15. 1 1
      server/src/main/java/password/pwm/util/localdb/LocalDBAdaptor.java
  16. 1 1
      server/src/main/java/password/pwm/util/localdb/LocalDBDataStore.java
  17. 1 1
      server/src/main/java/password/pwm/util/localdb/LocalDBProvider.java
  18. 5 4
      server/src/main/java/password/pwm/util/localdb/LocalDBStoredQueue.java
  19. 37 25
      server/src/main/java/password/pwm/util/localdb/LocalDBUtility.java
  20. 17 17
      server/src/main/java/password/pwm/util/localdb/MemoryLocalDB.java
  21. 15 11
      server/src/main/java/password/pwm/util/localdb/XodusLocalDB.java

+ 1 - 2
server/src/main/java/password/pwm/PwmEnvironment.java

@@ -41,7 +41,6 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -638,7 +637,7 @@ public class PwmEnvironment
             try
             {
                 final Properties props = new Properties();
-                props.put( "timestamp", JavaHelper.toIsoDate( new Date() ) );
+                props.put( "timestamp", JavaHelper.toIsoDate( Instant.now() ) );
                 props.put( "applicationPath", PwmEnvironment.this.getApplicationPath() == null ? "n/a" : PwmEnvironment.this.getApplicationPath().getAbsolutePath() );
                 props.put( "configurationFile", PwmEnvironment.this.getConfigurationFile() == null ? "n/a" : PwmEnvironment.this.getConfigurationFile().getAbsolutePath() );
                 final String comment = PwmConstants.PWM_APP_NAME + " file lock";

+ 1 - 2
server/src/main/java/password/pwm/http/ContextManager.java

@@ -64,7 +64,6 @@ import java.security.cert.X509Certificate;
 import java.time.Instant;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -617,7 +616,7 @@ public class ContextManager implements Serializable
 
     private static void outputError( final String outputText )
     {
-        final String msg = PwmConstants.PWM_APP_NAME + " " + JavaHelper.toIsoDate( new Date() ) + " " + outputText;
+        final String msg = PwmConstants.PWM_APP_NAME + " " + JavaHelper.toIsoDate( Instant.now() ) + " " + outputText;
         System.out.println( msg );
         System.out.println( msg );
     }

+ 4 - 0
server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerLocalDBServlet.java

@@ -185,16 +185,20 @@ public class ConfigManagerLocalDBServlet extends AbstractPwmServlet
         LocalDB localDB = null;
         try
         {
+            localDB = pwmApplication.getLocalDB();
             final File localDBLocation = pwmApplication.getLocalDB().getFileLocation();
             final Configuration configuration = pwmApplication.getConfig();
             contextManager.shutdown();
 
+            localDB.close();
             localDB = LocalDBFactory.getInstance( localDBLocation, false, null, configuration );
+
             final LocalDBUtility localDBUtility = new LocalDBUtility( localDB );
             LOGGER.info( pwmRequest, () -> "beginning LocalDB import" );
             localDBUtility.importLocalDB( inputStream,
                     LOGGER.asAppendable( PwmLogLevel.DEBUG, pwmRequest.getLabel() ) );
             LOGGER.info( pwmRequest, () -> "completed LocalDB import" );
+            localDB.close();
         }
         catch ( final Exception e )
         {

+ 6 - 5
server/src/main/java/password/pwm/svc/intruder/DataStoreRecordStore.java

@@ -36,6 +36,7 @@ import password.pwm.util.logging.PwmLogger;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 class DataStoreRecordStore implements RecordStore
 {
@@ -132,9 +133,9 @@ class DataStoreRecordStore implements RecordStore
 
     private class RecordIterator implements ClosableIterator<IntruderRecord>
     {
-        private final ClosableIterator<String> dbIterator;
+        private final ClosableIterator<Map.Entry<String, String>> dbIterator;
 
-        private RecordIterator( final ClosableIterator<String> dbIterator )
+        private RecordIterator( final ClosableIterator<Map.Entry<String, String>> dbIterator )
         {
             this.dbIterator = dbIterator;
         }
@@ -148,7 +149,7 @@ class DataStoreRecordStore implements RecordStore
         @Override
         public IntruderRecord next( )
         {
-            final String key = dbIterator.next();
+            final String key = dbIterator.next().getKey();
             try
             {
                 return read( key );
@@ -220,11 +221,11 @@ class DataStoreRecordStore implements RecordStore
     private List<String> discoverPurgableKeys( final TimeDuration maxRecordAge )
     {
         final List<String> recordsToRemove = new ArrayList<>();
-        try ( ClosableIterator<String> dbIterator = dataStore.iterator() )
+        try ( ClosableIterator<Map.Entry<String, String>> dbIterator = dataStore.iterator() )
         {
             while ( intruderManager.status() == PwmService.STATUS.OPEN && dbIterator.hasNext() && recordsToRemove.size() < MAX_REMOVALS_PER_CYCLE )
             {
-                final String key = dbIterator.next();
+                final String key = dbIterator.next().getKey();
                 final IntruderRecord record = read( key );
                 if ( record != null )
                 {

+ 2 - 2
server/src/main/java/password/pwm/svc/node/DatabaseNodeDataService.java

@@ -79,11 +79,11 @@ class DatabaseNodeDataService implements NodeDataServiceProvider
     {
         final Map<String, StoredNodeData> returnList = new LinkedHashMap<>();
         final DatabaseAccessor databaseAccessor = getDatabaseAccessor();
-        try ( ClosableIterator<String> tableIterator = databaseAccessor.iterator( TABLE ) )
+        try ( ClosableIterator<Map.Entry<String, String>> tableIterator = databaseAccessor.iterator( TABLE ) )
         {
             while ( tableIterator.hasNext() )
             {
-                final String dbKey = tableIterator.next();
+                final String dbKey = tableIterator.next().getKey();
                 if ( dbKey.startsWith( KEY_PREFIX_NODE ) )
                 {
                     final String rawValueInDb = databaseAccessor.get( TABLE, dbKey );

+ 3 - 2
server/src/main/java/password/pwm/svc/report/UserCacheService.java

@@ -40,6 +40,7 @@ import password.pwm.util.secure.SecureService;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 public class UserCacheService implements PwmService
 {
@@ -114,7 +115,7 @@ public class UserCacheService implements PwmService
     public class UserStatusCacheBeanIterator<K extends StorageKey> implements ClosableIterator
     {
 
-        private LocalDB.LocalDBIterator<String> innerIterator;
+        private LocalDB.LocalDBIterator<Map.Entry<String, String>> innerIterator;
 
         private UserStatusCacheBeanIterator( ) throws LocalDBException
         {
@@ -128,7 +129,7 @@ public class UserCacheService implements PwmService
 
         public StorageKey next( )
         {
-            final String nextKey = innerIterator.next();
+            final String nextKey = innerIterator.next().getKey();
             return new StorageKey( nextKey );
         }
 

+ 3 - 2
server/src/main/java/password/pwm/svc/token/DataStoreTokenMachine.java

@@ -33,6 +33,7 @@ import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
 import java.time.Instant;
+import java.util.Map;
 import java.util.Optional;
 
 public class DataStoreTokenMachine implements TokenMachine
@@ -85,11 +86,11 @@ public class DataStoreTokenMachine implements TokenMachine
             final long finalSize = size();
             LOGGER.trace( () -> "beginning purge cycle; database size = " + finalSize );
         }
-        try ( ClosableIterator<String> keyIterator = dataStore.iterator() )
+        try ( ClosableIterator<Map.Entry<String, String>> keyIterator = dataStore.iterator() )
         {
             while ( tokenService.status() == PwmService.STATUS.OPEN && keyIterator.hasNext() )
             {
-                final String storedHash = keyIterator.next();
+                final String storedHash = keyIterator.next().getKey();
                 final TokenKey loopKey = keyFromStoredHash( storedHash );
 
                 // retrieving token tests validity and causes purging

+ 5 - 3
server/src/main/java/password/pwm/svc/wordlist/SharedHistoryManager.java

@@ -43,6 +43,7 @@ import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.TimerTask;
 import java.util.concurrent.ExecutorService;
 
@@ -369,14 +370,15 @@ public class SharedHistoryManager implements PwmService
             LOGGER.debug( () -> "beginning wordDB reduce operation, examining " + initialSize
                     + " words for entries older than " + TimeDuration.asCompactString( settings.maxAgeMs ) );
 
-            LocalDB.LocalDBIterator<String> keyIterator = null;
+            LocalDB.LocalDBIterator<Map.Entry<String, String>> keyIterator = null;
             try
             {
                 keyIterator = localDB.iterator( WORDS_DB );
                 while ( status == STATUS.OPEN && keyIterator.hasNext() )
                 {
-                    final String key = keyIterator.next();
-                    final String value = localDB.get( WORDS_DB, key );
+                    final Map.Entry<String, String> entry = keyIterator.next();
+                    final String key = entry.getKey();
+                    final String value = entry.getValue();
                     final long timeStamp = Long.parseLong( value );
                     final long entryAge = System.currentTimeMillis() - timeStamp;
 

+ 3 - 1
server/src/main/java/password/pwm/util/DataStore.java

@@ -24,6 +24,8 @@ import password.pwm.error.PwmDataStoreException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.ClosableIterator;
 
+import java.util.Map;
+
 public interface DataStore
 {
     enum Status
@@ -40,7 +42,7 @@ public interface DataStore
     String get( String key )
             throws PwmDataStoreException, PwmUnrecoverableException;
 
-    ClosableIterator<String> iterator( )
+    ClosableIterator<Map.Entry<String, String>> iterator( )
             throws PwmDataStoreException, PwmUnrecoverableException;
 
     Status status( );

+ 2 - 1
server/src/main/java/password/pwm/util/db/DatabaseAccessor.java

@@ -24,6 +24,7 @@ import password.pwm.util.java.ClosableIterator;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Map;
 
 public interface DatabaseAccessor
 {
@@ -76,7 +77,7 @@ public interface DatabaseAccessor
     )
             throws DatabaseException;
 
-    ClosableIterator<String> iterator( DatabaseTable table )
+    ClosableIterator<Map.Entry<String, String>> iterator( DatabaseTable table )
             throws DatabaseException;
 
     @DbOperation

+ 10 - 7
server/src/main/java/password/pwm/util/db/DatabaseAccessorImpl.java

@@ -30,6 +30,7 @@ import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.AbstractMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -231,7 +232,7 @@ class DatabaseAccessorImpl implements DatabaseAccessor
     }
 
     @Override
-    public ClosableIterator<String> iterator( final DatabaseTable table )
+    public ClosableIterator<Map.Entry<String, String>> iterator( final DatabaseTable table )
             throws DatabaseException
     {
         try
@@ -331,12 +332,12 @@ class DatabaseAccessorImpl implements DatabaseAccessor
     }
 
 
-    public class DBIterator implements ClosableIterator<String>
+    public class DBIterator implements ClosableIterator<Map.Entry<String, String>>
     {
         private final DatabaseTable table;
         private ResultSet resultSet;
         private PreparedStatement statement;
-        private String nextValue;
+        private Map.Entry<String, String> nextValue;
         private boolean finished;
         private int counter = ITERATOR_COUNTER.getAndIncrement();
 
@@ -354,7 +355,7 @@ class DatabaseAccessorImpl implements DatabaseAccessor
                     "iterator #" + counter + " open", table, null, null );
             traceBegin( debugInfo );
 
-            final String sqlText = "SELECT " + DatabaseService.KEY_COLUMN + " FROM " + table.name();
+            final String sqlText = "SELECT * FROM " + table.name();
             try
             {
                 outstandingIterators.add( this );
@@ -375,13 +376,13 @@ class DatabaseAccessorImpl implements DatabaseAccessor
             return !finished;
         }
 
-        public String next( )
+        public Map.Entry<String, String> next( )
         {
             if ( finished )
             {
                 throw new IllegalStateException( "iterator completed" );
             }
-            final String returnValue = nextValue;
+            final Map.Entry<String, String> returnValue = nextValue;
             getNextItem();
             return returnValue;
         }
@@ -397,7 +398,9 @@ class DatabaseAccessorImpl implements DatabaseAccessor
             {
                 if ( resultSet.next() )
                 {
-                    nextValue = resultSet.getString( DatabaseService.KEY_COLUMN );
+                    final String key = resultSet.getString( DatabaseService.KEY_COLUMN );
+                    final String value = resultSet.getString( DatabaseService.VALUE_COLUMN );
+                    nextValue = new AbstractMap.SimpleEntry<>( key, value );
                 }
                 else
                 {

+ 3 - 1
server/src/main/java/password/pwm/util/db/DatabaseDataStore.java

@@ -25,6 +25,8 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.DataStore;
 import password.pwm.util.java.ClosableIterator;
 
+import java.util.Map;
+
 public class DatabaseDataStore implements DataStore
 {
     private final DatabaseService databaseService;
@@ -50,7 +52,7 @@ public class DatabaseDataStore implements DataStore
         return databaseService.getAccessor().get( table, key );
     }
 
-    public ClosableIterator<String> iterator( ) throws PwmDataStoreException, PwmUnrecoverableException
+    public ClosableIterator<Map.Entry<String, String>> iterator( ) throws PwmDataStoreException, PwmUnrecoverableException
     {
         return databaseService.getAccessor().iterator( table );
     }

+ 13 - 25
server/src/main/java/password/pwm/util/localdb/AbstractJDBCLocalDB.java

@@ -36,6 +36,7 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.time.Instant;
+import java.util.AbstractMap;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
@@ -59,8 +60,8 @@ public abstract class AbstractJDBCLocalDB implements LocalDBProvider
     protected File dbDirectory;
 
     // cache of dbIterators
-    private final Set<LocalDB.LocalDBIterator<String>> dbIterators = Collections.newSetFromMap(
-            new ConcurrentHashMap<LocalDB.LocalDBIterator<String>, Boolean>() );
+    private final Set<LocalDB.LocalDBIterator<Map.Entry<String, String>>> dbIterators = Collections.newSetFromMap(
+            new ConcurrentHashMap<>() );
 
     // sql db connection
     protected Connection dbConnection;
@@ -289,7 +290,7 @@ public abstract class AbstractJDBCLocalDB implements LocalDBProvider
         this.status = LocalDB.Status.OPEN;
     }
 
-    public LocalDB.LocalDBIterator<String> iterator( final LocalDB.DB db )
+    public LocalDB.LocalDBIterator<Map.Entry<String, String>> iterator( final LocalDB.DB db )
             throws LocalDBException
     {
         try
@@ -521,7 +522,7 @@ public abstract class AbstractJDBCLocalDB implements LocalDBProvider
             lock.writeLock().lock();
             try
             {
-                final Set<LocalDB.LocalDBIterator<String>> copiedIterators = new HashSet<>();
+                final Set<LocalDB.LocalDBIterator<Map.Entry<String, String>>> copiedIterators = new HashSet<>();
                 copiedIterators.addAll( dbIterators );
 
                 for ( final LocalDB.LocalDBIterator dbIterator : copiedIterators )
@@ -595,10 +596,9 @@ public abstract class AbstractJDBCLocalDB implements LocalDBProvider
     ) throws LocalDBException;
 
 
-    private class DbIterator implements Closeable, LocalDB.LocalDBIterator<String>
+    private class DbIterator implements Closeable, LocalDB.LocalDBIterator<Map.Entry<String, String>>
     {
-        private String nextItem;
-        private String currentItem;
+        private Map.Entry<String, String> nextItem;
 
         private ResultSet resultSet;
         private final LocalDB.DB db;
@@ -630,7 +630,10 @@ public abstract class AbstractJDBCLocalDB implements LocalDBProvider
             {
                 if ( resultSet.next() )
                 {
-                    nextItem = resultSet.getString( KEY_COLUMN );
+                    final String key = resultSet.getString( KEY_COLUMN );
+                    final String value = resultSet.getString( VALUE_COLUMN );
+
+                    nextItem = new AbstractMap.SimpleImmutableEntry<>( key, value );
                 }
                 else
                 {
@@ -660,27 +663,12 @@ public abstract class AbstractJDBCLocalDB implements LocalDBProvider
             dbIterators.remove( this );
         }
 
-        public String next( )
+        public Map.Entry<String, String> next( )
         {
-            currentItem = nextItem;
+            final Map.Entry<String, String> currentItem = nextItem;
             fetchNext();
             return currentItem;
         }
-
-        public void remove( )
-        {
-            if ( currentItem != null )
-            {
-                try
-                {
-                    AbstractJDBCLocalDB.this.remove( db, currentItem );
-                }
-                catch ( final LocalDBException e )
-                {
-                    throw new RuntimeException( e );
-                }
-            }
-        }
     }
 
     public File getFileLocation( )

+ 2 - 2
server/src/main/java/password/pwm/util/localdb/LocalDB.java

@@ -62,7 +62,7 @@ public interface LocalDB
     String get( DB db, String key )
             throws LocalDBException;
 
-    LocalDBIterator<String> iterator( DB db )
+    LocalDBIterator<Map.Entry<String, String>> iterator( DB db )
             throws LocalDBException;
 
     @WriteOperation
@@ -172,7 +172,7 @@ public interface LocalDB
     }
 
 
-    interface LocalDBIterator<K> extends ClosableIterator<String>
+    interface LocalDBIterator<K> extends ClosableIterator<Map.Entry<String, String>>
     {
     }
 }

+ 1 - 1
server/src/main/java/password/pwm/util/localdb/LocalDBAdaptor.java

@@ -78,7 +78,7 @@ public class LocalDBAdaptor implements LocalDB
         innerDB.init( dbDirectory, initParameters, parameters );
     }
 
-    public LocalDBIterator<String> iterator( final DB db ) throws LocalDBException
+    public LocalDBIterator<Map.Entry<String, String>> iterator( final DB db ) throws LocalDBException
     {
         ParameterValidator.validateDBValue( db );
         return innerDB.iterator( db );

+ 1 - 1
server/src/main/java/password/pwm/util/localdb/LocalDBDataStore.java

@@ -52,7 +52,7 @@ public class LocalDBDataStore implements DataStore
         return localDB.get( db, key );
     }
 
-    public ClosableIterator<String> iterator( ) throws PwmDataStoreException
+    public ClosableIterator<Map.Entry<String, String>> iterator( ) throws PwmDataStoreException
     {
         return localDB.iterator( db );
     }

+ 1 - 1
server/src/main/java/password/pwm/util/localdb/LocalDBProvider.java

@@ -56,7 +56,7 @@ public interface LocalDBProvider
     void init( File dbDirectory, Map<String, String> initParameters, Map<Parameter, String> parameters )
             throws LocalDBException;
 
-    LocalDB.LocalDBIterator<String> iterator( LocalDB.DB db )
+    LocalDB.LocalDBIterator<Map.Entry<String, String>> iterator( LocalDB.DB db )
             throws LocalDBException;
 
     @LocalDB.WriteOperation

+ 5 - 4
server/src/main/java/password/pwm/util/localdb/LocalDBStoredQueue.java

@@ -1037,13 +1037,14 @@ LocalDBStoredQueue implements Queue<String>, Deque<String>
                     sb.append( "  tailPosition=" ).append( tailPosition ).append( ", headPosition=" ).append( headPosition ).append( ", db=" ).append( db );
                     sb.append( ", size=" ).append( internalSize() ).append( "\n" );
 
-                    try ( LocalDB.LocalDBIterator<String> keyIter = localDB.iterator( db ) )
+                    try ( LocalDB.LocalDBIterator<Map.Entry<String, String>> localDBIterator = localDB.iterator( db ) )
                     {
                         int rowCount = 0;
-                        while ( keyIter.hasNext() && rowCount < DEBUG_MAX_ROWS )
+                        while ( localDBIterator.hasNext() && rowCount < DEBUG_MAX_ROWS )
                         {
-                            final String key = keyIter.next();
-                            String value = localDB.get( db, key );
+                            final Map.Entry<String, String> entry = localDBIterator.next();
+                            final String key = entry.getKey();
+                            String value = entry.getValue();
                             value = value == null ? "" : value;
                             value = value.length() < DEBUG_MAX_WIDTH ? value : value.substring( 0, DEBUG_MAX_WIDTH ) + "...";
                             final String row = key + " " + value;

+ 37 - 25
server/src/main/java/password/pwm/util/localdb/LocalDBUtility.java

@@ -30,6 +30,7 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.util.EventRateMeter;
 import password.pwm.util.ProgressInfo;
 import password.pwm.util.TransactionSizeCalculator;
+import password.pwm.util.java.AverageTracker;
 import password.pwm.util.java.ConditionalTaskExecutor;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.Percent;
@@ -48,12 +49,12 @@ import java.io.PrintStream;
 import java.io.Reader;
 import java.math.RoundingMode;
 import java.time.Instant;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Objects;
 import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.zip.GZIPInputStream;
 import java.util.zip.GZIPOutputStream;
 import java.util.zip.ZipEntry;
@@ -66,7 +67,6 @@ public class LocalDBUtility
     private static final String IN_PROGRESS_STATUS_VALUE = "in-progress";
 
     private final LocalDB localDB;
-    private int exportLineCounter;
 
     private static final int GZIP_BUFFER_SIZE = 1024 * 1024;
 
@@ -96,7 +96,7 @@ public class LocalDBUtility
             throws PwmOperationalException
     {
         Objects.requireNonNull( outputStream );
-        exportLineCounter = 0;
+        final AtomicLong exportLineCounter = new AtomicLong( 0 );
 
         final long totalLines = countBackupableRecords( debugOutput );
 
@@ -106,7 +106,7 @@ public class LocalDBUtility
 
         final EventRateMeter eventRateMeter = new EventRateMeter( TimeDuration.MINUTE );
         final ConditionalTaskExecutor debugOutputter = ConditionalTaskExecutor.forPeriodicTask( () ->
-                        outputExportDebugStats( totalLines, eventRateMeter, startTime, debugOutput ),
+                        outputExportDebugStats( totalLines, exportLineCounter.get(), eventRateMeter, startTime, debugOutput ),
                 TimeDuration.MINUTE );
 
         try ( CSVPrinter csvPrinter = JavaHelper.makeCsvPrinter( new GZIPOutputStream( outputStream, GZIP_BUFFER_SIZE ) ) )
@@ -117,14 +117,15 @@ public class LocalDBUtility
                 if ( loopDB.isBackup() )
                 {
                     csvPrinter.printComment( "Export of " + loopDB.toString() );
-                    try ( LocalDB.LocalDBIterator<String> localDBIterator = localDB.iterator( loopDB ) )
+                    try ( LocalDB.LocalDBIterator<Map.Entry<String, String>> localDBIterator = localDB.iterator( loopDB ) )
                     {
                         while ( localDBIterator.hasNext() )
                         {
-                            final String key = localDBIterator.next();
-                            final String value = localDB.get( loopDB, key );
+                            final Map.Entry<String, String> entry = localDBIterator.next();
+                            final String key = entry.getKey();
+                            final String value = entry.getValue();
                             csvPrinter.printRecord( loopDB.toString(), key, value );
-                            exportLineCounter++;
+                            exportLineCounter.incrementAndGet();
                             eventRateMeter.markEvents( 1 );
                             debugOutputter.conditionallyExecuteTask();
                         }
@@ -132,7 +133,7 @@ public class LocalDBUtility
                     csvPrinter.flush();
                 }
             }
-            csvPrinter.printComment( "export completed at " + JavaHelper.toIsoDate( new Date() ) );
+            csvPrinter.printComment( "export completed at " + JavaHelper.toIsoDate( Instant.now() ) );
         }
         catch ( final IOException e )
         {
@@ -149,7 +150,7 @@ public class LocalDBUtility
 
         final long totalLines = localDB.size( LocalDB.DB.WORDLIST_WORDS );
 
-        exportLineCounter = 0;
+        final AtomicLong exportLineCounter = new AtomicLong( 0 );
 
         writeStringToOut( debugOutput, "Wordlist ZIP export beginning of "
                 + StringUtil.formatDiskSize( totalLines ) + " records" );
@@ -157,20 +158,21 @@ public class LocalDBUtility
 
         final EventRateMeter eventRateMeter = new EventRateMeter( TimeDuration.MINUTE );
         final ConditionalTaskExecutor debugOutputter = ConditionalTaskExecutor.forPeriodicTask( () ->
-                        outputExportDebugStats( totalLines, eventRateMeter, startTime, debugOutput ),
+                        outputExportDebugStats( totalLines, exportLineCounter.get(), eventRateMeter, startTime, debugOutput ),
                 TimeDuration.MINUTE );
 
         try ( ZipOutputStream zipOutputStream = new ZipOutputStream( outputStream, PwmConstants.DEFAULT_CHARSET ) )
         {
             zipOutputStream.putNextEntry( new ZipEntry( "wordlist.txt" ) );
-            try ( LocalDB.LocalDBIterator<String> localDBIterator = localDB.iterator( LocalDB.DB.WORDLIST_WORDS ) )
+            try ( LocalDB.LocalDBIterator<Map.Entry<String, String>> localDBIterator = localDB.iterator( LocalDB.DB.WORDLIST_WORDS ) )
             {
                 while ( localDBIterator.hasNext() )
                 {
-                    final String key = localDBIterator.next();
+                    final Map.Entry<String, String> entry = localDBIterator.next();
+                    final String key = entry.getKey();
                     zipOutputStream.write( key.getBytes( PwmConstants.DEFAULT_CHARSET ) );
                     zipOutputStream.write( '\n' );
-                    exportLineCounter++;
+                    exportLineCounter.incrementAndGet();
                     eventRateMeter.markEvents( 1 );
                     debugOutputter.conditionallyExecuteTask();
                 }
@@ -186,6 +188,7 @@ public class LocalDBUtility
 
     private void outputExportDebugStats(
             final long totalLines,
+            final long exportLineCounter,
             final EventRateMeter eventRateMeter,
             final Instant startTime,
             final Appendable debugOutput
@@ -196,8 +199,8 @@ public class LocalDBUtility
         final long secondsRemaining = totalLines / eventRateMeter.readEventRate().longValue();
 
         final String msg = "export stats: recordsOut=" + PwmNumberFormat.forDefaultLocale().format( exportLineCounter )
-                + ", duration=" + percentStr
-                + ", percentComplete=" + TimeDuration.fromCurrent( startTime ).asCompactString()
+                + ", duration=" + TimeDuration.fromCurrent( startTime ).asCompactString()
+                + ", percentComplete=" + percentStr
                 + ", recordsPerSecond=" + PwmNumberFormat.forDefaultLocale().format( eventRateMeter.readEventRate().longValue() )
                 + ", remainingTime=" + TimeDuration.of( secondsRemaining, TimeDuration.Unit.SECONDS ).asCompactString();
         writeStringToOut( debugOutput, msg );
@@ -264,17 +267,22 @@ public class LocalDBUtility
 
     private static class ImportLocalDBMachine
     {
+        private static final long MAX_CHAR_PER_TRANSACTIONS = 50_000_000;
+
         private int lineReaderCounter;
         private long byteReaderCounter;
         private int recordImportCounter;
+        private long transactionCharCounter;
+
         private final Instant startTime = Instant.now();
         final Map<LocalDB.DB, Map<String, String>> transactionMap = new HashMap<>();
         private final EventRateMeter eventRateMeter = new EventRateMeter( TimeDuration.MINUTE );
+        private final AverageTracker charsPerTransactionAverageTracker = new AverageTracker( 50 );
         private final TransactionSizeCalculator transactionCalculator = new TransactionSizeCalculator(
                 TransactionSizeCalculator.Settings.builder()
-                        .durationGoal( TimeDuration.of( 100, TimeDuration.Unit.MILLISECONDS ) )
-                        .minTransactions( 50 )
-                        .maxTransactions( 5 * 1000 )
+                        .durationGoal( TimeDuration.of( 1000, TimeDuration.Unit.MILLISECONDS ) )
+                        .minTransactions( 5 )
+                        .maxTransactions( 5_000_000 )
                         .build()
         );
 
@@ -333,9 +341,10 @@ public class LocalDBUtility
                         }
                         else
                         {
+                            transactionCharCounter += key.length() + value.length();
                             transactionMap.get( db ).put( key, value );
                             cachedTransactions++;
-                            if ( cachedTransactions >= transactionCalculator.getTransactionSize() )
+                            if ( cachedTransactions >= transactionCalculator.getTransactionSize() || transactionCharCounter > MAX_CHAR_PER_TRANSACTIONS )
                             {
                                 flushCachedTransactions();
                                 cachedTransactions = 0;
@@ -365,6 +374,8 @@ public class LocalDBUtility
                 transactionMap.get( loopDB ).clear();
             }
             transactionCalculator.recordLastTransactionDuration( TimeDuration.fromCurrent( startTxnTime ) );
+            charsPerTransactionAverageTracker.addSample( transactionCharCounter );
+            transactionCharCounter = 0;
         }
 
         private void prepareForImport( )
@@ -404,8 +415,9 @@ public class LocalDBUtility
             stats.put( "linesRead", Integer.toString( lineReaderCounter ) );
             stats.put( "bytesRead", Long.toString( byteReaderCounter ) );
             stats.put( "recordsImported", Integer.toString( recordImportCounter ) );
-            stats.put( "avgTransactionSize", Integer.toString( transactionCalculator.getTransactionSize() ) );
-            stats.put( "recordsPerMinute", eventRateMeter.readEventRate().setScale( 2, RoundingMode.DOWN ).toString() );
+            stats.put( "rowsPerTransaction", Integer.toString( transactionCalculator.getTransactionSize() ) );
+            stats.put( "charsPerTransaction", charsPerTransactionAverageTracker.avg().toPlainString() );
+            stats.put( "rowsPerMinute", eventRateMeter.readEventRate().setScale( 2, RoundingMode.DOWN ).toString() );
             stats.put( "duration", TimeDuration.compactFromCurrent( startTime ) );
             return StringUtil.mapToString( stats );
         }
@@ -420,14 +432,14 @@ public class LocalDBUtility
         long storedChars = 0;
         final long totalChars = 0;
 
-        LocalDB.LocalDBIterator<String> iter = null;
+        LocalDB.LocalDBIterator<Map.Entry<String, String>> iter = null;
         try
         {
             iter = localDB.iterator( db );
             while ( iter.hasNext() )
             {
-                final String key = iter.next();
-                final String rawValue = localDB.get( db, key );
+                final Map.Entry<String, String> entry = iter.next();
+                final String rawValue = entry.getValue();
                 if ( rawValue != null )
                 {
                     totalValues++;

+ 17 - 17
server/src/main/java/password/pwm/util/localdb/MemoryLocalDB.java

@@ -39,7 +39,7 @@ public class MemoryLocalDB implements LocalDBProvider
 
     private Map<LocalDB.DB, Map<String, String>> maps = new ConcurrentHashMap<>();
 
-    private void opertationPreCheck( ) throws LocalDBException
+    private void operationPreCheck( ) throws LocalDBException
     {
         if ( state != LocalDB.Status.OPEN )
         {
@@ -70,7 +70,7 @@ public class MemoryLocalDB implements LocalDBProvider
     public boolean contains( final LocalDB.DB db, final String key )
             throws LocalDBException
     {
-        opertationPreCheck();
+        operationPreCheck();
         final Map<String, String> map = maps.get( db );
         return map.containsKey( key );
     }
@@ -78,7 +78,7 @@ public class MemoryLocalDB implements LocalDBProvider
     public String get( final LocalDB.DB db, final String key )
             throws LocalDBException
     {
-        opertationPreCheck();
+        operationPreCheck();
         final Map<String, String> map = maps.get( db );
         return map.get( key );
     }
@@ -107,16 +107,16 @@ public class MemoryLocalDB implements LocalDBProvider
         state = LocalDB.Status.OPEN;
     }
 
-    public LocalDB.LocalDBIterator<String> iterator( final LocalDB.DB db ) throws LocalDBException
+    public LocalDB.LocalDBIterator<Map.Entry<String, String>> iterator( final LocalDB.DB db ) throws LocalDBException
     {
-        return new DbIterator( db );
+        return new MapIterator( db );
     }
 
     @LocalDB.WriteOperation
     public void putAll( final LocalDB.DB db, final Map<String, String> keyValueMap )
             throws LocalDBException
     {
-        opertationPreCheck();
+        operationPreCheck();
 
         if ( keyValueMap != null )
         {
@@ -129,7 +129,7 @@ public class MemoryLocalDB implements LocalDBProvider
     public boolean put( final LocalDB.DB db, final String key, final String value )
             throws LocalDBException
     {
-        opertationPreCheck();
+        operationPreCheck();
 
         final Map<String, String> map = maps.get( db );
         return null != map.put( key, value );
@@ -139,7 +139,7 @@ public class MemoryLocalDB implements LocalDBProvider
     public boolean putIfAbsent( final LocalDB.DB db, final String key, final String value )
             throws LocalDBException
     {
-        opertationPreCheck();
+        operationPreCheck();
 
         final Map<String, String> map = maps.get( db );
         final String oldValue = map.putIfAbsent( key, value );
@@ -150,7 +150,7 @@ public class MemoryLocalDB implements LocalDBProvider
     public boolean remove( final LocalDB.DB db, final String key )
             throws LocalDBException
     {
-        opertationPreCheck();
+        operationPreCheck();
 
         final Map<String, String> map = maps.get( db );
         return null != map.remove( key );
@@ -159,7 +159,7 @@ public class MemoryLocalDB implements LocalDBProvider
     public long size( final LocalDB.DB db )
             throws LocalDBException
     {
-        opertationPreCheck();
+        operationPreCheck();
 
         final Map<String, String> map = maps.get( db );
         return map.size();
@@ -169,7 +169,7 @@ public class MemoryLocalDB implements LocalDBProvider
     public void truncate( final LocalDB.DB db )
             throws LocalDBException
     {
-        opertationPreCheck();
+        operationPreCheck();
 
         final Map<String, String> map = maps.get( db );
         map.clear();
@@ -177,7 +177,7 @@ public class MemoryLocalDB implements LocalDBProvider
 
     public void removeAll( final LocalDB.DB db, final Collection<String> keys ) throws LocalDBException
     {
-        opertationPreCheck();
+        operationPreCheck();
 
         maps.get( db ).keySet().removeAll( keys );
     }
@@ -194,13 +194,13 @@ public class MemoryLocalDB implements LocalDBProvider
     }
 
 
-    private class DbIterator implements LocalDB.LocalDBIterator<String>
+    private class MapIterator implements LocalDB.LocalDBIterator<Map.Entry<String, String>>
     {
-        private final Iterator<String> iterator;
+        private final Iterator<Map.Entry<String, String>> iterator;
 
-        private DbIterator( final LocalDB.DB db )
+        private MapIterator( final LocalDB.DB db )
         {
-            iterator = maps.get( db ).keySet().iterator();
+            iterator = maps.get( db ).entrySet().iterator();
         }
 
         public boolean hasNext( )
@@ -208,7 +208,7 @@ public class MemoryLocalDB implements LocalDBProvider
             return iterator.hasNext();
         }
 
-        public String next( )
+        public Map.Entry<String, String> next( )
         {
             return iterator.next();
         }

+ 15 - 11
server/src/main/java/password/pwm/util/localdb/XodusLocalDB.java

@@ -50,6 +50,7 @@ import java.io.Serializable;
 import java.nio.file.Files;
 import java.nio.file.StandardOpenOption;
 import java.time.Instant;
+import java.util.AbstractMap;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -265,18 +266,18 @@ public class XodusLocalDB implements LocalDBProvider
     }
 
     @Override
-    public LocalDB.LocalDBIterator<String> iterator( final LocalDB.DB db )  throws LocalDBException
+    public LocalDB.LocalDBIterator<Map.Entry<String, String>> iterator( final LocalDB.DB db )  throws LocalDBException
     {
         return new InnerIterator( db );
     }
 
-    private class InnerIterator implements LocalDB.LocalDBIterator<String>
+    public class InnerIterator implements LocalDB.LocalDBIterator<Map.Entry<String, String>>
     {
         private final Transaction transaction;
         private final Cursor cursor;
 
         private boolean closed;
-        private String nextValue = "";
+        private Map.Entry<String, String> nextValue = null;
 
         InnerIterator( final LocalDB.DB db )
         {
@@ -307,19 +308,22 @@ public class XodusLocalDB implements LocalDBProvider
                     close();
                     return;
                 }
-                final ByteIterable nextKey = cursor.getKey();
-                if ( nextKey == null || nextKey.getLength() == 0 )
+                final ByteIterable nextCursor = cursor.getKey();
+                if ( nextCursor == null || nextCursor.getLength() == 0 )
                 {
                     close();
                     return;
                 }
-                final String decodedValue = bindMachine.entryToKey( nextKey );
-                if ( decodedValue == null )
+                final String decodedKey = bindMachine.entryToKey( nextCursor );
+                if ( decodedKey == null )
                 {
                     close();
                     return;
                 }
-                nextValue = decodedValue;
+                final ByteIterable nextValueIterable = cursor.getValue();
+                final String nextStringValue = nextValueIterable == null ? null : bindMachine.entryToValue( nextValueIterable );
+
+                nextValue = new AbstractMap.SimpleImmutableEntry<>( decodedKey, nextStringValue );
             }
             catch ( final Exception e )
             {
@@ -348,17 +352,17 @@ public class XodusLocalDB implements LocalDBProvider
         }
 
         @Override
-        public String next( )
+        public Map.Entry<String, String> next( )
         {
             if ( closed )
             {
                 return null;
             }
-            final String value = nextValue;
+            final Map.Entry<String, String> value = nextValue;
             doNext();
             return value;
         }
-
+        
         @Override
         public void remove( )
         {