Speed up updating of premium lists (#1482)

* Speed up updating of premium lists

There are two parts to this:
1. Don't load the premium entries in the command prompt (this isn't
necessary and we didn't display that information anyway).
2. Set a proper batch size (rather than just 1) when saving all the
premium entries. This means that we generate only one INSERT statement
rather than N statements.
This commit is contained in:
gbrodman
2022-01-10 16:33:35 -05:00
committed by GitHub
parent 5822f53e14
commit ff8f86090d
12 changed files with 105 additions and 93 deletions
@@ -33,6 +33,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import dagger.Module;
import dagger.Provides;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.util.TaskQueueUtils;
import google.registry.util.YamlUtils;
import java.lang.annotation.Documented;
@@ -1531,6 +1532,31 @@ public final class RegistryConfig {
return CONFIG_SETTINGS.get().hibernate.hikariIdleTimeout;
}
/**
* JDBC-specific: driver default batch size is 0, which means that every INSERT statement will be
* sent to the database individually. Batching allows us to group together multiple inserts into
* one single INSERT statement which can dramatically increase speed in situations with many
* inserts.
*
* <p>Hibernate docs, i.e.
* https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html,
* recommend between 10 and 50.
*/
public static String getHibernateJdbcBatchSize() {
return CONFIG_SETTINGS.get().hibernate.jdbcBatchSize;
}
/**
* Returns the JDBC fetch size.
*
* <p>Postgresql-specific: driver default fetch size is 0, which disables streaming result sets.
* Here we set a small default geared toward Nomulus server transactions. Large queries can
* override the defaults using {@link JpaTransactionManager#setQueryFetchSize}.
*/
public static String getHibernateJdbcFetchSize() {
return CONFIG_SETTINGS.get().hibernate.jdbcFetchSize;
}
/** Returns the roid suffix to be used for the roids of all contacts and hosts. */
public static String getContactAndHostRoidSuffix() {
return CONFIG_SETTINGS.get().registryPolicy.contactAndHostRoidSuffix;
@@ -120,6 +120,8 @@ public class RegistryConfigSettings {
public String hikariMinimumIdle;
public String hikariMaximumPoolSize;
public String hikariIdleTimeout;
public String jdbcBatchSize;
public String jdbcFetchSize;
}
/** Configuration for Cloud SQL. */
@@ -221,6 +221,17 @@ hibernate:
hikariMinimumIdle: 1
hikariMaximumPoolSize: 10
hikariIdleTimeout: 300000
# The batch size is basically the number of insertions / updates in a single
# transaction that will be batched together into one INSERT/UPDATE statement.
# A larger batch size is useful when inserting or updating many entities in a
# single transaction. Hibernate docs
# (https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html)
# recommend between 10 and 50.
jdbcBatchSize: 50
# The fetch size is the number of entities retrieved at a time from the
# database cursor. Here we set a small default geared toward Nomulus server
# transactions. Large queries can override the defaults on a per-query basis.
jdbcFetchSize: 20
cloudSql:
# jdbc url for the Cloud SQL database.
@@ -26,7 +26,6 @@ import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config;
@@ -143,7 +142,7 @@ public class ExportPremiumTermsAction implements Runnable {
PremiumListDao.getLatestRevision(premiumListName).isPresent(),
"Could not load premium list for " + tld);
SortedSet<String> premiumTerms =
Streams.stream(PremiumListDao.loadAllPremiumEntries(premiumListName))
PremiumListDao.loadAllPremiumEntries(premiumListName).stream()
.map(PremiumEntry::toString)
.collect(ImmutableSortedSet.toImmutableSortedSet(String::compareTo));
@@ -21,7 +21,6 @@ import static com.google.common.hash.Funnels.stringFunnel;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import com.google.common.hash.BloomFilter;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
@@ -86,9 +85,8 @@ public final class PremiumList extends BaseDomainLabelList<BigDecimal, PremiumEn
*/
public synchronized ImmutableMap<String, BigDecimal> getLabelsToPrices() {
if (labelsToPrices == null) {
Iterable<PremiumEntry> entries = PremiumListDao.loadAllPremiumEntries(name);
labelsToPrices =
Streams.stream(entries)
PremiumListDao.loadAllPremiumEntries(name).stream()
.collect(
toImmutableMap(
PremiumEntry::getDomainLabel,
@@ -28,8 +28,8 @@ import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import google.registry.model.tld.label.PremiumList.PremiumEntry;
import google.registry.util.NonFinalForTesting;
import java.math.BigDecimal;
@@ -56,8 +56,7 @@ public class PremiumListDao {
* <p>This is cached for a shorter duration because we need to periodically reload this entity to
* check if a new revision has been published, and if so, then use that.
*
* <p>We also cache the absence of premium lists with a given name to avoid unnecessary pointless
* lookups. Note that this cache is only applicable to PremiumList objects stored in SQL.
* <p>We also cache the absence of premium lists with a given name to avoid pointless lookups.
*/
@NonFinalForTesting
static LoadingCache<String, Optional<PremiumList>> premiumListCache =
@@ -170,11 +169,10 @@ public class PremiumListDao {
if (!isNullOrEmpty(premiumList.getLabelsToPrices())) {
ImmutableSet.Builder<PremiumEntry> entries = new ImmutableSet.Builder<>();
premiumList.getLabelsToPrices().entrySet().stream()
premiumList
.getLabelsToPrices()
.forEach(
entry ->
entries.add(
PremiumEntry.create(revisionId, entry.getValue(), entry.getKey())));
(key, value) -> entries.add(PremiumEntry.create(revisionId, value, key)));
jpaTm().insertAll(entries.build());
}
});
@@ -217,7 +215,7 @@ public class PremiumListDao {
*
* <p>This is an expensive operation and should only be used when the entire list is required.
*/
public static Iterable<PremiumEntry> loadPremiumEntries(PremiumList premiumList) {
public static List<PremiumEntry> loadPremiumEntries(PremiumList premiumList) {
return jpaTm()
.transact(
() ->
@@ -254,15 +252,14 @@ public class PremiumListDao {
*
* <p>This is an expensive operation and should only be used when the entire list is required.
*/
public static Iterable<PremiumEntry> loadAllPremiumEntries(String premiumListName) {
public static ImmutableList<PremiumEntry> loadAllPremiumEntries(String premiumListName) {
PremiumList premiumList =
getLatestRevision(premiumListName)
.orElseThrow(
() ->
new IllegalArgumentException(
String.format("No premium list with name %s.", premiumListName)));
Iterable<PremiumEntry> entries = loadPremiumEntries(premiumList);
return Streams.stream(entries)
return loadPremiumEntries(premiumList).stream()
.map(
premiumEntry ->
new PremiumEntry.Builder()
@@ -20,6 +20,8 @@ import static google.registry.config.RegistryConfig.getHibernateHikariConnection
import static google.registry.config.RegistryConfig.getHibernateHikariIdleTimeout;
import static google.registry.config.RegistryConfig.getHibernateHikariMaximumPoolSize;
import static google.registry.config.RegistryConfig.getHibernateHikariMinimumIdle;
import static google.registry.config.RegistryConfig.getHibernateJdbcBatchSize;
import static google.registry.config.RegistryConfig.getHibernateJdbcFetchSize;
import static google.registry.config.RegistryConfig.getHibernateLogSqlQueries;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -76,15 +78,9 @@ public abstract class PersistenceModule {
public static final String HIKARI_DS_CLOUD_SQL_INSTANCE =
"hibernate.hikari.dataSource.cloudSqlInstance";
/**
* Postgresql-specific: driver default fetch size is 0, which disables streaming result sets. Here
* we set a small default geared toward Nomulus server transactions. Large queries can override
* the defaults using {@link JpaTransactionManager#setQueryFetchSize}.
*/
public static final String JDBC_BATCH_SIZE = "hibernate.jdbc.batch_size";
public static final String JDBC_FETCH_SIZE = "hibernate.jdbc.fetch_size";
private static final int DEFAULT_SERVER_FETCH_SIZE = 20;
@VisibleForTesting
@Provides
@DefaultHibernateConfigs
@@ -111,7 +107,8 @@ public abstract class PersistenceModule {
properties.put(HIKARI_MAXIMUM_POOL_SIZE, getHibernateHikariMaximumPoolSize());
properties.put(HIKARI_IDLE_TIMEOUT, getHibernateHikariIdleTimeout());
properties.put(Environment.DIALECT, NomulusPostgreSQLDialect.class.getName());
properties.put(JDBC_FETCH_SIZE, Integer.toString(DEFAULT_SERVER_FETCH_SIZE));
properties.put(JDBC_BATCH_SIZE, getHibernateJdbcBatchSize());
properties.put(JDBC_FETCH_SIZE, getHibernateJdbcFetchSize());
return properties.build();
}
@@ -16,7 +16,6 @@ package google.registry.tools;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.Streams;
import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.PremiumList.PremiumEntry;
import google.registry.model.tld.label.PremiumListDao;
@@ -40,7 +39,7 @@ public class GetPremiumListCommand implements CommandWithRemoteApi {
System.out.printf(
"%s:\n%s\n",
premiumListName,
Streams.stream(PremiumListDao.loadAllPremiumEntries(premiumListName))
PremiumListDao.loadAllPremiumEntries(premiumListName).stream()
.sorted(Comparator.comparing(PremiumEntry::getDomainLabel))
.map(premiumEntry -> premiumEntry.toString(premiumList.get().getCurrency()))
.collect(Collectors.joining("\n")));
@@ -15,21 +15,15 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.tld.label.PremiumListUtils.parseToPremiumList;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.util.ListNamingUtils.convertFilePathToName;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameters;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.PremiumList.PremiumEntry;
import google.registry.model.tld.label.PremiumListDao;
import google.registry.model.tld.label.PremiumListUtils;
import java.nio.file.Files;
import java.util.List;
import java.util.Optional;
/** Command to safely update {@link PremiumList} in Database for a given TLD. */
@@ -43,46 +37,12 @@ class UpdatePremiumListCommand extends CreateOrUpdatePremiumListCommand {
checkArgument(
list.isPresent(),
String.format("Could not update premium list %s because it doesn't exist.", name));
List<String> existingEntry = getExistingPremiumEntry(list.get()).asList();
inputData = Files.readAllLines(inputFile, UTF_8);
checkArgument(!inputData.isEmpty(), "New premium list data cannot be empty");
currency = list.get().getCurrency();
// reconstructing existing premium list to bypass Hibernate lazy initialization exception
PremiumList existingPremiumList = parseToPremiumList(name, currency, existingEntry);
PremiumList updatedPremiumList = parseToPremiumList(name, currency, inputData);
PremiumList updatedPremiumList = PremiumListUtils.parseToPremiumList(name, currency, inputData);
return String.format(
"Update premium list for %s?\n Old List: %s\n New List: %s",
name, existingPremiumList, updatedPremiumList);
}
/*
To get premium list content as a set of string. This is a workaround to avoid dealing with
Hibernate.LazyInitizationException error. It occurs when trying to access data of the
latest revision of an existing premium list.
"Cannot evaluate google.registry.model.tld.label.PremiumList.toString()'".
Ideally, the following should be the way to verify info in latest revision of a premium list:
PremiumList existingPremiumList =
PremiumListSqlDao.getLatestRevision(name)
.orElseThrow(
() ->
new IllegalArgumentException(
String.format(
"Could not update premium list %s because it doesn't exist.", name)));
assertThat(persistedList.getLabelsToPrices()).containsEntry("foo", new BigDecimal("9000.00"));
assertThat(persistedList.size()).isEqualTo(1);
*/
protected ImmutableSet<String> getExistingPremiumEntry(PremiumList list) {
Iterable<PremiumEntry> sqlListEntries =
jpaTm().transact(() -> PremiumListDao.loadPremiumEntries(list));
return Streams.stream(sqlListEntries)
.map(
premiumEntry ->
String.format(
"%s,%s %s",
premiumEntry.getDomainLabel(), list.getCurrency(), premiumEntry.getValue()))
.collect(toImmutableSet());
name, list, updatedPremiumList);
}
}