Third-Party Packages
import java.util.Collections;
import java.util.Comparator;
import org.joda.time.DateTime;
My Comparator
public static Comparator<Task> TASK_PRIORITY = new Comparator<Task>() {
public int compare(Task task1, Task task2) {
if (task1 == null && task2 == null) return 0;
if (task1 == null) return +1; //null last
if (task2 == null) return -1; //null last
// Only consider retries after a task is retried 5+ times
if (task1.getRetries() >= 5 || task2.getRetries() >= 5) {
// Primary sort: retry count (ascending)
int retriesCompare = Integer.compare(task1.getRetries(), task2.getRetries());
if (retriesCompare != 0) return retriesCompare;
}
// Secondary sort: creation time (ascending, null first)
int creationCompare = compareTimeNullFirst(task1.getCreationTime(), task2.getCreationTime());
if (creationCompare != 0) return creationCompare;
// Tertiary sort: load time (ascending, null last)
int loadCompare = compareTimeNullLast(task1.getLoadTime(), task2.getLoadTime());
if (loadCompare != 0) return loadCompare;
return 0;
}
};
private static int compareTimeNullLast(DateTime time1, DateTime time2) {
if (time1 == null && time2 == null) return 0;
if (time1 == null) return +1;
if (time2 == null) return -1;
if (time1.isBefore(time2) return -1;
if (time1.isAfter(time2)) return +1;
return 0;
}
private static int compareTimeNullFirst(DateTime time1, DateTime time2) {
if (time1 == null && time2 == null) return 0;
if (time1 == null) return -1;
if (time2 == null) return +1;
if (time1.isBefore(time2) return -1;
if (time1.isAfter(time2)) return +1;
return 0;
}
Using My Comparator
//tasks is a List<Task>
Collections.sort(tasks, TASK_PRIORITY);
My Problem
I sometimes get an IllegalArgumentException
for Comparison method violates its general contract!
. I can consistently get this Exception thrown with live data running long enough, but I'm not sure how to fix the actual cause of the problem.
My Question
What is wrong with my comparator? (Specifically, which part of the contract am I violating?) How do I fix it without covering up the Exception?
Notes
- I'm using Java 7 and cannot upgrade without a major rewrite.
- I could possibly cover-up the Exception by setting
java.util.Arrays.useLegacyMergeSort
totrue
, but that is not a desirable solution. - I tried to create tests to randomly generate data and verify each of the contract conditions. I wasn't able to get the exception to be thrown.
- I tried removing the condition around the retry comparison, but I still eventually got the Exception.
- This line throws the exception:
Collections.sort(tasks, TASK_PRIORITY);
Let us start at the beginning. My reading of your code is that the logic of your Comparator is sound. (I would have avoided having null Task
and DateTime
values, but that is not relevant to your problem.)
The other thing that can cause this exception is if the compare
method is giving inconsistent results because the Task
objects are changing. Indeed, it looks like it is semantically meaningful for (at least) the retry counts to change. If there is another thread that is changing Task
the fields that can effect the ordering ... while the current thread is sorting ... that could the IllegalArgumentException
.
(Part of the comparison contract is that pairwise ordering does not change while you are sorting the collection.)
You then say this:
I use
ImmutableSet.copyOf
to copy the list before sorting, and I do that under a read lock injava.util.concurrent.locks.ReadWriteLock
.
Copying a collection doesn't make copies of the elements of the collection. It is a shallow copy. So you will end up with two collections that contain the same objects. If the other thread mutates any of the objects (e.g. by increasing retry counts), that could change the ordering of objects.
The locking makes sure that you have consistent copy, but that's not what the problem is.
What is the solution? I can think of a couple:
You could lock something to block all updates to the collections AND the element objects while you copy and sort.
You could deep-copy the collect; i.e. create a new collection containing copies of the elements of the original collection.
You could create light-weight objects that contain a snapshots of the fields of the
Task
objects that are relevant to sorting; e.g.public class Key implements Comparable<Key> { private int retries; private DateTime creation; private DateTime load; private Task task; public Key(Task task) { this.task = task; this.retries = task.getRetryCount(); ... } public int compareTo(Key other) { // compare using retries, creation, load } }
This has the potential advantages that you are copying less information, and you can go from the sorted collection of
Key
objects to the originalTask
objects.
Note all of these alternatives are slower than what you are currently doing. I don't think there is a way to avoid this.