package org.atlasapi.messaging.v3; import java.math.BigInteger; import java.util.Optional; import java.util.UUID; import java.util.stream.StreamSupport; import org.atlasapi.media.entity.Content; import org.atlasapi.media.entity.LookupRef; import org.atlasapi.persistence.lookup.entry.LookupEntry; import org.atlasapi.persistence.lookup.entry.LookupEntryStore; import com.metabroadcast.common.ids.NumberToShortStringCodec; import com.metabroadcast.common.ids.SubstitutionTableNumberCodec; import com.metabroadcast.common.queue.MessageSender; import com.metabroadcast.common.stream.MoreCollectors; import com.metabroadcast.common.time.Timestamp; import com.metabroadcast.common.time.Timestamper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Longs; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static com.google.common.base.Preconditions.checkNotNull; public class ContentEquivalenceAssertionMessenger { private static final Logger log = LoggerFactory .getLogger(ContentEquivalenceAssertionMessenger.class); private final MessageSender<ContentEquivalenceAssertionMessage> sender; private final Timestamper timestamper; private final LookupEntryStore lookupEntryStore; private final NumberToShortStringCodec entityIdCodec; private ContentEquivalenceAssertionMessenger( MessageSender<ContentEquivalenceAssertionMessage> sender, Timestamper timestamper, LookupEntryStore lookupEntryStore ) { this.sender = checkNotNull(sender); this.timestamper = checkNotNull(timestamper); this.lookupEntryStore = checkNotNull(lookupEntryStore); this.entityIdCodec = SubstitutionTableNumberCodec.lowerCaseOnly(); } public static ContentEquivalenceAssertionMessenger create( MessageSender<ContentEquivalenceAssertionMessage> sender, Timestamper timestamper, LookupEntryStore lookupEntryStore ) { return new ContentEquivalenceAssertionMessenger(sender, timestamper, lookupEntryStore); } public void sendMessage( Content subject, ImmutableList<Content> adjacents, ImmutableSet<String> sources ) { try { ContentEquivalenceAssertionMessage message = messageFrom( subject, adjacents, sources ); sender.sendMessage( message, getMessagePartitionKey(subject) ); } catch (Exception e) { log.error("Failed to send equiv update message: " + subject, e); } } private ContentEquivalenceAssertionMessage messageFrom( Content subject, ImmutableList<Content> adjacents, ImmutableSet<String> sources ) { String messageId = UUID.randomUUID().toString(); Timestamp timestamp = timestamper.timestamp(); String subjectId = entityIdCodec.encode(BigInteger.valueOf(subject.getId())); String subjectType = subject.getClass().getSimpleName().toLowerCase(); String subjectSource = subject.getPublisher().key(); ImmutableList<ContentEquivalenceAssertionMessage.AdjacentRef> adjacentRefs = adjacents( adjacents ); return new ContentEquivalenceAssertionMessage( messageId, timestamp, subjectId, subjectType, subjectSource, adjacentRefs, sources ); } private ImmutableList<ContentEquivalenceAssertionMessage.AdjacentRef> adjacents( ImmutableList<Content> adjacents ) { return adjacents.stream() .map(candidate -> new ContentEquivalenceAssertionMessage.AdjacentRef( entityIdCodec.encode(BigInteger.valueOf(candidate.getId())), candidate.getClass().getSimpleName().toLowerCase(), candidate.getPublisher().key() )) .collect(MoreCollectors.toImmutableList()); } private byte[] getMessagePartitionKey(Content subject) { Iterable<LookupEntry> lookupEntries = lookupEntryStore.entriesForIds( ImmutableSet.of(subject.getId()) ); Optional<LookupEntry> lookupEntryOptional = StreamSupport.stream( lookupEntries.spliterator(), false ) .findFirst(); if (lookupEntryOptional.isPresent()) { LookupEntry lookupEntry = lookupEntryOptional.get(); // Given most of the time the equivalence results do not change the existing graph // (due to the fact that we are often rerunning equivalence on the same items with // the same results) the underlying graph will remain unchanged. Therefore if we get // the smallest lookup entry ID from that graph that ID should be consistent enough // to use as a partition key and ensure updates on the same graph end up on the same // partition. Optional<Long> graphId = ImmutableSet.<LookupRef>builder() .addAll(lookupEntry.equivalents()) .addAll(lookupEntry.explicitEquivalents()) .addAll(lookupEntry.directEquivalents()) .build() .stream() .map(LookupRef::id) .sorted() .findFirst(); if (graphId.isPresent()) { return Longs.toByteArray(graphId.get()); } } // Default to returning the subject ID as the partition key return Longs.toByteArray(subject.getId()); } }