Ignore version UIDs during txn deserialization (#1607)

* Ignore version UIDs during txn deserialization

When deserializing transactions for replay to datastore, ignore class version
UIDs that don't match those of the local classes and just use the local class
descriptors instead.  This is a simple solution for the problem of persisted
VKeys containing references to classes where the class has been updated and
the serial version UID has changed.

Also add a "replay_txns" command that replays the transactions from a given
start point so we can verify all transactions are deserializable.

TESTED:
    Ran replay_txns against all transactions on sandbox beginning with
    transaction id 1828385, which includes Recurring billing events containing
    both the old and the new serial version UIDs.
This commit is contained in:
Michael Muller 2022-04-27 15:40:27 -04:00 committed by GitHub
parent 103f43465c
commit d61a4671c8
3 changed files with 95 additions and 1 deletions

View file

@ -33,8 +33,10 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
/** /**
* A SQL transaction that can be serialized and stored in its own table. * 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 { 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. // Verify that the data is what we expect.
int version = in.readInt(); int version = in.readInt();
@ -304,4 +307,35 @@ public class Transaction extends ImmutableObject implements Buildable {
} }
} }
} }
/**
* ObjectInputStream that ignores the UIDs of serialized objects.
*
* <p>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.
*
* <p>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;
}
}
} }

View file

@ -104,6 +104,7 @@ public final class RegistryTool {
.put("registrar_contact", RegistrarContactCommand.class) .put("registrar_contact", RegistrarContactCommand.class)
.put("remove_registry_one_key", RemoveRegistryOneKeyCommand.class) .put("remove_registry_one_key", RemoveRegistryOneKeyCommand.class)
.put("renew_domain", RenewDomainCommand.class) .put("renew_domain", RenewDomainCommand.class)
.put("replay_txns", ReplayTxnsCommand.class)
.put("resave_entities", ResaveEntitiesCommand.class) .put("resave_entities", ResaveEntitiesCommand.class)
.put("resave_environment_entities", ResaveEnvironmentEntitiesCommand.class) .put("resave_environment_entities", ResaveEnvironmentEntitiesCommand.class)
.put("resave_epp_resource", ResaveEppResourceCommand.class) .put("resave_epp_resource", ResaveEppResourceCommand.class)

View file

@ -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<TransactionEntity> 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);
}
}