package org.atlasapi.persistence.lookup; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Predicates.in; import static com.google.common.base.Predicates.not; import static com.google.common.base.Strings.emptyToNull; import static com.google.common.collect.Iterables.transform; import java.util.Map; import java.util.Queue; import java.util.Set; import javax.annotation.Nullable; import org.atlasapi.equiv.ContentRef; import org.atlasapi.media.entity.LookupRef; import org.atlasapi.media.entity.Publisher; import org.atlasapi.persistence.lookup.entry.LookupEntry; import org.atlasapi.persistence.lookup.entry.LookupEntryStore; import org.atlasapi.util.GroupLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Functions; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.metabroadcast.common.base.MorePredicates; import com.metabroadcast.common.collect.MoreSets; public class TransitiveLookupWriter implements LookupWriter { private static final GroupLock<String> lock = GroupLock.<String>natural(); private static final Logger log = LoggerFactory.getLogger(TransitiveLookupWriter.class); private static final int maxSetSize = 150; private final LookupEntryStore entryStore; private final boolean explicit; public static TransitiveLookupWriter explicitTransitiveLookupWriter(LookupEntryStore entryStore) { return new TransitiveLookupWriter(entryStore, true); } public static TransitiveLookupWriter generatedTransitiveLookupWriter(LookupEntryStore entryStore) { return new TransitiveLookupWriter(entryStore, false); } private TransitiveLookupWriter(LookupEntryStore entryStore, boolean explicit) { this.entryStore = entryStore; this.explicit = explicit; } private static final Function<ContentRef, String> TO_URI = new Function<ContentRef, String>() { @Override public String apply(@Nullable ContentRef input) { return input.getCanonicalUri(); } }; @Override public Optional<Set<LookupEntry>> writeLookup(ContentRef subject, Iterable<ContentRef> equivalents, Set<Publisher> sources) { Iterable<String> neighbourUris = Iterables.transform(filterContentsources(equivalents, sources), TO_URI); return writeLookup(subject.getCanonicalUri(), ImmutableSet.copyOf(neighbourUris), sources); } private Iterable<ContentRef> filterContentsources(Iterable<ContentRef> content, final Set<Publisher> sources) { return Iterables.filter(content, new Predicate<ContentRef>() { @Override public boolean apply(ContentRef input) { return sources.contains(input.getPublisher()); } }); } public Optional<Set<LookupEntry>> writeLookup(final String subjectUri, Iterable<String> equivalentUris, final Set<Publisher> sources) { Preconditions.checkNotNull(emptyToNull(subjectUri), "null subject"); ImmutableSet<String> newNeighboursUris = ImmutableSet.copyOf(equivalentUris); Set<String> subjectAndNeighbours = MoreSets.add(newNeighboursUris, subjectUri); Set<String> transitiveSetsUris = null; try { synchronized (lock) { while((transitiveSetsUris = tryLockAllIds(subjectAndNeighbours)) == null) { lock.unlock(subjectAndNeighbours); lock.wait(); } } return updateEntries(subjectUri, newNeighboursUris, transitiveSetsUris, sources); } catch(OversizeTransitiveSetException otse) { log.info(String.format("Oversize set: %s + %s: %s", subjectUri, newNeighboursUris, otse.getMessage())); return Optional.absent(); } catch(InterruptedException e) { log.error(String.format("%s: %s", subjectUri, newNeighboursUris), e); return Optional.absent(); } finally { synchronized (lock) { lock.unlock(subjectAndNeighbours); if (transitiveSetsUris != null) { lock.unlock(transitiveSetsUris); } lock.notifyAll(); } } } private Optional<Set<LookupEntry>> updateEntries(String subjectUri, ImmutableSet<String> newNeighboursUris, Set<String> transitiveSetsUris, Set<Publisher> sources) { LookupEntry subject = entryFor(subjectUri); checkNotNull(subject, "No entry for %s", subjectUri); if(noChangeInNeighbours(subject, newNeighboursUris, sources)) { log.debug("{}: no change in neighbours: {}", subjectUri, newNeighboursUris); return Optional.absent(); } // entries for all members in all transitive sets involved Map<String, LookupEntry> entryIndex = resolveTransitiveSets(transitiveSetsUris); Set<LookupEntry> newNeighbours = newSubjectNeighbours(newNeighboursUris, entryIndex); for (LookupEntry entry : entryIndex.values()) { entryIndex.put(entry.uri(), updateEntryNeighbours(entry, subject, newNeighbours, sources) ); } Set<LookupEntry> newLookups = recomputeTransitiveClosures(entryIndex); for (LookupEntry entry : newLookups) { entryStore.store(entry); } return Optional.of(newLookups); } private ImmutableSet<LookupEntry> newSubjectNeighbours(Set<String> neighboursUris, Map<String, LookupEntry> entryIndex) { return ImmutableSet.copyOf(Iterables.transform(neighboursUris, Functions.forMap(entryIndex))); } private Map<String, LookupEntry> resolveTransitiveSets(Set<String> transitiveSetUris) { return Maps.newHashMap(Maps.uniqueIndex(entriesFor(transitiveSetUris), LookupEntry.TO_ID)); } /* * Attempts to lock the URIs of the directly affected entries before * resolving the entries and then attempting to lock the full equivalence * sets. * * The initial lock need to be attempted since between resolving the entries * and locking the full equivalence sets another thread could potentially * have changed those entries. * * A return value of null means either of the lock attempts failed an * locking needs to re-attempted. Non-null return is a set containing all * URIs in all transitive sets relevant to this update. */ private Set<String> tryLockAllIds(Set<String> neighboursUris) throws InterruptedException { if (!lock.tryLock(neighboursUris)) { return null; } Set<LookupEntry> entries = entriesFor(neighboursUris); Iterable<LookupRef> transitiveSetRefs = Iterables.concat(Iterables.transform(entries, LookupEntry.TO_EQUIVS)); Set<String> transitiveSetUris = ImmutableSet.copyOf(Iterables.transform(transitiveSetRefs, LookupRef.TO_URI)); // We allow oversize sets if this is being written as an explicit equivalence, // since a user has explicitly asked us to make the assertion, so we must // honour it if (!explicit && transitiveSetUris.size() > maxSetSize) { throw new OversizeTransitiveSetException(transitiveSetUris.size()); } Iterable<String> urisToLock = Iterables.filter(transitiveSetUris, not(in(neighboursUris))); return lock.tryLock(ImmutableSet.copyOf(urisToLock)) ? transitiveSetUris : null; } private LookupEntry updateEntryNeighbours(LookupEntry entry, LookupEntry subject, Set<LookupEntry> subjectNeighbours, Set<Publisher> sources) { if (entry.equals(subject)) { return updateSubjectNeighbours(subject, subjectNeighbours, sources); } if (sources.contains(entry.lookupRef().publisher())) { return updateEntrysNeighbours(entry, subject, subjectNeighbours); } return entry; } private LookupEntry updateSubjectNeighbours(LookupEntry subject, Set<LookupEntry> neighbours, Set<Publisher> sources) { Predicate<LookupRef> unaffectedSources = MorePredicates.transformingPredicate(LookupRef.TO_SOURCE, not(in(sources))); return updateRelevantNeighbours(subject, Iterables.concat( Sets.filter(getRelevantNeighbours(subject), unaffectedSources), Iterables.transform(neighbours, LookupEntry.TO_SELF) )); } private LookupEntry updateEntrysNeighbours(LookupEntry entry, LookupEntry subject, Set<LookupEntry> subjectNeighbours) { ImmutableSet<LookupRef> subjectRef = ImmutableSet.of(subject.lookupRef()); Set<LookupRef> entryNeighbours = getRelevantNeighbours(entry); if (subjectNeighbours.contains(entry)) { entryNeighbours = Sets.union(entryNeighbours, subjectRef); } else { entryNeighbours = Sets.difference(entryNeighbours, subjectRef); } return updateRelevantNeighbours(entry, entryNeighbours); } private LookupEntry updateRelevantNeighbours(LookupEntry equivalent, Iterable<LookupRef> updatedNeighbours) { return explicit ? equivalent.copyWithExplicitEquivalents(updatedNeighbours) : equivalent.copyWithDirectEquivalents(updatedNeighbours); } private boolean noChangeInNeighbours(LookupEntry subject, ImmutableSet<String> newNeighbours, Set<Publisher> sources) { Set<LookupRef> currentNeighbours = Sets.filter( getRelevantNeighbours(subject), MorePredicates.transformingPredicate(LookupRef.TO_SOURCE, in(sources)) ); Set<String> subjectAndNeighbours = MoreSets.add(newNeighbours, subject.uri()); Set<String> currentNeighbourUris = ImmutableSet.copyOf(transform(currentNeighbours, LookupRef.TO_URI)); boolean noChange = currentNeighbourUris.equals(subjectAndNeighbours); if (!noChange) { log.debug("Equivalence change: {} -> {}", currentNeighbourUris, subjectAndNeighbours); } return noChange; } private Set<LookupRef> getRelevantNeighbours(LookupEntry subjectEntry) { return explicit ? subjectEntry.explicitEquivalents() : subjectEntry.directEquivalents(); } private Set<LookupEntry> recomputeTransitiveClosures(Map<String, LookupEntry> entries) { Set<LookupEntry> updatedEntries = Sets.newHashSet(); for (LookupEntry entry : Iterables.filter(entries.values(), not(in(updatedEntries)))) { Set<LookupRef> transitiveSet = Sets.newHashSet(); Queue<LookupRef> direct = Lists.newLinkedList(neighbours(entry)); //Traverse equivalence graph breadth-first Set<LookupRef> seen = Sets.newHashSet(); while(!direct.isEmpty()) { LookupRef current = direct.poll(); if (!seen.contains(current)) { transitiveSet.add(current); if (entries.get(current.uri())!= null) { LookupEntry currentEntry = entries.get(current.uri()); Iterables.addAll(direct, Iterables.filter(neighbours(currentEntry), not(in(transitiveSet)))); } seen.add(current); } } // Because all entries in the same transitive set should have // the same equivalents their entries can be updated here, // short-circuiting the top-level loop. for (LookupRef lookupRef : transitiveSet) { LookupEntry lookupEntry = entries.get(lookupRef.uri()); if(lookupEntry != null ) { updatedEntries.add(lookupEntry.copyWithEquivalents(transitiveSet)); } } } return updatedEntries; } private Iterable<LookupRef> neighbours(LookupEntry current) { return Iterables.concat(current.directEquivalents(), current.explicitEquivalents()); } private Set<LookupEntry> entriesFor(Iterable<String> equivalents) { return ImmutableSet.copyOf(entryStore.entriesForCanonicalUris(equivalents)); } private LookupEntry entryFor(String subject) { return Iterables.getOnlyElement(entryStore.entriesForCanonicalUris(ImmutableList.of(subject)), null); } private static class OversizeTransitiveSetException extends RuntimeException { private int size; public OversizeTransitiveSetException(int size) { this.size = size; } @Override public String getMessage() { return String.valueOf(size); } } }