Streamline large synch blocks in list/TMCH CA loading (#3183)

For reserved/premium lists:
Use double-check locking so that subsequent calls to get the entire map
of entries don't need to even check the locking object. This makes
things quicker and removes lock-tracking overhead.

For TMCH CA:
we can just remove the synchronization block entirely. Everything inside
of it is either constants (e.g. ROOT_CERTS) or a Guava loading cache
(CRL_CACHE) which takes care of synchronization for us anyway.
This commit is contained in:
gbrodman
2026-08-04 20:06:53 +00:00
committed by GitHub
parent fabf0c07b2
commit c9a82f1322
3 changed files with 52 additions and 48 deletions
@@ -59,10 +59,10 @@ public final class PremiumList extends BaseDomainLabelList<BigDecimal, PremiumEn
* Mapping from unqualified domain names to their prices. * Mapping from unqualified domain names to their prices.
* *
* <p>This field requires special treatment since we want to lazy load it. We have to remove it * <p>This field requires special treatment since we want to lazy load it. We have to remove it
* from the immutability contract so we can modify it after construction and we have to handle the * from the immutability contract so we can modify it after construction, and we have to handle
* database processing on our own so we can detach it after load. * the database processing on our own so we can detach it after load.
*/ */
@Insignificant @Transient ImmutableMap<String, BigDecimal> labelsToPrices; @Insignificant @Transient volatile ImmutableMap<String, BigDecimal> labelsToPrices;
@Column(nullable = false) @Column(nullable = false)
BloomFilter<String> bloomFilter; BloomFilter<String> bloomFilter;
@@ -76,18 +76,27 @@ public final class PremiumList extends BaseDomainLabelList<BigDecimal, PremiumEn
* Returns a {@link Map} of domain labels to prices. * Returns a {@link Map} of domain labels to prices.
* *
* <p>Note that this is lazily loaded and thus must be called inside a transaction. You generally * <p>Note that this is lazily loaded and thus must be called inside a transaction. You generally
* should not be using this anyway as it's inefficient to load all of the PremiumEntry rows if you * should not be using this anyway as it's inefficient to load all the PremiumEntry rows if you
* don't need them. To check prices, use {@link PremiumListDao#getPremiumPrice} instead. * don't need them. To check prices, use {@link PremiumListDao#getPremiumPrice} instead.
*
* <p>We use locking to memoize the resulting object. We cannot use a simple memoizing Supplier
* because we need to be able to set this value when creating the lists.
*/ */
public synchronized ImmutableMap<String, BigDecimal> getLabelsToPrices() { public ImmutableMap<String, BigDecimal> getLabelsToPrices() {
if (labelsToPrices == null) { if (labelsToPrices == null) {
labelsToPrices = synchronized (this) {
PremiumListDao.loadAllPremiumEntries(name).stream() // Extra null check to avoid race conditions
.collect( if (labelsToPrices == null) {
toImmutableMap( labelsToPrices =
PremiumEntry::getDomainLabel, PremiumListDao.loadAllPremiumEntries(name).stream()
// Set the correct amount of precision for the premium list's currency. .collect(
premiumEntry -> convertAmountToMoney(premiumEntry.getValue()).getAmount())); toImmutableMap(
PremiumEntry::getDomainLabel,
// Set the correct amount of precision for the list's currency.
premiumEntry ->
convertAmountToMoney(premiumEntry.getValue()).getAmount()));
}
}
} }
return labelsToPrices; return labelsToPrices;
} }
@@ -23,7 +23,6 @@ import static google.registry.config.RegistryConfig.getDomainLabelListCacheDurat
import static google.registry.model.tld.label.ReservationType.FULLY_BLOCKED; import static google.registry.model.tld.label.ReservationType.FULLY_BLOCKED;
import static google.registry.persistence.transaction.QueryComposer.Comparator.EQ; import static google.registry.persistence.transaction.QueryComposer.Comparator.EQ;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.nullToEmpty;
import com.github.benmanes.caffeine.cache.LoadingCache; import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
@@ -69,7 +68,7 @@ public final class ReservedList
* from the immutability contract so we can modify it after construction and we have to handle the * from the immutability contract so we can modify it after construction and we have to handle the
* database processing on our own so we can detach it after load. * database processing on our own so we can detach it after load.
*/ */
@Insignificant @Transient Map<String, ReservedListEntry> reservedListMap; @Insignificant @Transient volatile ImmutableMap<String, ReservedListEntry> reservedListMap;
@RecursivePreRemove @RecursivePreRemove
void preRemove() { void preRemove() {
@@ -149,7 +148,7 @@ public final class ReservedList
} }
/** A builder for constructing {@link ReservedListEntry} objects, since they are immutable. */ /** A builder for constructing {@link ReservedListEntry} objects, since they are immutable. */
private static class Builder public static class Builder
extends DomainLabelEntry.Builder<ReservedListEntry, ReservedListEntry.Builder> { extends DomainLabelEntry.Builder<ReservedListEntry, ReservedListEntry.Builder> {
Builder() {} Builder() {}
@@ -185,19 +184,27 @@ public final class ReservedList
* *
* <p>Note that this involves a database fetch of a potentially large number of elements and * <p>Note that this involves a database fetch of a potentially large number of elements and
* should be avoided unless necessary. * should be avoided unless necessary.
*
* <p>We use locking to memoize the resulting object. We cannot use a simple memoizing Supplier
* because we need to be able to set this value when creating the lists.
*/ */
public synchronized ImmutableMap<String, ReservedListEntry> getReservedListEntries() { public ImmutableMap<String, ReservedListEntry> getReservedListEntries() {
if (reservedListMap == null) { if (reservedListMap == null) {
reservedListMap = synchronized (this) {
tm().reTransact( // Extra null check to avoid race conditions
() -> if (reservedListMap == null) {
tm() reservedListMap =
.createQueryComposer(ReservedListEntry.class) tm().reTransact(
.where("revisionId", EQ, revisionId) () ->
.stream() tm()
.collect(toImmutableMap(ReservedListEntry::getDomainLabel, e -> e))); .createQueryComposer(ReservedListEntry.class)
.where("revisionId", EQ, revisionId)
.stream()
.collect(toImmutableMap(ReservedListEntry::getDomainLabel, e -> e)));
}
}
} }
return ImmutableMap.copyOf(nullToEmpty(reservedListMap)); return reservedListMap;
} }
/** /**
@@ -220,7 +227,7 @@ public final class ReservedList
*/ */
public static ImmutableSet<ReservationType> getReservationTypes(String label, String tld) { public static ImmutableSet<ReservationType> getReservationTypes(String label, String tld) {
checkNotNull(label, "label"); checkNotNull(label, "label");
if (label.length() == 0) { if (label.isEmpty()) {
return ImmutableSet.of(FULLY_BLOCKED); return ImmutableSet.of(FULLY_BLOCKED);
} }
return getReservedListEntries(label, tld).stream() return getReservedListEntries(label, tld).stream()
@@ -127,9 +127,7 @@ public final class TmchCertificateAuthority {
* @see X509Utils#verifyCertificate * @see X509Utils#verifyCertificate
*/ */
public void verify(X509Certificate cert) throws GeneralSecurityException { public void verify(X509Certificate cert) throws GeneralSecurityException {
synchronized (TmchCertificateAuthority.class) { X509Utils.verifyCertificate(getAndValidateRoot(), getCrl(), cert, clock.now());
X509Utils.verifyCertificate(getAndValidateRoot(), getCrl(), cert, clock.now());
}
} }
/** /**
@@ -157,33 +155,23 @@ public final class TmchCertificateAuthority {
} }
public X509Certificate getAndValidateRoot() throws GeneralSecurityException { public X509Certificate getAndValidateRoot() throws GeneralSecurityException {
try { X509Certificate root = ROOT_CERTS.get(tmchCaMode);
X509Certificate root = ROOT_CERTS.get(tmchCaMode); // The current production certificate expires on 2042-11-15. Future code monkey be reminded,
// The current production certificate expires on 2023-07-23. Future code monkey be reminded, // if you are looking at this code because the next line throws an exception, ask ICANN for a
// if you are looking at this code because the next line throws an exception, ask ICANN for a // new root certificate! (preferably before the current one expires...)
// new root certificate! (preferably before the current one expires...) root.checkValidity(Date.from(clock.now()));
root.checkValidity(Date.from(clock.now())); return root;
return root;
} catch (Exception e) {
if (e instanceof GeneralSecurityException generalSecurityException) {
throw generalSecurityException;
} else if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw new RuntimeException(e);
}
} }
public X509CRL getCrl() throws GeneralSecurityException { public X509CRL getCrl() throws GeneralSecurityException {
try { try {
return CRL_CACHE.get(tmchCaMode); return CRL_CACHE.get(tmchCaMode);
} catch (Exception e) { } catch (RuntimeException e) {
if (e.getCause() instanceof GeneralSecurityException generalSecurityException) { if (e.getCause() instanceof GeneralSecurityException generalSecurityException) {
throw generalSecurityException; throw generalSecurityException;
} else if (e instanceof RuntimeException runtimeException) { } else {
throw runtimeException; throw e;
} }
throw new RuntimeException(e);
} }
} }
} }