// Copyright 2016 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.model.smd; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Iterables.isEmpty; import static google.registry.model.common.EntityGroupRoot.getCrossTldKey; import static google.registry.model.ofy.ObjectifyService.allocateId; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.util.CacheUtils.memoizeWithShortExpiration; import static google.registry.util.DateTimeUtils.START_OF_TIME; import static google.registry.util.DateTimeUtils.isBeforeOrAt; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Supplier; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.googlecode.objectify.Key; import com.googlecode.objectify.VoidWork; import com.googlecode.objectify.Work; import com.googlecode.objectify.annotation.EmbedMap; import com.googlecode.objectify.annotation.Entity; import com.googlecode.objectify.annotation.Id; import com.googlecode.objectify.annotation.Ignore; import com.googlecode.objectify.annotation.OnSave; import com.googlecode.objectify.annotation.Parent; import google.registry.model.ImmutableObject; import google.registry.model.annotations.NotBackedUp; import google.registry.model.annotations.NotBackedUp.Reason; import google.registry.model.common.EntityGroupRoot; import google.registry.util.CollectionUtils; import java.util.Map; import org.joda.time.DateTime; /** * Signed Mark Data Revocation List (SMDRL). * *

Represents a SMDRL file downloaded from the TMCH MarksDB each day. The list holds the ids of * all the {@link SignedMark SignedMarks} that have been revoked. A new list is created for each new * file that's created, depending on the timestamp. * *

We'll be putting the entire table into a single entity for the sake of performance. But in * order to avoid exceeding the one megabyte max entity size limit, we'll also be sharding that * entity into multiple entities, each entity containing {@value #SHARD_SIZE} rows. * * @see google.registry.tmch.SmdrlCsvParser * @see "http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.2" */ @Entity @NotBackedUp(reason = Reason.EXTERNALLY_SOURCED) public class SignedMarkRevocationList extends ImmutableObject { @VisibleForTesting static final int SHARD_SIZE = 10000; /** Common ancestor for queries. */ @Parent Key parent = getCrossTldKey(); /** ID for the sharded entity. */ @Id long id; /** Time when this list was last updated, as specified in the first line of the CSV file. */ DateTime creationTime; /** A map from SMD IDs to revocation time. */ @EmbedMap Map revokes; /** Indicates that this is a shard rather than a "full" list. */ @Ignore boolean isShard; /** * A cached supplier that fetches the SMDRL shards from the datastore and recombines them into a * single {@link SignedMarkRevocationList} object. */ private static final Supplier CACHE = memoizeWithShortExpiration(new Supplier() { @Override public SignedMarkRevocationList get() { // Open a new transactional read even if we are in a transaction currently. return ofy().transactNewReadOnly(new Work() { @Override public SignedMarkRevocationList run() { Iterable shards = ofy() .load() .type(SignedMarkRevocationList.class) .ancestor(getCrossTldKey()); DateTime creationTime = isEmpty(shards) ? START_OF_TIME : checkNotNull(Iterables.get(shards, 0).creationTime, "creationTime"); ImmutableMap.Builder revokes = new ImmutableMap.Builder<>(); for (SignedMarkRevocationList shard : shards) { revokes.putAll(shard.revokes); checkState( creationTime.equals(shard.creationTime), "Inconsistent creation times: %s vs. %s", creationTime, shard.creationTime); } return create(creationTime, revokes.build()); }}); }}); /** Return a single logical instance that combines all the datastore shards. */ public static SignedMarkRevocationList get() { return CACHE.get(); } /** Create a new {@link SignedMarkRevocationList} without saving it. */ public static SignedMarkRevocationList create( DateTime creationTime, ImmutableMap revokes) { SignedMarkRevocationList instance = new SignedMarkRevocationList(); instance.creationTime = checkNotNull(creationTime, "creationTime"); instance.revokes = checkNotNull(revokes, "revokes"); return instance; } /** Returns {@code true} if the SMD ID has been revoked at the given point in time. */ public boolean isSmdRevoked(String smdId, DateTime now) { DateTime revoked = revokes.get(checkNotNull(smdId, "smdId")); if (revoked == null) { return false; } return isBeforeOrAt(revoked, now); } /** Returns the creation timestamp specified at the top of the SMDRL CSV file. */ public DateTime getCreationTime() { return creationTime; } /** Returns the number of revocations. */ public int size() { return revokes.size(); } /** Save this list to the datastore in sharded form. Returns {@code this}. */ public SignedMarkRevocationList save() { ofy().transact(new VoidWork() { @Override public void vrun() { ofy().deleteWithoutBackup().keys(ofy() .load() .type(SignedMarkRevocationList.class) .ancestor(getCrossTldKey()) .keys()); ofy().saveWithoutBackup().entities(FluentIterable .from(CollectionUtils.partitionMap(revokes, SHARD_SIZE)) .transform(new Function, SignedMarkRevocationList>() { @Override public SignedMarkRevocationList apply(ImmutableMap shardRevokes) { SignedMarkRevocationList shard = create(creationTime, shardRevokes); shard.id = allocateId(); shard.isShard = true; // Avoid the exception in disallowUnshardedSaves(). return shard; }})); }}); return this; } /** As a safety mechanism, fail if someone tries to save this class directly. */ @OnSave void disallowUnshardedSaves() { if (!isShard) { throw new UnshardedSaveException(); } } /** Exception when trying to directly save a {@link SignedMarkRevocationList} without sharding. */ public static class UnshardedSaveException extends RuntimeException {} }