How does my comparator method violate its general contract?

NoBrainer :

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 to true, 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);
Stephen C :

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 in java.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:

  1. You could lock something to block all updates to the collections AND the element objects while you copy and sort.

  2. You could deep-copy the collect; i.e. create a new collection containing copies of the elements of the original collection.

  3. 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 original Task 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.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=237334&siteId=1