diff --git a/java/google/registry/batch/BatchComponent.java b/java/google/registry/batch/BatchComponent.java deleted file mode 100644 index 9f5c3c3f6..000000000 --- a/java/google/registry/batch/BatchComponent.java +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2017 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.batch; - -import dagger.Component; -import google.registry.bigquery.BigqueryModule; -import google.registry.config.RegistryConfig.ConfigModule; -import google.registry.monitoring.whitebox.WhiteboxModule; -import google.registry.request.Modules.DatastoreServiceModule; -import google.registry.util.SystemSleeper.SystemSleeperModule; -import javax.inject.Singleton; - -/** Dagger component with instance lifetime for batch package. */ -@Singleton -@Component( - modules = { - BatchModule.class, - BigqueryModule.class, - ConfigModule.class, - DatastoreServiceModule.class, - SystemSleeperModule.class, - WhiteboxModule.class - }) -interface BatchComponent { - VerifyEntityIntegrityStreamerFactory verifyEntityIntegrityStreamerFactory(); -} diff --git a/java/google/registry/batch/BatchModule.java b/java/google/registry/batch/BatchModule.java index 7b34122b6..d771f81cb 100644 --- a/java/google/registry/batch/BatchModule.java +++ b/java/google/registry/batch/BatchModule.java @@ -24,14 +24,10 @@ import static google.registry.request.RequestParameters.extractRequiredDatetimeP import static google.registry.request.RequestParameters.extractRequiredParameter; import static google.registry.request.RequestParameters.extractSetOfDatetimeParameters; -import com.google.api.services.bigquery.model.TableFieldSchema; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import dagger.Module; import dagger.Provides; -import dagger.multibindings.IntoMap; -import dagger.multibindings.StringKey; import google.registry.model.ImmutableObject; import google.registry.request.Parameter; import java.util.Optional; @@ -44,13 +40,6 @@ import org.joda.time.DateTime; @Module public class BatchModule { - @Provides - @IntoMap - @StringKey(EntityIntegrityAlertsSchema.TABLE_ID) - static ImmutableList provideEntityIntegrityAlertsSchema() { - return EntityIntegrityAlertsSchema.SCHEMA_FIELDS; - } - @Provides @Parameter("jobName") static Optional provideJobName(HttpServletRequest req) { diff --git a/java/google/registry/batch/EntityIntegrityAlertsSchema.java b/java/google/registry/batch/EntityIntegrityAlertsSchema.java deleted file mode 100644 index cb7f3206e..000000000 --- a/java/google/registry/batch/EntityIntegrityAlertsSchema.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2017 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.batch; - -import static google.registry.bigquery.BigqueryUtils.FieldType.STRING; -import static google.registry.bigquery.BigqueryUtils.FieldType.TIMESTAMP; - -import com.google.api.services.bigquery.model.TableFieldSchema; -import com.google.common.collect.ImmutableList; - -/** The Bigquery schema for the entity integrity alerts table. */ -final class EntityIntegrityAlertsSchema { - - static final String DATASET = "entity_integrity"; - static final String TABLE_ID = "alerts"; - static final String FIELD_SCANTIME = "scanTime"; - static final String FIELD_SOURCE = "source"; - static final String FIELD_TARGET = "target"; - static final String FIELD_MESSAGE = "message"; - - static final ImmutableList SCHEMA_FIELDS = - ImmutableList.of( - new TableFieldSchema().setName(FIELD_SCANTIME).setType(TIMESTAMP.name()), - new TableFieldSchema().setName(FIELD_SOURCE).setType(STRING.name()), - new TableFieldSchema().setName(FIELD_TARGET).setType(STRING.name()), - new TableFieldSchema().setName(FIELD_MESSAGE).setType(STRING.name())); - - private EntityIntegrityAlertsSchema() {} -} - diff --git a/java/google/registry/batch/VerifyEntityIntegrityAction.java b/java/google/registry/batch/VerifyEntityIntegrityAction.java deleted file mode 100644 index 956da4b9a..000000000 --- a/java/google/registry/batch/VerifyEntityIntegrityAction.java +++ /dev/null @@ -1,579 +0,0 @@ -// Copyright 2017 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.batch; - -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.collect.ImmutableSet.toImmutableSet; -import static com.google.common.collect.Iterables.getOnlyElement; -import static com.googlecode.objectify.Key.getKind; -import static google.registry.model.EppResourceUtils.isActive; -import static google.registry.model.ofy.ObjectifyService.ofy; -import static google.registry.request.Action.Method.POST; -import static google.registry.util.DateTimeUtils.END_OF_TIME; -import static google.registry.util.DateTimeUtils.START_OF_TIME; -import static google.registry.util.DateTimeUtils.earliestOf; -import static google.registry.util.DateTimeUtils.isAtOrAfter; -import static google.registry.util.DateTimeUtils.isBeforeOrAt; -import static google.registry.util.PipelineUtils.createJobPath; -import static org.joda.time.DateTimeZone.UTC; - -import com.google.appengine.tools.mapreduce.Input; -import com.google.appengine.tools.mapreduce.Mapper; -import com.google.appengine.tools.mapreduce.Reducer; -import com.google.appengine.tools.mapreduce.ReducerInput; -import com.google.appengine.tools.mapreduce.inputs.DatastoreKeyInput; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; -import com.google.common.flogger.FluentLogger; -import com.googlecode.objectify.Key; -import google.registry.mapreduce.MapreduceRunner; -import google.registry.mapreduce.inputs.EppResourceInputs; -import google.registry.model.EppResource; -import google.registry.model.ImmutableObject; -import google.registry.model.contact.ContactResource; -import google.registry.model.domain.DomainApplication; -import google.registry.model.domain.DomainBase; -import google.registry.model.domain.DomainResource; -import google.registry.model.domain.GracePeriod; -import google.registry.model.host.HostResource; -import google.registry.model.index.DomainApplicationIndex; -import google.registry.model.index.EppResourceIndex; -import google.registry.model.index.ForeignKeyIndex; -import google.registry.model.index.ForeignKeyIndex.ForeignKeyContactIndex; -import google.registry.model.index.ForeignKeyIndex.ForeignKeyDomainIndex; -import google.registry.model.index.ForeignKeyIndex.ForeignKeyHostIndex; -import google.registry.model.transfer.TransferData.TransferServerApproveEntity; -import google.registry.request.Action; -import google.registry.request.Response; -import google.registry.request.auth.Auth; -import google.registry.util.NonFinalForTesting; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.annotation.Nullable; -import javax.inject.Inject; -import org.joda.time.DateTime; - -/** - * A mapreduce to verify integrity of entities in Datastore. - * - *

Specifically this validates all of the following system invariants that are expected to hold - * true for all {@link EppResource} entities and their related indexes: - *

    - *
  • All {@link Key} fields (including nested ones) point to entities that exist. - *
  • There is exactly one {@link EppResourceIndex} pointing to each {@link EppResource}. - *
  • All contacts, hosts, and domains, when grouped by foreign key, have at most one active - * resource, and exactly one {@link ForeignKeyIndex} of the appropriate type, which points to - * the active resource if one exists, or to the most recently deleted resource if not. The - * foreignKey and deletionTime fields on the index must also match the respective resource(s). - *
  • All domain applications, when grouped by foreign key, have exactly one - * {@link DomainApplicationIndex} that links to all of them, and has a matching - * fullyQualifiedDomainName. - *
- */ -@Action( - path = "/_dr/task/verifyEntityIntegrity", - method = POST, - auth = Auth.AUTH_INTERNAL_ONLY -) -public class VerifyEntityIntegrityAction implements Runnable { - - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - private static final int NUM_SHARDS = 200; - @NonFinalForTesting - @VisibleForTesting - static BatchComponent component = DaggerBatchComponent.create(); - private static final ImmutableSet> RESOURCE_CLASSES = - ImmutableSet.of( - ForeignKeyDomainIndex.class, - DomainApplicationIndex.class, - ForeignKeyHostIndex.class, - ForeignKeyContactIndex.class, - DomainBase.class, - HostResource.class, - ContactResource.class); - - static final String KIND_CONTACT_RESOURCE = getKind(ContactResource.class); - static final String KIND_CONTACT_INDEX = getKind(ForeignKeyContactIndex.class); - static final String KIND_DOMAIN_APPLICATION_INDEX = getKind(DomainApplicationIndex.class); - static final String KIND_DOMAIN_BASE_RESOURCE = getKind(DomainBase.class); - static final String KIND_DOMAIN_INDEX = getKind(ForeignKeyDomainIndex.class); - static final String KIND_EPPRESOURCE_INDEX = getKind(EppResourceIndex.class); - static final String KIND_HOST_RESOURCE = getKind(HostResource.class); - static final String KIND_HOST_INDEX = getKind(ForeignKeyHostIndex.class); - - @Inject MapreduceRunner mrRunner; - @Inject Response response; - @Inject VerifyEntityIntegrityAction() {} - - @Override - public void run() { - DateTime scanTime = DateTime.now(UTC); - response.sendJavaScriptRedirect(createJobPath(mrRunner - .setJobName("Verify entity integrity") - .setModuleName("backend") - .setDefaultReduceShards(NUM_SHARDS) - .runMapreduce( - new VerifyEntityIntegrityMapper(scanTime), - new VerifyEntityIntegrityReducer(scanTime), - getInputs()))); - } - - private static ImmutableSet> getInputs() { - ImmutableSet.Builder> builder = - new ImmutableSet.Builder>().add(EppResourceInputs.createIndexInput()); - RESOURCE_CLASSES - .stream() - .map(clazz -> new DatastoreKeyInput(getKind(clazz), NUM_SHARDS)) - .forEach(builder::add); - return builder.build(); - } - - /** - * The mapreduce key that the mapper outputs. Each {@link EppResource} has two different - * mapreduce keys that are output for it: one for its specific type (domain, application, host, or - * contact), which is used to check {@link ForeignKeyIndex} constraints, and one that is common - * for all EppResources, to check {@link EppResourceIndex} constraints. - */ - private enum EntityKind { - DOMAIN, - APPLICATION, - CONTACT, - HOST, - /** - * Used to verify 1-to-1 constraints between all types of EPP resources and their indexes. - */ - EPP_RESOURCE - } - - private static class MapperKey implements Serializable { - - private static final long serialVersionUID = 3222302549441420932L; - - /** - * The relevant id for this mapper key, which is either the foreign key of the EppResource (for - * verifying foreign key indexes) or its repoId (for verifying EppResourceIndexes). - */ - public String id; - public EntityKind kind; - - public static MapperKey create(EntityKind kind, String id) { - MapperKey instance = new MapperKey(); - instance.kind = kind; - instance.id = id; - return instance; - } - } - - /** - * Mapper that checks validity of references on all resources and outputs key/value pairs used to - * check integrity of foreign key entities. - */ - public static class VerifyEntityIntegrityMapper - extends Mapper> { - - private static final long serialVersionUID = -5413882340475018051L; - private final DateTime scanTime; - - private transient VerifyEntityIntegrityStreamer integrityStreamer; - - // The integrityStreamer field must be marked as transient so that instances of the Mapper class - // can be serialized by the MapReduce framework. Thus, every time is used, lazily construct it - // if it doesn't exist yet. - private VerifyEntityIntegrityStreamer integrity() { - if (integrityStreamer == null) { - integrityStreamer = component.verifyEntityIntegrityStreamerFactory().create(scanTime); - } - return integrityStreamer; - } - - public VerifyEntityIntegrityMapper(DateTime scanTime) { - this.scanTime = scanTime; - } - - @Override - public final void map(Object keyOrEntity) { - try { - // We use different inputs, some that return keys and some that return entities. Load any - // keys that we get so we're dealing only with entities. - if (keyOrEntity instanceof com.google.appengine.api.datastore.Key) { - Key key = Key.create((com.google.appengine.api.datastore.Key) keyOrEntity); - keyOrEntity = ofy().load().key(key).now(); - } - mapEntity(keyOrEntity); - } catch (Throwable e) { - // Log and swallow so that the mapreduce doesn't abort on first error. - logger.atSevere().withCause(e).log( - "Exception while checking integrity of entity: %s", keyOrEntity); - } - } - - private void mapEntity(Object entity) { - if (entity instanceof EppResource) { - mapEppResource((EppResource) entity); - } else if (entity instanceof ForeignKeyIndex) { - mapForeignKeyIndex((ForeignKeyIndex) entity); - } else if (entity instanceof DomainApplicationIndex) { - mapDomainApplicationIndex((DomainApplicationIndex) entity); - } else if (entity instanceof EppResourceIndex) { - mapEppResourceIndex((EppResourceIndex) entity); - } else { - throw new IllegalStateException( - String.format("Unknown entity in integrity mapper: %s", entity)); - } - } - - private void mapEppResource(EppResource resource) { - emit(MapperKey.create(EntityKind.EPP_RESOURCE, resource.getRepoId()), Key.create(resource)); - if (resource instanceof DomainBase) { - DomainBase domainBase = (DomainBase) resource; - Key key = Key.create(domainBase); - verifyExistence(key, domainBase.getReferencedContacts()); - verifyExistence(key, domainBase.getNameservers()); - if (domainBase instanceof DomainApplication) { - getContext().incrementCounter("domain applications"); - DomainApplication application = (DomainApplication) domainBase; - emit( - MapperKey.create(EntityKind.APPLICATION, application.getFullyQualifiedDomainName()), - Key.create(application)); - } else if (domainBase instanceof DomainResource) { - getContext().incrementCounter("domain resources"); - DomainResource domain = (DomainResource) domainBase; - verifyExistence(key, domain.getTransferData().getServerApproveAutorenewEvent()); - verifyExistence(key, domain.getTransferData().getServerApproveAutorenewPollMessage()); - verifyExistence(key, domain.getTransferData().getServerApproveBillingEvent()); - verifyExistence( - key, - domain - .getTransferData() - .getServerApproveEntities() - .stream() - .map(VerifyEntityIntegrityMapper::castTransferServerApproveEntityKey) - .collect(toImmutableSet())); - verifyExistence(key, domain.getApplication()); - verifyExistence(key, domain.getAutorenewBillingEvent()); - for (GracePeriod gracePeriod : domain.getGracePeriods()) { - verifyExistence(key, gracePeriod.getOneTimeBillingEvent()); - verifyExistence(key, gracePeriod.getRecurringBillingEvent()); - } - emit( - MapperKey.create(EntityKind.DOMAIN, domain.getFullyQualifiedDomainName()), - Key.create(domain)); - } - } else if (resource instanceof ContactResource) { - getContext().incrementCounter("contact resources"); - ContactResource contact = (ContactResource) resource; - emit( - MapperKey.create(EntityKind.CONTACT, contact.getContactId()), - Key.create(contact)); - } else if (resource instanceof HostResource) { - getContext().incrementCounter("host resources"); - HostResource host = (HostResource) resource; - verifyExistence(Key.create(host), host.getSuperordinateDomain()); - emit( - MapperKey.create(EntityKind.HOST, host.getFullyQualifiedHostName()), - Key.create(host)); - } else { - throw new IllegalStateException( - String.format("EppResource with unknown type in integrity mapper: %s", resource)); - } - } - - @SuppressWarnings("unchecked") - private static Key castTransferServerApproveEntityKey( - Key key) { - return (Key) key; - } - - private void mapForeignKeyIndex(ForeignKeyIndex fki) { - Key> fkiKey = Key.create(fki); - @SuppressWarnings("cast") - EppResource resource = verifyExistence(fkiKey, fki.getResourceKey()); - if (resource != null) { - // TODO(user): Traverse the chain of pointers to old FKIs instead once they are written. - if (isAtOrAfter(fki.getDeletionTime(), resource.getDeletionTime())) { - integrity().check( - fki.getForeignKey().equals(resource.getForeignKey()), - fkiKey, - Key.create(resource), - "Foreign key index points to EppResource with different foreign key"); - } - } - if (fki instanceof ForeignKeyDomainIndex) { - getContext().incrementCounter("domain foreign key indexes"); - emit(MapperKey.create(EntityKind.DOMAIN, fki.getForeignKey()), fkiKey); - } else if (fki instanceof ForeignKeyContactIndex) { - getContext().incrementCounter("contact foreign key indexes"); - emit(MapperKey.create(EntityKind.CONTACT, fki.getForeignKey()), fkiKey); - } else if (fki instanceof ForeignKeyHostIndex) { - getContext().incrementCounter("host foreign key indexes"); - emit(MapperKey.create(EntityKind.HOST, fki.getForeignKey()), fkiKey); - } else { - throw new IllegalStateException( - String.format("Foreign key index is of unknown type: %s", fki)); - } - } - - private void mapDomainApplicationIndex(DomainApplicationIndex dai) { - getContext().incrementCounter("domain application indexes"); - Key daiKey = Key.create(dai); - for (Key key : dai.getKeys()) { - DomainApplication application = verifyExistence(daiKey, key); - if (application != null) { - integrity().check( - dai.getFullyQualifiedDomainName().equals(application.getFullyQualifiedDomainName()), - daiKey, - Key.create(application), - "Domain application index points to application with different domain name"); - } - emit( - MapperKey.create(EntityKind.APPLICATION, dai.getFullyQualifiedDomainName()), - daiKey); - } - } - - private void mapEppResourceIndex(EppResourceIndex eri) { - Key eriKey = Key.create(eri); - String eriRepoId = Key.create(eri.getId()).getName(); - integrity().check( - eriRepoId.equals(eri.getKey().getName()), - eriKey, - eri.getKey(), - "EPP resource index id does not match repoId of reference"); - verifyExistence(eriKey, eri.getKey()); - emit(MapperKey.create(EntityKind.EPP_RESOURCE, eriRepoId), eriKey); - getContext().incrementCounter("EPP resource indexes to " + eri.getKind()); - } - - private void verifyExistence(Key source, Set> targets) { - Set> missingEntityKeys = - Sets.difference(targets, ofy().load().keys(targets).keySet()); - integrity().checkOneToMany( - missingEntityKeys.isEmpty(), - source, - targets, - "Target entity does not exist"); - } - - @Nullable - private E verifyExistence(Key source, @Nullable Key target) { - if (target == null) { - return null; - } - E entity = ofy().load().key(target).now(); - integrity().check(entity != null, source, target, "Target entity does not exist"); - return entity; - } - } - - /** Reducer that checks integrity of foreign key entities. */ - public static class VerifyEntityIntegrityReducer - extends Reducer, Void> { - - private static final long serialVersionUID = -151271247606894783L; - - private final DateTime scanTime; - - private transient VerifyEntityIntegrityStreamer integrityStreamer; - - // The integrityStreamer field must be marked as transient so that instances of the Reducer - // class can be serialized by the MapReduce framework. Thus, every time is used, lazily - // construct it if it doesn't exist yet. - private VerifyEntityIntegrityStreamer integrity() { - if (integrityStreamer == null) { - integrityStreamer = component.verifyEntityIntegrityStreamerFactory().create(scanTime); - } - return integrityStreamer; - } - - public VerifyEntityIntegrityReducer(DateTime scanTime) { - this.scanTime = scanTime; - } - - @SuppressWarnings("unchecked") - @Override - public void reduce(MapperKey mapperKey, ReducerInput> keys) { - try { - reduceKeys(mapperKey, keys); - } catch (Throwable e) { - // Log and swallow so that the mapreduce doesn't abort on first error. - logger.atSevere().withCause(e).log( - "Exception while checking foreign key integrity constraints for: %s", mapperKey); - } - } - - private void reduceKeys( - MapperKey mapperKey, ReducerInput> keys) { - getContext().incrementCounter("reduced resources " + mapperKey.kind); - switch (mapperKey.kind) { - case EPP_RESOURCE: - checkEppResourceIndexes(keys, mapperKey.id); - break; - case APPLICATION: - checkIndexes( - keys, - mapperKey.id, - KIND_DOMAIN_BASE_RESOURCE, - KIND_DOMAIN_APPLICATION_INDEX, - false); - break; - case CONTACT: - checkIndexes(keys, mapperKey.id, KIND_CONTACT_RESOURCE, KIND_CONTACT_INDEX, true); - break; - case DOMAIN: - checkIndexes( - keys, mapperKey.id, KIND_DOMAIN_BASE_RESOURCE, KIND_DOMAIN_INDEX, true); - break; - case HOST: - checkIndexes(keys, mapperKey.id, KIND_HOST_RESOURCE, KIND_HOST_INDEX, true); - break; - default: - throw new IllegalStateException( - String.format("Unknown type of foreign key %s", mapperKey.kind)); - } - } - - @SuppressWarnings("unchecked") - private void checkEppResourceIndexes( - Iterator> keys, String repoId) { - List> resources = new ArrayList<>(); - List> eppResourceIndexes = new ArrayList<>(); - while (keys.hasNext()) { - Key key = keys.next(); - String kind = key.getKind(); - if (kind.equals(KIND_EPPRESOURCE_INDEX)) { - eppResourceIndexes.add((Key) key); - } else if (kind.equals(KIND_DOMAIN_BASE_RESOURCE) - || kind.equals(KIND_CONTACT_RESOURCE) - || kind.equals(KIND_HOST_RESOURCE)) { - resources.add((Key) key); - } else { - throw new IllegalStateException( - String.format( - "While verifying EppResourceIndexes for repoId %s, found key of unknown type: %s", - repoId, - key)); - } - } - // This is a checkState and not an integrity check because the Datastore schema ensures that - // there can't be multiple EppResources with the same repoId. - checkState( - resources.size() == 1, - String.format("Found more than one EppResource for repoId %s: %s", repoId, resources)); - if (integrity().check( - !eppResourceIndexes.isEmpty(), - null, - getOnlyElement(resources), - "Missing EPP resource index for EPP resource")) { - integrity().checkManyToOne( - eppResourceIndexes.size() == 1, - eppResourceIndexes, - getOnlyElement(resources), - "Duplicate EPP resource indexes pointing to same resource"); - } - } - - @SuppressWarnings("unchecked") - private void checkIndexes( - Iterator> keys, - String foreignKey, - String resourceKind, - String foreignKeyIndexKind, - boolean thereCanBeOnlyOne) { - List> resources = new ArrayList<>(); - List> foreignKeyIndexes = new ArrayList<>(); - while (keys.hasNext()) { - Key key = keys.next(); - if (key.getKind().equals(resourceKind)) { - resources.add((Key) key); - } else if (key.getKind().equals(foreignKeyIndexKind)) { - foreignKeyIndexes.add((Key) key); - } else { - throw new IllegalStateException( - String.format( - "While processing links to foreign key %s of type %s, found unknown key: %s", - foreignKey, - resourceKind, - key)); - } - } - // This is a checkState and not an integrity check because it should truly be impossible to - // have multiple foreign key indexes for the same foreign key because of the Datastore schema. - checkState( - foreignKeyIndexes.size() <= 1, - String.format( - "Found more than one foreign key index for %s: %s", foreignKey, foreignKeyIndexes)); - integrity().check( - !foreignKeyIndexes.isEmpty(), - foreignKey, - resourceKind, - "Missing foreign key index for EppResource"); - // Skip the case where no resources were found because entity exceptions are already thrown in - // the mapper in invalid situations where FKIs point to non-existent entities. - if (thereCanBeOnlyOne && !resources.isEmpty()) { - verifyOnlyOneActiveResource(resources, getOnlyElement(foreignKeyIndexes)); - } - } - - private void verifyOnlyOneActiveResource( - List> resources, Key fkiKey) { - DateTime oldestActive = END_OF_TIME; - DateTime mostRecentInactive = START_OF_TIME; - Key mostRecentInactiveKey = null; - List> activeResources = new ArrayList<>(); - Map, R> allResources = ofy().load().keys(resources); - ForeignKeyIndex fki = (ForeignKeyIndex) ofy().load().key(fkiKey).now(); - for (Map.Entry, R> entry : allResources.entrySet()) { - R resource = entry.getValue(); - if (isActive(resource, scanTime)) { - activeResources.add(entry.getKey()); - oldestActive = earliestOf(oldestActive, resource.getCreationTime()); - } else { - if (resource.getDeletionTime().isAfter(mostRecentInactive)) { - mostRecentInactive = resource.getDeletionTime(); - mostRecentInactiveKey = entry.getKey(); - } - } - } - if (activeResources.isEmpty()) { - integrity().check( - fki.getDeletionTime().isEqual(mostRecentInactive), - fkiKey, - mostRecentInactiveKey, - "Foreign key index deletion time not equal to that of most recently deleted resource"); - } else { - integrity().checkOneToMany( - activeResources.size() == 1, - fkiKey, - activeResources, - "Multiple active EppResources with same foreign key"); - integrity().check( - fki.getDeletionTime().isEqual(END_OF_TIME), - fkiKey, - null, - "Foreign key index has deletion time but active resource exists"); - integrity().check( - isBeforeOrAt(mostRecentInactive, oldestActive), - fkiKey, - mostRecentInactiveKey, - "Found inactive resource deleted more recently than when active resource was created"); - } - } - } -} diff --git a/java/google/registry/batch/VerifyEntityIntegrityStreamer.java b/java/google/registry/batch/VerifyEntityIntegrityStreamer.java deleted file mode 100644 index 74936e649..000000000 --- a/java/google/registry/batch/VerifyEntityIntegrityStreamer.java +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2017 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.batch; - -import static com.google.api.client.util.Data.NULL_STRING; -import static google.registry.batch.EntityIntegrityAlertsSchema.DATASET; -import static google.registry.batch.EntityIntegrityAlertsSchema.FIELD_MESSAGE; -import static google.registry.batch.EntityIntegrityAlertsSchema.FIELD_SCANTIME; -import static google.registry.batch.EntityIntegrityAlertsSchema.FIELD_SOURCE; -import static google.registry.batch.EntityIntegrityAlertsSchema.FIELD_TARGET; -import static google.registry.batch.EntityIntegrityAlertsSchema.TABLE_ID; -import static java.util.stream.Collectors.joining; - -import com.google.api.services.bigquery.Bigquery; -import com.google.api.services.bigquery.Bigquery.Tabledata.InsertAll; -import com.google.api.services.bigquery.model.TableDataInsertAllRequest; -import com.google.api.services.bigquery.model.TableDataInsertAllRequest.Rows; -import com.google.api.services.bigquery.model.TableDataInsertAllResponse; -import com.google.auto.factory.AutoFactory; -import com.google.auto.factory.Provided; -import com.google.common.base.Supplier; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import google.registry.bigquery.BigqueryFactory; -import google.registry.config.RegistryConfig.Config; -import google.registry.util.Retrier; -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Callable; -import javax.annotation.Nullable; -import javax.inject.Named; -import org.joda.time.DateTime; - -/** - * An injected utility class used to check entity integrity and stream violations to BigQuery. - */ -@AutoFactory(allowSubclasses = true) -public class VerifyEntityIntegrityStreamer { - - private final String projectId; - private final BigqueryFactory bigqueryFactory; - private final Supplier idGenerator; - private final Retrier retrier; - private final DateTime scanTime; - private Bigquery bigquery; - - public VerifyEntityIntegrityStreamer( - @Provided @Config("projectId") String projectId, - @Provided BigqueryFactory bigqueryFactory, - @Provided Retrier retrier, - @Provided @Named("insertIdGenerator") Supplier idGenerator, - DateTime scanTime) { - this.projectId = projectId; - this.bigqueryFactory = bigqueryFactory; - this.retrier = retrier; - this.idGenerator = idGenerator; - this.scanTime = scanTime; - } - - // This is lazily loaded so we only construct the connection when needed, as in a healthy - // Datastore almost every single check should short-circuit return and won't output anything to - // BigQuery. - private Bigquery getBigquery() throws IOException { - if (bigquery == null) { - bigquery = bigqueryFactory.create(projectId, DATASET, TABLE_ID); - } - return bigquery; - } - - /** - * Check that the given conditional holds, and if not, stream the supplied source, target, and - * message information to BigQuery. - * - * @return Whether the check succeeded. - */ - boolean check( - boolean conditional, - @Nullable Object source, - @Nullable Object target, - @Nullable String message) { - return checkOneToMany( - conditional, source, ImmutableList.of((target == null) ? NULL_STRING : target), message); - } - - /** - * Check that the given conditional holds, and if not, stream a separate row to BigQuery for each - * supplied target (the source and message will be the same for each). - * - * @return Whether the check succeeded. - */ - boolean checkOneToMany( - boolean conditional, - @Nullable Object source, - Iterable targets, - @Nullable String message) { - return checkManyToMany( - conditional, ImmutableList.of((source == null) ? NULL_STRING : source), targets, message); - } - - /** - * Check that the given conditional holds, and if not, stream a separate row to BigQuery for each - * supplied target (the source and message will be the same for each). - * - * @return Whether the check succeeded. - */ - boolean checkManyToOne( - boolean conditional, - Iterable sources, - @Nullable Object target, - @Nullable String message) { - return checkManyToMany( - conditional, sources, ImmutableList.of((target == null) ? NULL_STRING : target), message); - } - - - /** - * Check that the given conditional holds, and if not, stream a separate row to BigQuery for the - * cross product of every supplied target and source (the message will be the same for each). - * This is used in preference to records (repeated fields) in BigQuery because they are - * significantly harder to work with. - * - * @return Whether the check succeeded. - */ - private boolean checkManyToMany( - boolean conditional, - Iterable sources, - Iterable targets, - @Nullable String message) { - if (conditional) { - return true; - } - ImmutableList.Builder rows = new ImmutableList.Builder<>(); - for (S source : sources) { - for (T target : targets) { - Map rowData = - new ImmutableMap.Builder() - .put( - FIELD_SCANTIME, - new com.google.api.client.util.DateTime(scanTime.toDate())) - .put( - FIELD_SOURCE, - source.toString()) - .put( - FIELD_TARGET, - target.toString()) - .put( - FIELD_MESSAGE, - (message == null) ? NULL_STRING : message) - .build(); - rows.add( - new TableDataInsertAllRequest.Rows().setJson(rowData).setInsertId(idGenerator.get())); - } - } - streamToBigqueryWithRetry(rows.build()); - return false; - } - - private void streamToBigqueryWithRetry(List rows) { - try { - final InsertAll request = - getBigquery() - .tabledata() - .insertAll( - projectId, - DATASET, - TABLE_ID, - new TableDataInsertAllRequest().setRows(rows)); - - Callable callable = - () -> { - TableDataInsertAllResponse response = request.execute(); - // Turn errors on the response object into RuntimeExceptions that the retrier will - // retry. - if (response.getInsertErrors() != null && !response.getInsertErrors().isEmpty()) { - throw new RuntimeException( - response - .getInsertErrors() - .stream() - .map( - error -> { - try { - return error.toPrettyString(); - } catch (IOException e) { - return error.toString(); - } - }) - .collect(joining("\n"))); - } - return null; - }; - retrier.callWithRetry(callable, RuntimeException.class); - } catch (IOException e) { - throw new RuntimeException("Error sending integrity error to BigQuery", e); - } - } -} - diff --git a/java/google/registry/env/common/backend/WEB-INF/web.xml b/java/google/registry/env/common/backend/WEB-INF/web.xml index c00deae46..2092c4782 100644 --- a/java/google/registry/env/common/backend/WEB-INF/web.xml +++ b/java/google/registry/env/common/backend/WEB-INF/web.xml @@ -174,12 +174,6 @@ /_dr/dnsRefresh - - - backend-servlet - /_dr/task/verifyEntityIntegrity - - backend-servlet diff --git a/java/google/registry/env/sandbox/default/WEB-INF/cron.xml b/java/google/registry/env/sandbox/default/WEB-INF/cron.xml index 557f079f7..9ed16a801 100644 --- a/java/google/registry/env/sandbox/default/WEB-INF/cron.xml +++ b/java/google/registry/env/sandbox/default/WEB-INF/cron.xml @@ -210,17 +210,4 @@ backend - - diff --git a/java/google/registry/module/backend/BackendRequestComponent.java b/java/google/registry/module/backend/BackendRequestComponent.java index 14468c152..0b5df3532 100644 --- a/java/google/registry/module/backend/BackendRequestComponent.java +++ b/java/google/registry/module/backend/BackendRequestComponent.java @@ -28,7 +28,6 @@ import google.registry.batch.ExpandRecurringBillingEventsAction; import google.registry.batch.RefreshDnsOnHostRenameAction; import google.registry.batch.ResaveAllEppResourcesAction; import google.registry.batch.ResaveEntityAction; -import google.registry.batch.VerifyEntityIntegrityAction; import google.registry.cron.CommitLogFanoutAction; import google.registry.cron.CronModule; import google.registry.cron.TldFanoutAction; @@ -163,7 +162,6 @@ interface BackendRequestComponent { TmchSmdrlAction tmchSmdrlAction(); UpdateSnapshotViewAction updateSnapshotViewAction(); PublishInvoicesAction uploadInvoicesAction(); - VerifyEntityIntegrityAction verifyEntityIntegrityAction(); @Subcomponent.Builder abstract class Builder implements RequestComponentBuilder { diff --git a/javatests/google/registry/batch/VerifyEntityIntegrityActionTest.java b/javatests/google/registry/batch/VerifyEntityIntegrityActionTest.java deleted file mode 100644 index affcdaba8..000000000 --- a/javatests/google/registry/batch/VerifyEntityIntegrityActionTest.java +++ /dev/null @@ -1,317 +0,0 @@ -// Copyright 2017 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.batch; - -import static com.google.common.truth.Truth.assertThat; -import static google.registry.testing.DatastoreHelper.createTld; -import static google.registry.testing.DatastoreHelper.deleteResource; -import static google.registry.testing.DatastoreHelper.newContactResource; -import static google.registry.testing.DatastoreHelper.newDomainResource; -import static google.registry.testing.DatastoreHelper.persistActiveContact; -import static google.registry.testing.DatastoreHelper.persistActiveDomain; -import static google.registry.testing.DatastoreHelper.persistActiveHost; -import static google.registry.testing.DatastoreHelper.persistDomainAsDeleted; -import static google.registry.testing.DatastoreHelper.persistResource; -import static google.registry.testing.DatastoreHelper.persistSimpleResource; -import static org.joda.time.DateTimeZone.UTC; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -import com.google.api.client.util.Data; -import com.google.api.services.bigquery.Bigquery; -import com.google.api.services.bigquery.model.TableDataInsertAllRequest; -import com.google.api.services.bigquery.model.TableDataInsertAllRequest.Rows; -import com.google.api.services.bigquery.model.TableDataInsertAllResponse; -import com.google.common.base.Suppliers; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.googlecode.objectify.Key; -import google.registry.bigquery.BigqueryFactory; -import google.registry.mapreduce.MapreduceRunner; -import google.registry.model.contact.ContactResource; -import google.registry.model.domain.DomainResource; -import google.registry.model.host.HostResource; -import google.registry.model.index.EppResourceIndex; -import google.registry.model.index.ForeignKeyIndex; -import google.registry.testing.FakeClock; -import google.registry.testing.FakeResponse; -import google.registry.testing.FakeSleeper; -import google.registry.testing.InjectRule; -import google.registry.testing.mapreduce.MapreduceTestCase; -import google.registry.util.Retrier; -import java.util.Map; -import java.util.Optional; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.ArgumentCaptor; - -/** Unit tests for {@link VerifyEntityIntegrityAction}. */ -@RunWith(JUnit4.class) -public class VerifyEntityIntegrityActionTest - extends MapreduceTestCase { - - @Rule - public final InjectRule inject = new InjectRule(); - - private VerifyEntityIntegrityStreamer integrity; - private ArgumentCaptor rowsCaptor; - private final DateTime now = DateTime.parse("2012-01-02T03:04:05Z"); - - private final Bigquery bigquery = mock(Bigquery.class); - private final Bigquery.Tabledata bigqueryTableData = mock(Bigquery.Tabledata.class); - private final Bigquery.Tabledata.InsertAll bigqueryInsertAll = - mock(Bigquery.Tabledata.InsertAll.class); - private final BigqueryFactory bigqueryFactory = mock(BigqueryFactory.class); - private final VerifyEntityIntegrityStreamerFactory streamerFactory = - mock(VerifyEntityIntegrityStreamerFactory.class); - - @Before - public void before() throws Exception { - createTld("tld"); - - action = new VerifyEntityIntegrityAction(); - action.mrRunner = new MapreduceRunner(Optional.of(2), Optional.of(2)); - action.response = new FakeResponse(); - BatchComponent component = mock(BatchComponent.class); - inject.setStaticField(VerifyEntityIntegrityAction.class, "component", component); - integrity = - new VerifyEntityIntegrityStreamer( - "project-id", - bigqueryFactory, - new Retrier(new FakeSleeper(new FakeClock()), 1), - Suppliers.ofInstance("rowid"), - now); - when(bigqueryFactory.create(anyString(), anyString(), anyString())).thenReturn(bigquery); - when(component.verifyEntityIntegrityStreamerFactory()).thenReturn(streamerFactory); - when(streamerFactory.create(any(DateTime.class))).thenReturn(integrity); - when(bigquery.tabledata()).thenReturn(bigqueryTableData); - rowsCaptor = ArgumentCaptor.forClass(TableDataInsertAllRequest.class); - when(bigqueryTableData.insertAll(anyString(), anyString(), anyString(), rowsCaptor.capture())) - .thenReturn(bigqueryInsertAll); - when(bigqueryInsertAll.execute()).thenReturn(new TableDataInsertAllResponse()); - - } - - private void runMapreduce() throws Exception { - action.run(); - executeTasksUntilEmpty("mapreduce"); - } - - @Test - public void test_singleDomain_noBadInvariants() throws Exception { - persistActiveDomain("ninetails.tld"); - runMapreduce(); - verifyZeroInteractions(bigquery); - } - - @Test - public void test_lotsOfData_noBadInvariants() throws Exception { - createTld("march"); - ContactResource contact = persistActiveContact("longbottom"); - persistResource(newDomainResource("ninetails.tld", contact)); - persistResource(newDomainResource("tentails.tld", contact)); - persistDomainAsDeleted(newDomainResource("long.march", contact), now.minusMonths(4)); - persistResource( - newDomainResource("long.march", contact) - .asBuilder() - .setCreationTimeForTest(now.minusMonths(3)) - .build()); - DateTime now = DateTime.now(UTC); - persistResource( - newContactResource("ricketycricket") - .asBuilder() - .setCreationTimeForTest(now.minusDays(10)) - .setDeletionTime(now.minusDays(9)) - .build()); - persistResource( - newContactResource("ricketycricket") - .asBuilder() - .setCreationTimeForTest(now.minusDays(7)) - .setDeletionTime(now.minusDays(6)) - .build()); - persistResource( - newContactResource("ricketycricket") - .asBuilder() - .setCreationTimeForTest(now.minusDays(4)) - .setDeletionTime(now.minusDays(3)) - .build()); - persistResource( - newContactResource("ricketycricket") - .asBuilder() - .setCreationTimeForTest(now.minusDays(1)) - .build()); - persistActiveHost("ns9001.example.net"); - runMapreduce(); - verifyZeroInteractions(bigquery); - } - - @Test - public void test_missingFki() throws Exception { - persistActiveDomain("ninetails.tld"); - ForeignKeyIndex fki = - ForeignKeyIndex.load(DomainResource.class, "ninetails.tld", DateTime.now(DateTimeZone.UTC)); - deleteResource(fki); - runMapreduce(); - assertIntegrityErrors(IntegrityError.create( - "ninetails.tld", "DomainBase", "Missing foreign key index for EppResource")); - } - - @Test - public void test_missingEppResourceIndex() throws Exception { - Key cooperKey = Key.create(persistActiveContact("cooper")); - deleteResource(EppResourceIndex.create(cooperKey)); - runMapreduce(); - assertIntegrityErrors(IntegrityError.create( - Data.NULL_STRING, cooperKey, "Missing EPP resource index for EPP resource")); - } - - @Test - public void test_referencesToHostsThatDontExist() throws Exception { - Key missingHost1 = Key.create(HostResource.class, "DEADBEEF-ROID"); - Key missingHost2 = Key.create(HostResource.class, "123ABC-ROID"); - Key missingHost3 = Key.create(HostResource.class, "FADDACA-ROID"); - DomainResource domain = - persistResource( - newDomainResource("blah.tld") - .asBuilder() - .setNameservers(ImmutableSet.of(missingHost1, missingHost2, missingHost3)) - .build()); - Key domainKey = Key.create(domain); - runMapreduce(); - assertIntegrityErrors( - IntegrityError.create(domainKey, missingHost1, "Target entity does not exist"), - IntegrityError.create(domainKey, missingHost2, "Target entity does not exist"), - IntegrityError.create(domainKey, missingHost3, "Target entity does not exist")); - } - - @Test - public void test_overlappingActivePeriods() throws Exception { - ContactResource contact123 = persistActiveContact("contact123"); - // These two have overlapping active periods because they will have both been created at - // START_OF_TIME. - DomainResource domain1 = - persistDomainAsDeleted(newDomainResource("penny.tld", contact123), now.minusYears(2)); - DomainResource domain2 = persistActiveDomain("penny.tld"); - runMapreduce(); - assertIntegrityErrors( - IntegrityError.create( - ForeignKeyIndex.createKey(domain2), - Key.create(domain1), - "Found inactive resource deleted more recently than when active resource was created")); - } - - @Test - public void test_multipleActiveContactsWithSameContactId() throws Exception { - ContactResource contact1 = persistActiveContact("dupeid"); - ContactResource contact2 = persistActiveContact("dupeid"); - runMapreduce(); - assertIntegrityErrors( - IntegrityError.create( - ForeignKeyIndex.createKey(contact1), - Key.create(contact1), - "Multiple active EppResources with same foreign key"), - IntegrityError.create( - ForeignKeyIndex.createKey(contact2), - Key.create(contact2), - "Multiple active EppResources with same foreign key")); - } - - @Test - public void test_deletedFkiWithOldHostnameIsntAnError() throws Exception { - HostResource hostLosing = persistActiveHost("losing.example.tld"); - persistResource( - hostLosing.asBuilder().setFullyQualifiedHostName("gaining.example.tld").build()); - persistSimpleResource( - ForeignKeyIndex.create(hostLosing, DateTime.parse("2010-01-01T00:00:00Z"))); - // The old FKI with a primary key of "losing.example.tld" still exists, and it points to the - // resource which has a hostname of "gaining.example.tld", but this isn't an error because the - // FKI is soft-deleted. - runMapreduce(); - verifyZeroInteractions(bigquery); - } - - @Test - public void test_fkiWithDifferentHostnameDeletedMoreRecentlyIsAnError() throws Exception { - HostResource hostLosing = persistActiveHost("losing.example.tld"); - HostResource hostGaining = persistResource( - hostLosing - .asBuilder() - .setFullyQualifiedHostName("gaining.example.tld") - .setDeletionTime(DateTime.parse("2013-01-01T00:00:00Z")) - .build()); - ForeignKeyIndex fki = persistSimpleResource( - ForeignKeyIndex.create(hostLosing, DateTime.parse("2014-01-01T00:00:00Z"))); - // The old FKI is pointing to the old name but has a deletion time more recent than the deleted - // host, so it should have the same foreign key. - runMapreduce(); - assertIntegrityErrors( - IntegrityError.create( - Key.create(fki), - Key.create(hostGaining), - "Foreign key index points to EppResource with different foreign key")); - } - - /** Encapsulates the data representing a single integrity error. */ - private static class IntegrityError { - String source; - String target; - String message; - - static IntegrityError create(Object source, Object target, String message) { - IntegrityError instance = new IntegrityError(); - instance.source = source.toString(); - instance.target = target.toString(); - instance.message = message; - return instance; - } - - /** - * Returns a Map representing the JSON blob corresponding to the BigQuery output for this - * integrity violation at the given scan time. - */ - Map toMap(DateTime scanTime) { - return new ImmutableMap.Builder() - .put("scanTime", new com.google.api.client.util.DateTime(scanTime.toDate())) - .put("source", source) - .put("target", target) - .put("message", message) - .build(); - } - - private IntegrityError() {} - } - - /** Asserts that the given integrity errors, and no others, were logged to BigQuery. */ - private void assertIntegrityErrors(IntegrityError... errors) { - ImmutableList.Builder expected = new ImmutableList.Builder<>(); - for (IntegrityError error : errors) { - expected.add(new Rows().setInsertId("rowid").setJson(error.toMap(now))); - } - ImmutableList.Builder allRows = new ImmutableList.Builder<>(); - for (TableDataInsertAllRequest req : rowsCaptor.getAllValues()) { - allRows.addAll(req.getRows()); - } - assertThat(allRows.build()).containsExactlyElementsIn(expected.build()); - } -} diff --git a/javatests/google/registry/module/backend/testdata/backend_routing.txt b/javatests/google/registry/module/backend/testdata/backend_routing.txt index a0538b097..4f1e88ee7 100644 --- a/javatests/google/registry/module/backend/testdata/backend_routing.txt +++ b/javatests/google/registry/module/backend/testdata/backend_routing.txt @@ -45,4 +45,3 @@ PATH CLASS METHOD /_dr/task/tmchDnl TmchDnlAction POST y INTERNAL APP IGNORED /_dr/task/tmchSmdrl TmchSmdrlAction POST y INTERNAL APP IGNORED /_dr/task/updateSnapshotView UpdateSnapshotViewAction POST n INTERNAL APP IGNORED -/_dr/task/verifyEntityIntegrity VerifyEntityIntegrityAction POST n INTERNAL APP IGNORED diff --git a/javatests/google/registry/monitoring/whitebox/BUILD b/javatests/google/registry/monitoring/whitebox/BUILD index d5816da40..c831605a6 100644 --- a/javatests/google/registry/monitoring/whitebox/BUILD +++ b/javatests/google/registry/monitoring/whitebox/BUILD @@ -34,7 +34,6 @@ java_library( GenTestRules( name = "GeneratedTestRules", - medium_tests = ["VerifyEntityIntegrityActionTest"], test_files = glob(["*Test.java"]), deps = [":whitebox"], )