diff --git a/core/src/main/java/google/registry/persistence/transaction/Transaction.java b/core/src/main/java/google/registry/persistence/transaction/Transaction.java index b02609b51..e8a700767 100644 --- a/core/src/main/java/google/registry/persistence/transaction/Transaction.java +++ b/core/src/main/java/google/registry/persistence/transaction/Transaction.java @@ -33,8 +33,10 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; /** * A SQL transaction that can be serialized and stored in its own table. @@ -105,7 +107,8 @@ public class Transaction extends ImmutableObject implements Buildable { } public static Transaction deserialize(byte[] serializedTransaction) throws IOException { - ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(serializedTransaction)); + ObjectInputStream in = + new LenientObjectInputStream(new ByteArrayInputStream(serializedTransaction)); // Verify that the data is what we expect. int version = in.readInt(); @@ -304,4 +307,35 @@ public class Transaction extends ImmutableObject implements Buildable { } } } + + /** + * ObjectInputStream that ignores the UIDs of serialized objects. + * + *
We only really need to deserialize VKeys. However, VKeys have a class object associated with + * them, and if the class is changed and we haven't defined a serialVersionUID for it, we get an + * exception during deserialization. + * + *
It's safe for us to ignore this condition: we only care about attaching the correct local
+ * class object to the VKey. So this class effectively does so by replacing the class descriptor
+ * if it's version UID doesn't match that of the local class.
+ */
+ private static class LenientObjectInputStream extends ObjectInputStream {
+
+ public LenientObjectInputStream(InputStream in) throws IOException {
+ super(in);
+ }
+
+ @Override
+ protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
+ ObjectStreamClass persistedDescriptor = super.readClassDescriptor();
+ Class localClass = Class.forName(persistedDescriptor.getName());
+ ObjectStreamClass localDescriptor = ObjectStreamClass.lookup(localClass);
+ if (localDescriptor != null) {
+ if (persistedDescriptor.getSerialVersionUID() != localDescriptor.getSerialVersionUID()) {
+ return localDescriptor;
+ }
+ }
+ return persistedDescriptor;
+ }
+ }
}
diff --git a/core/src/main/java/google/registry/tools/RegistryTool.java b/core/src/main/java/google/registry/tools/RegistryTool.java
index c24261423..2bbbffa8c 100644
--- a/core/src/main/java/google/registry/tools/RegistryTool.java
+++ b/core/src/main/java/google/registry/tools/RegistryTool.java
@@ -104,6 +104,7 @@ public final class RegistryTool {
.put("registrar_contact", RegistrarContactCommand.class)
.put("remove_registry_one_key", RemoveRegistryOneKeyCommand.class)
.put("renew_domain", RenewDomainCommand.class)
+ .put("replay_txns", ReplayTxnsCommand.class)
.put("resave_entities", ResaveEntitiesCommand.class)
.put("resave_environment_entities", ResaveEnvironmentEntitiesCommand.class)
.put("resave_epp_resource", ResaveEppResourceCommand.class)
diff --git a/core/src/main/java/google/registry/tools/ReplayTxnsCommand.java b/core/src/main/java/google/registry/tools/ReplayTxnsCommand.java
new file mode 100644
index 000000000..25f683c0d
--- /dev/null
+++ b/core/src/main/java/google/registry/tools/ReplayTxnsCommand.java
@@ -0,0 +1,59 @@
+// Copyright 2022 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.tools;
+
+import static google.registry.persistence.transaction.TransactionManagerFactory.replicaJpaTm;
+
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import google.registry.persistence.transaction.Transaction;
+import google.registry.persistence.transaction.TransactionEntity;
+import java.util.List;
+
+@Parameters(separators = " =", commandDescription = "Replay a range of transactions.")
+public class ReplayTxnsCommand implements CommandWithRemoteApi {
+
+ private static final int BATCH_SIZE = 200;
+
+ @Parameter(
+ names = {"-s", "--start-txn-id"},
+ description = "Transaction id to start replaying at.",
+ required = true)
+ long startTxnId;
+
+ @Override
+ public void run() throws Exception {
+ List