package edu.harvard.econcs.turkserver.util; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListSet; import javax.annotation.Nullable; import net.andrewmao.math.RandomSelection; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; /** * Simple assignment maker which * - equalizes the number of assignees to items * - ensures that no assignee gets the same item twice * - re-assigns the same assignment to an existing assignee * * Thread safe. * * TODO this works fine for limited numbers of items, * but would be much better with a Redis implementation * when there are a large number of items and users * * @author mao * */ public class UserItemMatcher<U, I> { Map<I, Set<U>> itemUsers; ConcurrentHashMap<U, I> currentAssignments; ConcurrentSkipListSet<CountingKey> orderedItems; Comparator<I> defaultOrder; public UserItemMatcher(Set<I> items, final Comparator<I> defaultOrder, @Nullable Multimap<U, I> existing) { this.defaultOrder = defaultOrder; Map<I, Set<U>> temp = Maps.newHashMap(); for( I asst : items ) { // Synchronize all sets, no concurrency needed temp.put(asst, Collections.synchronizedSet(new HashSet<U>())); } itemUsers = Collections.unmodifiableMap(temp); // initialize existing users for each item, if any if( existing != null ) { for( Map.Entry<U, I> e : existing.entries() ) { U user = e.getKey(); I item = e.getValue(); if( items.contains(item) ) itemUsers.get(item).add(user); } } // Set up counts for existing keys int total = 0; orderedItems = new ConcurrentSkipListSet<CountingKey>(); for( Map.Entry<I, Set<U>> e : itemUsers.entrySet() ) { int count = e.getValue().size(); total += count; orderedItems.add(new CountingKey(e.getKey(), count)); } System.out.printf("Preloaded %d existing items\n", total); currentAssignments = new ConcurrentHashMap<U, I>(); } public UserItemMatcher(Set<I> items, final Comparator<I> defaultOrder) { this(items, defaultOrder, null); } /** * Give this worker an item with lowest count that he has not already done * don't reassign the same items to workers * * Returns null if no more tasks available for this user * @param user * @return */ public I getNewAssignment(U user) { List<CountingKey> lowestItems = Lists.newLinkedList(); int lowest = 0; for( Iterator<CountingKey> it = orderedItems.iterator(); it.hasNext(); ) { CountingKey currentKey = it.next(); if( currentKey.count > lowest ) { if( lowestItems.isEmpty() ) { lowest = currentKey.count; } else break; } // do not assign users to items they have already seen Set<U> existing = itemUsers.get(currentKey.key); if (existing.contains(user)) continue; lowestItems.add(currentKey); } // choose randomly among the lowest counts CountingKey selected = RandomSelection.selectRandom(lowestItems); I prevAssigned = null; if( selected != null && (prevAssigned = currentAssignments.putIfAbsent(user, selected.key)) != null ) { System.out.printf("Warning: returning existing current assignment %s for user %s\n", prevAssigned, user); return prevAssigned; } if( lowestItems.isEmpty() ) { System.out.printf("Sorry, nothing to assign for user %s\n", user); return null; } // To prevent others from viewing an empty map in the case of 1 assignment, we insert a duplicate before removing CountingKey newKey = new CountingKey(selected.key, selected.count + 1); orderedItems.add(newKey); // Remove old key from map, and update users in set orderedItems.remove(selected); // TODO very rare case that user could get reassigned same item if they complete before here, but it's practically impossible Set<U> users = itemUsers.get(selected.key); users.add(user); return selected.key; } public I getCurrentAssignment(U user) { return currentAssignments.get(user); } public void returnAssignment(U user, I item) { Set<U> users = itemUsers.get(item); if( users == null ) { System.out.println("Warning: unexpected assignment to return: " + item); return; } if( !currentAssignments.remove(user, item) ) { throw new IllegalStateException(String.format("unexpected return of %s: %s", item, user)); } if( !users.remove(user) ) { System.out.println("Warning: user not in set. shouldn't have gotten here."); } // Decrement key for item I for( Iterator<CountingKey> it = orderedItems.iterator(); it.hasNext(); ) { CountingKey current = it.next(); if( current.key.equals(item) ) { CountingKey newKey = new CountingKey(item, current.count - 1); orderedItems.add(newKey); // Remove old key from map it.remove(); break; } } } public void completeAssignment(U user, I item) { if( !itemUsers.containsKey(item) ) { System.out.println("Warning: unexpected assignment completed: " + item); return; } if( !currentAssignments.remove(user, item) ) { throw new IllegalStateException(String.format("Warning: unexpected completion of %s: %s", item, user)); } } class CountingKey implements Comparable<CountingKey> { I key; int count; CountingKey(I key, int count) { this.key = key; this.count = count; } @Override public int compareTo(CountingKey other) { // Two strings can't be equal so we just sort by some other order in that case if( this.count == other.count ) return defaultOrder.compare(this.key, other.key); else return (this.count < other.count ? -1 : 1); } } }