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 txns; + do { + txns = + replicaJpaTm() + .transact( + () -> + replicaJpaTm() + .query( + "SELECT txn FROM TransactionEntity txn where id >= :startTxn ORDER" + + " BY id", + TransactionEntity.class) + .setParameter("startTxn", startTxnId) + .setMaxResults(BATCH_SIZE) + .getResultList()); + for (TransactionEntity txn : txns) { + System.out.println("Replaying transaction " + txn.getId()); + Transaction.deserialize(txn.getContents()); + startTxnId = txn.getId() + 1; + } + } while (txns.size() > 0); + } +}