diff --git a/config/presubmits.py b/config/presubmits.py
index 9d85c4c3c..4d6275702 100644
--- a/config/presubmits.py
+++ b/config/presubmits.py
@@ -215,6 +215,7 @@ PRESUBMITS = {
"RdapDomainSearchAction.java",
"RdapNameserverSearchAction.java",
"RdapSearchActionBase.java",
+ "ReadOnlyCheckingEntityManager.java",
"RegistryQuery",
},
):
diff --git a/core/src/main/java/google/registry/backup/ReplayCommitLogsToSqlAction.java b/core/src/main/java/google/registry/backup/ReplayCommitLogsToSqlAction.java
index 96c57949a..a4a688d6e 100644
--- a/core/src/main/java/google/registry/backup/ReplayCommitLogsToSqlAction.java
+++ b/core/src/main/java/google/registry/backup/ReplayCommitLogsToSqlAction.java
@@ -260,7 +260,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
.ifPresent(
sqlEntity -> {
sqlEntity.beforeSqlSaveOnReplay();
- jpaTm().put(sqlEntity);
+ jpaTm().putIgnoringReadOnly(sqlEntity);
});
} else {
// this should never happen, but we shouldn't fail on it
@@ -293,7 +293,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
&& !DatastoreOnlyEntity.class.isAssignableFrom(entityClass)
&& entityClass.getAnnotation(javax.persistence.Entity.class) != null) {
ReplaySpecializer.beforeSqlDelete(entityVKey);
- jpaTm().delete(entityVKey);
+ jpaTm().deleteIgnoringReadOnly(entityVKey);
}
} catch (Throwable t) {
logger.atSevere().log("Error when deleting key %s", entityVKey);
diff --git a/core/src/main/java/google/registry/model/common/DatabaseMigrationStateSchedule.java b/core/src/main/java/google/registry/model/common/DatabaseMigrationStateSchedule.java
index 3cb240657..256999ec4 100644
--- a/core/src/main/java/google/registry/model/common/DatabaseMigrationStateSchedule.java
+++ b/core/src/main/java/google/registry/model/common/DatabaseMigrationStateSchedule.java
@@ -36,8 +36,8 @@ import org.joda.time.DateTime;
/**
* A wrapper object representing the stage-to-time mapping of the Registry 3.0 Cloud SQL migration.
*
- *
The entity is stored in Datastore throughout the entire migration so as to have a single point
- * of access.
+ *
The entity is stored in SQL throughout the entire migration so as to have a single point of
+ * access.
*/
@Entity
public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements SqlOnlyEntity {
@@ -187,12 +187,12 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
private DatabaseMigrationStateSchedule() {}
@VisibleForTesting
- DatabaseMigrationStateSchedule(
+ public DatabaseMigrationStateSchedule(
TimedTransitionProperty migrationTransitions) {
this.migrationTransitions = migrationTransitions;
}
- /** Sets and persists to Datastore the provided migration transition schedule. */
+ /** Sets and persists to SQL the provided migration transition schedule. */
public static void set(ImmutableSortedMap migrationTransitionMap) {
jpaTm().assertInTransaction();
TimedTransitionProperty transitions =
@@ -204,7 +204,7 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
MigrationState.DATASTORE_ONLY,
"migrationTransitionMap must start with DATASTORE_ONLY");
validateTransitionAtCurrentTime(transitions);
- jpaTm().put(new DatabaseMigrationStateSchedule(transitions));
+ jpaTm().putIgnoringReadOnly(new DatabaseMigrationStateSchedule(transitions));
CACHE.invalidateAll();
}
@@ -218,7 +218,7 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
return get().getValueAtTime(dateTime);
}
- /** Loads the currently-set migration schedule from Datastore, or the default if none exists. */
+ /** Loads the currently-set migration schedule from SQL, or the default if none exists. */
@VisibleForTesting
static TimedTransitionProperty getUncached() {
return jpaTm()
diff --git a/core/src/main/java/google/registry/model/ofy/DatastoreTransactionManager.java b/core/src/main/java/google/registry/model/ofy/DatastoreTransactionManager.java
index b9503b70a..145b3eb67 100644
--- a/core/src/main/java/google/registry/model/ofy/DatastoreTransactionManager.java
+++ b/core/src/main/java/google/registry/model/ofy/DatastoreTransactionManager.java
@@ -336,7 +336,7 @@ public class DatastoreTransactionManager implements TransactionManager {
@Override
public QueryComposer createQueryComposer(Class entity) {
- return new DatastoreQueryComposerImpl(entity);
+ return new DatastoreQueryComposerImpl<>(entity);
}
@Override
@@ -349,6 +349,16 @@ public class DatastoreTransactionManager implements TransactionManager {
return true;
}
+ @Override
+ public void putIgnoringReadOnly(Object entity) {
+ syncIfTransactionless(getOfy().saveIgnoringReadOnly().entities(toDatastoreEntity(entity)));
+ }
+
+ @Override
+ public void deleteIgnoringReadOnly(VKey> key) {
+ syncIfTransactionless(getOfy().deleteIgnoringReadOnly().key(key.getOfyKey()));
+ }
+
/**
* Executes the given {@link Result} instance synchronously if not in a transaction.
*
diff --git a/core/src/main/java/google/registry/model/ofy/Ofy.java b/core/src/main/java/google/registry/model/ofy/Ofy.java
index abd38f40c..38cdc9329 100644
--- a/core/src/main/java/google/registry/model/ofy/Ofy.java
+++ b/core/src/main/java/google/registry/model/ofy/Ofy.java
@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Maps.uniqueIndex;
import static com.googlecode.objectify.ObjectifyService.ofy;
import static google.registry.config.RegistryConfig.getBaseOfyRetryDuration;
+import static google.registry.persistence.transaction.TransactionManagerFactory.assertNotReadOnlyMode;
import static google.registry.util.CollectionUtils.union;
import com.google.appengine.api.datastore.DatastoreFailureException;
@@ -131,6 +132,7 @@ public class Ofy {
*
We only allow this in transactions so commit logs can be written in tandem with the delete.
*/
public Deleter delete() {
+ assertNotReadOnlyMode();
return new AugmentedDeleter() {
@Override
protected void handleDeletion(Iterable> keys) {
@@ -148,12 +150,8 @@ public class Ofy {
*
No backups get written.
*/
public Deleter deleteWithoutBackup() {
- return new AugmentedDeleter() {
- @Override
- protected void handleDeletion(Iterable> keys) {
- checkProhibitedAnnotations(keys, VirtualEntity.class);
- }
- };
+ assertNotReadOnlyMode();
+ return deleteIgnoringReadOnly();
}
/**
@@ -163,6 +161,7 @@ public class Ofy {
*
We only allow this in transactions so commit logs can be written in tandem with the save.
*/
public Saver save() {
+ assertNotReadOnlyMode();
return new AugmentedSaver() {
@Override
protected void handleSave(Iterable> entities) {
@@ -182,6 +181,12 @@ public class Ofy {
*
No backups get written.
*/
public Saver saveWithoutBackup() {
+ assertNotReadOnlyMode();
+ return saveIgnoringReadOnly();
+ }
+
+ /** Save, ignoring any backups or any read-only settings. */
+ public Saver saveIgnoringReadOnly() {
return new AugmentedSaver() {
@Override
protected void handleSave(Iterable> entities) {
@@ -190,6 +195,16 @@ public class Ofy {
};
}
+ /** Delete, ignoring any backups or any read-only settings. */
+ public Deleter deleteIgnoringReadOnly() {
+ return new AugmentedDeleter() {
+ @Override
+ protected void handleDeletion(Iterable> keys) {
+ checkProhibitedAnnotations(keys, VirtualEntity.class);
+ }
+ };
+ }
+
private Clock getClock() {
return injectedClock == null ? clock : injectedClock;
}
diff --git a/core/src/main/java/google/registry/model/replay/SqlReplayCheckpoint.java b/core/src/main/java/google/registry/model/replay/SqlReplayCheckpoint.java
index 85f6d5362..fa037567f 100644
--- a/core/src/main/java/google/registry/model/replay/SqlReplayCheckpoint.java
+++ b/core/src/main/java/google/registry/model/replay/SqlReplayCheckpoint.java
@@ -41,6 +41,6 @@ public class SqlReplayCheckpoint extends CrossTldSingleton implements SqlOnlyEnt
SqlReplayCheckpoint checkpoint = new SqlReplayCheckpoint();
checkpoint.lastReplayTime = lastReplayTime;
// this will overwrite the existing object due to the constant revisionId
- jpaTm().put(checkpoint);
+ jpaTm().putIgnoringReadOnly(checkpoint);
}
}
diff --git a/core/src/main/java/google/registry/model/server/Lock.java b/core/src/main/java/google/registry/model/server/Lock.java
index 69fddccf8..b3b3169f4 100644
--- a/core/src/main/java/google/registry/model/server/Lock.java
+++ b/core/src/main/java/google/registry/model/server/Lock.java
@@ -250,7 +250,7 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
resourceName, scope, requestStatusChecker.getLogId(), now, leaseLength);
// Locks are not parented under an EntityGroupRoot (so as to avoid write
// contention) and don't need to be backed up.
- tm().putWithoutBackup(newLock);
+ tm().putIgnoringReadOnly(newLock);
return AcquireResult.create(now, lock, newLock, lockState);
});
@@ -269,18 +269,15 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
// delete it. If the lock in Datastore was different then this lock is gone already;
// this can happen if release() is called around the expiration time and the lock
// expires underneath us.
- Lock loadedLock =
- tm().loadByKeyIfPresent(
- VKey.create(
- Lock.class,
- new LockId(resourceName, tld),
- Key.create(Lock.class, lockId)))
- .orElse(null);
+ VKey key =
+ VKey.create(
+ Lock.class, new LockId(resourceName, tld), Key.create(Lock.class, lockId));
+ Lock loadedLock = tm().loadByKeyIfPresent(key).orElse(null);
if (Lock.this.equals(loadedLock)) {
// Use deleteWithoutBackup() so that we don't create a commit log entry for deleting
// the lock.
logger.atInfo().log("Deleting lock: %s", lockId);
- tm().deleteWithoutBackup(Lock.this);
+ tm().deleteIgnoringReadOnly(key);
lockMetrics.recordRelease(
resourceName, tld, new Duration(acquiredTime, tm().getTransactionTime()));
diff --git a/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java b/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java
index 2f2eea9b1..7ec304a3d 100644
--- a/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java
+++ b/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java
@@ -119,22 +119,23 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public EntityManager getEntityManager() {
- if (transactionInfo.get().entityManager == null) {
+ EntityManager entityManager = transactionInfo.get().entityManager;
+ if (entityManager == null) {
throw new PersistenceException(
"No EntityManager has been initialized. getEntityManager() must be invoked in the scope"
+ " of a transaction");
}
- return transactionInfo.get().entityManager;
+ return entityManager;
}
@Override
public TypedQuery query(String sqlString, Class resultClass) {
- return new DetachingTypedQuery(getEntityManager().createQuery(sqlString, resultClass));
+ return new DetachingTypedQuery<>(getEntityManager().createQuery(sqlString, resultClass));
}
@Override
public TypedQuery query(CriteriaQuery criteriaQuery) {
- return new DetachingTypedQuery(getEntityManager().createQuery(criteriaQuery));
+ return new DetachingTypedQuery<>(getEntityManager().createQuery(criteriaQuery));
}
@Override
@@ -171,7 +172,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return work.get();
}
TransactionInfo txnInfo = transactionInfo.get();
- txnInfo.entityManager = emf.createEntityManager();
+ txnInfo.entityManager = createReadOnlyCheckingEntityManager();
EntityTransaction txn = txnInfo.entityManager.getTransaction();
try {
txn.begin();
@@ -203,7 +204,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return work.get();
}
TransactionInfo txnInfo = transactionInfo.get();
- txnInfo.entityManager = emf.createEntityManager();
+ txnInfo.entityManager = createReadOnlyCheckingEntityManager();
EntityTransaction txn = txnInfo.entityManager.getTransaction();
try {
txn.begin();
@@ -594,7 +595,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public QueryComposer createQueryComposer(Class entity) {
- return new JpaQueryComposerImpl(entity);
+ return new JpaQueryComposerImpl<>(entity);
}
@Override
@@ -607,6 +608,38 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return false;
}
+ @Override
+ public void putIgnoringReadOnly(Object entity) {
+ checkArgumentNotNull(entity);
+ if (isEntityOfIgnoredClass(entity)) {
+ return;
+ }
+ assertInTransaction();
+ // Necessary due to the changes in HistoryEntry representation during the migration to SQL
+ Object toPersist = toSqlEntity(entity);
+ TransactionInfo txn = transactionInfo.get();
+ Object merged = txn.entityManager.mergeIgnoringReadOnly(toPersist);
+ txn.objectsToSave.add(merged);
+ txn.addUpdate(toPersist);
+ }
+
+ @Override
+ public void deleteIgnoringReadOnly(VKey> key) {
+ checkArgumentNotNull(key, "key must be specified");
+ assertInTransaction();
+ if (IGNORED_ENTITY_CLASSES.contains(key.getKind())) {
+ return;
+ }
+ EntityType> entityType = getEntityType(key.getKind());
+ ImmutableSet entityIds = getEntityIdsFromSqlKey(entityType, key.getSqlKey());
+ String sql =
+ String.format("DELETE FROM %s WHERE %s", entityType.getName(), getAndClause(entityIds));
+ ReadOnlyCheckingQuery query = transactionInfo.get().entityManager.createQuery(sql);
+ entityIds.forEach(entityId -> query.setParameter(entityId.name, entityId.value));
+ transactionInfo.get().addDelete(key);
+ query.executeUpdateIgnoringReadOnly();
+ }
+
@Override
public void assertDelete(VKey key) {
if (internalDelete(key) != 1) {
@@ -615,6 +648,10 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
}
+ private ReadOnlyCheckingEntityManager createReadOnlyCheckingEntityManager() {
+ return new ReadOnlyCheckingEntityManager(emf.createEntityManager());
+ }
+
private EntityType getEntityType(Class clazz) {
return emf.getMetamodel().entity(clazz);
}
@@ -750,7 +787,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
private static class TransactionInfo {
- EntityManager entityManager;
+ ReadOnlyCheckingEntityManager entityManager;
boolean inTransaction = false;
DateTime transactionTime;
diff --git a/core/src/main/java/google/registry/persistence/transaction/ReadOnlyCheckingEntityManager.java b/core/src/main/java/google/registry/persistence/transaction/ReadOnlyCheckingEntityManager.java
new file mode 100644
index 000000000..7fe305666
--- /dev/null
+++ b/core/src/main/java/google/registry/persistence/transaction/ReadOnlyCheckingEntityManager.java
@@ -0,0 +1,320 @@
+// Copyright 2021 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.persistence.transaction;
+
+import static google.registry.persistence.transaction.TransactionManagerFactory.assertNotReadOnlyMode;
+
+import java.util.List;
+import java.util.Map;
+import javax.persistence.EntityGraph;
+import javax.persistence.EntityManager;
+import javax.persistence.EntityManagerFactory;
+import javax.persistence.EntityTransaction;
+import javax.persistence.FlushModeType;
+import javax.persistence.LockModeType;
+import javax.persistence.Query;
+import javax.persistence.StoredProcedureQuery;
+import javax.persistence.TypedQuery;
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaDelete;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.CriteriaUpdate;
+import javax.persistence.metamodel.Metamodel;
+
+/** An {@link EntityManager} that throws exceptions on write actions if in read-only mode. */
+public class ReadOnlyCheckingEntityManager implements EntityManager {
+
+ private final EntityManager delegate;
+
+ public ReadOnlyCheckingEntityManager(EntityManager delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void persist(Object entity) {
+ assertNotReadOnlyMode();
+ delegate.persist(entity);
+ }
+
+ @Override
+ public T merge(T entity) {
+ assertNotReadOnlyMode();
+ return delegate.merge(entity);
+ }
+
+ @Override
+ public void remove(Object entity) {
+ assertNotReadOnlyMode();
+ delegate.remove(entity);
+ }
+
+ @Override
+ public T find(Class entityClass, Object primaryKey) {
+ return delegate.find(entityClass, primaryKey);
+ }
+
+ @Override
+ public T find(Class entityClass, Object primaryKey, Map properties) {
+ return delegate.find(entityClass, primaryKey, properties);
+ }
+
+ @Override
+ public T find(Class entityClass, Object primaryKey, LockModeType lockMode) {
+ return delegate.find(entityClass, primaryKey, lockMode);
+ }
+
+ @Override
+ public T find(
+ Class entityClass,
+ Object primaryKey,
+ LockModeType lockMode,
+ Map properties) {
+ return delegate.find(entityClass, primaryKey, lockMode, properties);
+ }
+
+ @Override
+ public T getReference(Class entityClass, Object primaryKey) {
+ return delegate.getReference(entityClass, primaryKey);
+ }
+
+ @Override
+ public void flush() {
+ delegate.flush();
+ }
+
+ @Override
+ public void setFlushMode(FlushModeType flushMode) {
+ delegate.setFlushMode(flushMode);
+ }
+
+ @Override
+ public FlushModeType getFlushMode() {
+ return delegate.getFlushMode();
+ }
+
+ @Override
+ public void lock(Object entity, LockModeType lockMode) {
+ assertNotReadOnlyMode();
+ delegate.lock(entity, lockMode);
+ }
+
+ @Override
+ public void lock(Object entity, LockModeType lockMode, Map properties) {
+ assertNotReadOnlyMode();
+ delegate.lock(entity, lockMode, properties);
+ }
+
+ @Override
+ public void refresh(Object entity) {
+ delegate.refresh(entity);
+ }
+
+ @Override
+ public void refresh(Object entity, Map properties) {
+ delegate.refresh(entity, properties);
+ }
+
+ @Override
+ public void refresh(Object entity, LockModeType lockMode) {
+ delegate.refresh(entity, lockMode);
+ }
+
+ @Override
+ public void refresh(Object entity, LockModeType lockMode, Map properties) {
+ delegate.refresh(entity, lockMode, properties);
+ }
+
+ @Override
+ public void clear() {
+ delegate.clear();
+ }
+
+ @Override
+ public void detach(Object entity) {
+ delegate.detach(entity);
+ }
+
+ @Override
+ public boolean contains(Object entity) {
+ return delegate.contains(entity);
+ }
+
+ @Override
+ public LockModeType getLockMode(Object entity) {
+ return delegate.getLockMode(entity);
+ }
+
+ @Override
+ public void setProperty(String propertyName, Object value) {
+ delegate.setProperty(propertyName, value);
+ }
+
+ @Override
+ public Map getProperties() {
+ return delegate.getProperties();
+ }
+
+ @Override
+ public ReadOnlyCheckingQuery createQuery(String qlString) {
+ return new ReadOnlyCheckingQuery(delegate.createQuery(qlString));
+ }
+
+ @Override
+ public TypedQuery createQuery(CriteriaQuery criteriaQuery) {
+ return new ReadOnlyCheckingTypedQuery<>(delegate.createQuery(criteriaQuery));
+ }
+
+ @Override
+ public Query createQuery(CriteriaUpdate updateQuery) {
+ assertNotReadOnlyMode();
+ return delegate.createQuery(updateQuery);
+ }
+
+ @Override
+ public Query createQuery(CriteriaDelete deleteQuery) {
+ assertNotReadOnlyMode();
+ return delegate.createQuery(deleteQuery);
+ }
+
+ @Override
+ public TypedQuery createQuery(String qlString, Class resultClass) {
+ return new ReadOnlyCheckingTypedQuery<>(delegate.createQuery(qlString, resultClass));
+ }
+
+ @Override
+ public Query createNamedQuery(String name) {
+ return new ReadOnlyCheckingQuery(delegate.createNamedQuery(name));
+ }
+
+ @Override
+ public TypedQuery createNamedQuery(String name, Class resultClass) {
+ return new ReadOnlyCheckingTypedQuery<>(delegate.createNamedQuery(name, resultClass));
+ }
+
+ @Override
+ public Query createNativeQuery(String sqlString) {
+ return new ReadOnlyCheckingQuery(delegate.createNativeQuery(sqlString));
+ }
+
+ @Override
+ public Query createNativeQuery(String sqlString, Class resultClass) {
+ return new ReadOnlyCheckingQuery(delegate.createNativeQuery(sqlString, resultClass));
+ }
+
+ @Override
+ public Query createNativeQuery(String sqlString, String resultSetMapping) {
+ return new ReadOnlyCheckingQuery(delegate.createNativeQuery(sqlString, resultSetMapping));
+ }
+
+ @Override
+ public StoredProcedureQuery createNamedStoredProcedureQuery(String name) {
+ assertNotReadOnlyMode();
+ return delegate.createNamedStoredProcedureQuery(name);
+ }
+
+ @Override
+ public StoredProcedureQuery createStoredProcedureQuery(String procedureName) {
+ assertNotReadOnlyMode();
+ return delegate.createStoredProcedureQuery(procedureName);
+ }
+
+ @Override
+ public StoredProcedureQuery createStoredProcedureQuery(
+ String procedureName, Class... resultClasses) {
+ assertNotReadOnlyMode();
+ return delegate.createStoredProcedureQuery(procedureName, resultClasses);
+ }
+
+ @Override
+ public StoredProcedureQuery createStoredProcedureQuery(
+ String procedureName, String... resultSetMappings) {
+ assertNotReadOnlyMode();
+ return delegate.createStoredProcedureQuery(procedureName, resultSetMappings);
+ }
+
+ @Override
+ public void joinTransaction() {
+ delegate.joinTransaction();
+ }
+
+ @Override
+ public boolean isJoinedToTransaction() {
+ return delegate.isJoinedToTransaction();
+ }
+
+ @Override
+ public T unwrap(Class cls) {
+ return delegate.unwrap(cls);
+ }
+
+ @Override
+ public Object getDelegate() {
+ return delegate.getDelegate();
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ }
+
+ @Override
+ public boolean isOpen() {
+ return delegate.isOpen();
+ }
+
+ @Override
+ public EntityTransaction getTransaction() {
+ return delegate.getTransaction();
+ }
+
+ @Override
+ public EntityManagerFactory getEntityManagerFactory() {
+ return delegate.getEntityManagerFactory();
+ }
+
+ @Override
+ public CriteriaBuilder getCriteriaBuilder() {
+ return delegate.getCriteriaBuilder();
+ }
+
+ @Override
+ public Metamodel getMetamodel() {
+ return delegate.getMetamodel();
+ }
+
+ @Override
+ public EntityGraph createEntityGraph(Class rootType) {
+ return delegate.createEntityGraph(rootType);
+ }
+
+ @Override
+ public EntityGraph> createEntityGraph(String graphName) {
+ return delegate.createEntityGraph(graphName);
+ }
+
+ @Override
+ public EntityGraph> getEntityGraph(String graphName) {
+ return delegate.getEntityGraph(graphName);
+ }
+
+ @Override
+ public List> getEntityGraphs(Class entityClass) {
+ return delegate.getEntityGraphs(entityClass);
+ }
+
+ public T mergeIgnoringReadOnly(T entity) {
+ return delegate.merge(entity);
+ }
+}
diff --git a/core/src/main/java/google/registry/persistence/transaction/ReadOnlyCheckingQuery.java b/core/src/main/java/google/registry/persistence/transaction/ReadOnlyCheckingQuery.java
new file mode 100644
index 000000000..aaed3d028
--- /dev/null
+++ b/core/src/main/java/google/registry/persistence/transaction/ReadOnlyCheckingQuery.java
@@ -0,0 +1,203 @@
+// Copyright 2021 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.persistence.transaction;
+
+import static google.registry.persistence.transaction.TransactionManagerFactory.assertNotReadOnlyMode;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.persistence.FlushModeType;
+import javax.persistence.LockModeType;
+import javax.persistence.Parameter;
+import javax.persistence.Query;
+import javax.persistence.TemporalType;
+
+/** A {@link Query} that throws exceptions on write actions if in read-only mode. */
+class ReadOnlyCheckingQuery implements Query {
+
+ private final Query delegate;
+
+ ReadOnlyCheckingQuery(Query delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public List getResultList() {
+ return delegate.getResultList();
+ }
+
+ @Override
+ public Object getSingleResult() {
+ return delegate.getSingleResult();
+ }
+
+ @Override
+ public int executeUpdate() {
+ assertNotReadOnlyMode();
+ return delegate.executeUpdate();
+ }
+
+ @Override
+ public Query setMaxResults(int maxResult) {
+ return delegate.setMaxResults(maxResult);
+ }
+
+ @Override
+ public int getMaxResults() {
+ return delegate.getMaxResults();
+ }
+
+ @Override
+ public Query setFirstResult(int startPosition) {
+ return delegate.setFirstResult(startPosition);
+ }
+
+ @Override
+ public int getFirstResult() {
+ return delegate.getFirstResult();
+ }
+
+ @Override
+ public Query setHint(String hintName, Object value) {
+ return delegate.setHint(hintName, value);
+ }
+
+ @Override
+ public Map getHints() {
+ return delegate.getHints();
+ }
+
+ @Override
+ public Query setParameter(Parameter param, T value) {
+ return delegate.setParameter(param, value);
+ }
+
+ @Override
+ public Query setParameter(Parameter param, Calendar value, TemporalType temporalType) {
+ return delegate.setParameter(param, value, temporalType);
+ }
+
+ @Override
+ public Query setParameter(Parameter param, Date value, TemporalType temporalType) {
+ return delegate.setParameter(param, value, temporalType);
+ }
+
+ @Override
+ public Query setParameter(String name, Object value) {
+ return delegate.setParameter(name, value);
+ }
+
+ @Override
+ public Query setParameter(String name, Calendar value, TemporalType temporalType) {
+ return delegate.setParameter(name, value, temporalType);
+ }
+
+ @Override
+ public Query setParameter(String name, Date value, TemporalType temporalType) {
+ return delegate.setParameter(name, value, temporalType);
+ }
+
+ @Override
+ public Query setParameter(int position, Object value) {
+ return delegate.setParameter(position, value);
+ }
+
+ @Override
+ public Query setParameter(int position, Calendar value, TemporalType temporalType) {
+ return delegate.setParameter(position, value, temporalType);
+ }
+
+ @Override
+ public Query setParameter(int position, Date value, TemporalType temporalType) {
+ return delegate.setParameter(position, value, temporalType);
+ }
+
+ @Override
+ public Set> getParameters() {
+ return delegate.getParameters();
+ }
+
+ @Override
+ public Parameter> getParameter(String name) {
+ return delegate.getParameter(name);
+ }
+
+ @Override
+ public Parameter getParameter(String name, Class type) {
+ return delegate.getParameter(name, type);
+ }
+
+ @Override
+ public Parameter> getParameter(int position) {
+ return delegate.getParameter(position);
+ }
+
+ @Override
+ public Parameter getParameter(int position, Class type) {
+ return delegate.getParameter(position, type);
+ }
+
+ @Override
+ public boolean isBound(Parameter> param) {
+ return delegate.isBound(param);
+ }
+
+ @Override
+ public T getParameterValue(Parameter param) {
+ return delegate.getParameterValue(param);
+ }
+
+ @Override
+ public Object getParameterValue(String name) {
+ return delegate.getParameterValue(name);
+ }
+
+ @Override
+ public Object getParameterValue(int position) {
+ return delegate.getParameterValue(position);
+ }
+
+ @Override
+ public Query setFlushMode(FlushModeType flushMode) {
+ return delegate.setFlushMode(flushMode);
+ }
+
+ @Override
+ public FlushModeType getFlushMode() {
+ return delegate.getFlushMode();
+ }
+
+ @Override
+ public Query setLockMode(LockModeType lockMode) {
+ return delegate.setLockMode(lockMode);
+ }
+
+ @Override
+ public LockModeType getLockMode() {
+ return delegate.getLockMode();
+ }
+
+ @Override
+ public T unwrap(Class cls) {
+ return delegate.unwrap(cls);
+ }
+
+ public int executeUpdateIgnoringReadOnly() {
+ return delegate.executeUpdate();
+ }
+}
diff --git a/core/src/main/java/google/registry/persistence/transaction/ReadOnlyCheckingTypedQuery.java b/core/src/main/java/google/registry/persistence/transaction/ReadOnlyCheckingTypedQuery.java
new file mode 100644
index 000000000..dcda3a136
--- /dev/null
+++ b/core/src/main/java/google/registry/persistence/transaction/ReadOnlyCheckingTypedQuery.java
@@ -0,0 +1,200 @@
+// Copyright 2021 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.persistence.transaction;
+
+import static google.registry.persistence.transaction.TransactionManagerFactory.assertNotReadOnlyMode;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.persistence.FlushModeType;
+import javax.persistence.LockModeType;
+import javax.persistence.Parameter;
+import javax.persistence.TemporalType;
+import javax.persistence.TypedQuery;
+
+/** A {@link TypedQuery } that throws exceptions on write actions if in read-only mode. */
+class ReadOnlyCheckingTypedQuery implements TypedQuery {
+
+ private final TypedQuery delegate;
+
+ ReadOnlyCheckingTypedQuery(TypedQuery delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public List getResultList() {
+ return delegate.getResultList();
+ }
+
+ @Override
+ public T getSingleResult() {
+ return delegate.getSingleResult();
+ }
+
+ @Override
+ public int executeUpdate() {
+ assertNotReadOnlyMode();
+ return delegate.executeUpdate();
+ }
+
+ @Override
+ public TypedQuery setMaxResults(int maxResult) {
+ return delegate.setMaxResults(maxResult);
+ }
+
+ @Override
+ public int getMaxResults() {
+ return delegate.getMaxResults();
+ }
+
+ @Override
+ public TypedQuery setFirstResult(int startPosition) {
+ return delegate.setFirstResult(startPosition);
+ }
+
+ @Override
+ public int getFirstResult() {
+ return delegate.getFirstResult();
+ }
+
+ @Override
+ public TypedQuery setHint(String hintName, Object value) {
+ return delegate.setHint(hintName, value);
+ }
+
+ @Override
+ public Map getHints() {
+ return delegate.getHints();
+ }
+
+ @Override
+ public TypedQuery setParameter(Parameter param, T1 value) {
+ return delegate.setParameter(param, value);
+ }
+
+ @Override
+ public TypedQuery setParameter(
+ Parameter param, Calendar value, TemporalType temporalType) {
+ return delegate.setParameter(param, value, temporalType);
+ }
+
+ @Override
+ public TypedQuery setParameter(Parameter param, Date value, TemporalType temporalType) {
+ return delegate.setParameter(param, value, temporalType);
+ }
+
+ @Override
+ public TypedQuery setParameter(String name, Object value) {
+ return delegate.setParameter(name, value);
+ }
+
+ @Override
+ public TypedQuery setParameter(String name, Calendar value, TemporalType temporalType) {
+ return delegate.setParameter(name, value, temporalType);
+ }
+
+ @Override
+ public TypedQuery setParameter(String name, Date value, TemporalType temporalType) {
+ return delegate.setParameter(name, value, temporalType);
+ }
+
+ @Override
+ public TypedQuery setParameter(int position, Object value) {
+ return delegate.setParameter(position, value);
+ }
+
+ @Override
+ public TypedQuery setParameter(int position, Calendar value, TemporalType temporalType) {
+ return delegate.setParameter(position, value, temporalType);
+ }
+
+ @Override
+ public TypedQuery setParameter(int position, Date value, TemporalType temporalType) {
+ return delegate.setParameter(position, value, temporalType);
+ }
+
+ @Override
+ public Set> getParameters() {
+ return delegate.getParameters();
+ }
+
+ @Override
+ public Parameter> getParameter(String name) {
+ return delegate.getParameter(name);
+ }
+
+ @Override
+ public Parameter getParameter(String name, Class type) {
+ return delegate.getParameter(name, type);
+ }
+
+ @Override
+ public Parameter> getParameter(int position) {
+ return delegate.getParameter(position);
+ }
+
+ @Override
+ public Parameter getParameter(int position, Class type) {
+ return delegate.getParameter(position, type);
+ }
+
+ @Override
+ public boolean isBound(Parameter> param) {
+ return delegate.isBound(param);
+ }
+
+ @Override
+ public X getParameterValue(Parameter param) {
+ return delegate.getParameterValue(param);
+ }
+
+ @Override
+ public Object getParameterValue(String name) {
+ return delegate.getParameterValue(name);
+ }
+
+ @Override
+ public Object getParameterValue(int position) {
+ return delegate.getParameterValue(position);
+ }
+
+ @Override
+ public TypedQuery setFlushMode(FlushModeType flushMode) {
+ return delegate.setFlushMode(flushMode);
+ }
+
+ @Override
+ public FlushModeType getFlushMode() {
+ return delegate.getFlushMode();
+ }
+
+ @Override
+ public TypedQuery setLockMode(LockModeType lockMode) {
+ return delegate.setLockMode(lockMode);
+ }
+
+ @Override
+ public LockModeType getLockMode() {
+ return delegate.getLockMode();
+ }
+
+ @Override
+ public X unwrap(Class cls) {
+ return delegate.unwrap(cls);
+ }
+}
diff --git a/core/src/main/java/google/registry/persistence/transaction/TransactionManager.java b/core/src/main/java/google/registry/persistence/transaction/TransactionManager.java
index e92d1b381..08c7c6930 100644
--- a/core/src/main/java/google/registry/persistence/transaction/TransactionManager.java
+++ b/core/src/main/java/google/registry/persistence/transaction/TransactionManager.java
@@ -307,4 +307,10 @@ public interface TransactionManager {
/** Returns true if the transaction manager is DatastoreTransactionManager, false otherwise. */
boolean isOfy();
+
+ /** Performs the given write ignoring any read-only restrictions, for use only in replay. */
+ void putIgnoringReadOnly(Object entity);
+
+ /** Performs the given delete ignoring any read-only restrictions, for use only in replay. */
+ void deleteIgnoringReadOnly(VKey> key);
}
diff --git a/core/src/main/java/google/registry/persistence/transaction/TransactionManagerFactory.java b/core/src/main/java/google/registry/persistence/transaction/TransactionManagerFactory.java
index bdeee15e1..e957cf147 100644
--- a/core/src/main/java/google/registry/persistence/transaction/TransactionManagerFactory.java
+++ b/core/src/main/java/google/registry/persistence/transaction/TransactionManagerFactory.java
@@ -86,9 +86,11 @@ public class TransactionManagerFactory {
if (tmForTest.isPresent()) {
return tmForTest.get();
}
- PrimaryDatabase primaryDatabase =
- DatabaseMigrationStateSchedule.getValueAtTime(DateTime.now(UTC)).getPrimaryDatabase();
- return primaryDatabase.equals(PrimaryDatabase.DATASTORE) ? ofyTm() : jpaTm();
+ return DatabaseMigrationStateSchedule.getValueAtTime(DateTime.now(UTC))
+ .getPrimaryDatabase()
+ .equals(PrimaryDatabase.DATASTORE)
+ ? ofyTm()
+ : jpaTm();
}
/**
@@ -141,4 +143,17 @@ public class TransactionManagerFactory {
public static void removeTmOverrideForTest() {
tmForTest = Optional.empty();
}
+
+ public static void assertNotReadOnlyMode() {
+ if (DatabaseMigrationStateSchedule.getValueAtTime(DateTime.now(UTC)).isReadOnly()) {
+ throw new ReadOnlyModeException();
+ }
+ }
+
+ /** Thrown when a write is attempted when the DB is in read-only mode. */
+ public static class ReadOnlyModeException extends IllegalStateException {
+ public ReadOnlyModeException() {
+ super("Registry is currently in read-only mode");
+ }
+ }
}
diff --git a/core/src/test/java/google/registry/backup/ReplayCommitLogsToSqlActionTest.java b/core/src/test/java/google/registry/backup/ReplayCommitLogsToSqlActionTest.java
index 9c45a959b..e4024f763 100644
--- a/core/src/test/java/google/registry/backup/ReplayCommitLogsToSqlActionTest.java
+++ b/core/src/test/java/google/registry/backup/ReplayCommitLogsToSqlActionTest.java
@@ -40,6 +40,7 @@ import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.collect.Iterables;
import com.google.common.truth.Truth8;
import com.google.common.util.concurrent.MoreExecutors;
import com.googlecode.objectify.Key;
@@ -357,10 +358,10 @@ public class ReplayCommitLogsToSqlActionTest {
// even though the domain came first in the file
// 2. that the allocation token delete occurred after the insertions
InOrder inOrder = Mockito.inOrder(spy);
- inOrder.verify(spy).put(any(ContactResource.class));
- inOrder.verify(spy).put(any(DomainBase.class));
- inOrder.verify(spy).delete(toDelete.createVKey());
- inOrder.verify(spy).put(any(SqlReplayCheckpoint.class));
+ inOrder.verify(spy).putIgnoringReadOnly(any(ContactResource.class));
+ inOrder.verify(spy).putIgnoringReadOnly(any(DomainBase.class));
+ inOrder.verify(spy).deleteIgnoringReadOnly(toDelete.createVKey());
+ inOrder.verify(spy).putIgnoringReadOnly(any(SqlReplayCheckpoint.class));
}
@Test
@@ -399,8 +400,8 @@ public class ReplayCommitLogsToSqlActionTest {
// deletes have higher weight
ArgumentCaptor