diff --git a/akka-actor/src/main/scala/akka/util/UUIDComparator.scala b/akka-actor/src/main/scala/akka/util/UUIDComparator.scala new file mode 100644 index 0000000000..87e75460d2 --- /dev/null +++ b/akka-actor/src/main/scala/akka/util/UUIDComparator.scala @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2016-2019 Lightbend Inc. + */ + +package akka.util + +import java.util.Comparator +import java.util.UUID + +/** + * Scala implementation of UUIDComparator in + * https://github.com/cowtowncoder/java-uuid-generator + * Apache License 2.0. + */ +class UUIDComparator extends Comparator[UUID] { + + def compare(u1: UUID, u2: UUID): Int = { + // First: major sorting by types + val version = u1.version() + val diff = version - u2.version() + if (diff != 0) { + diff + } else { + // Second: for time-based variant, order by time stamp: + if (version == 1) { + val diff2 = compareULongs(u1.timestamp(), u2.timestamp()) + if (diff2 == 0) { + // or if that won't work, by other bits lexically + compareULongs(u1.getLeastSignificantBits(), u2.getLeastSignificantBits()) + } else + diff2 + } else { + // note: java.util.Uuids compares with sign extension, IMO that's wrong, so: + val diff2 = compareULongs(u1.getMostSignificantBits(), u2.getMostSignificantBits()) + if (diff2 == 0) { + compareULongs(u1.getLeastSignificantBits(), u2.getLeastSignificantBits()) + } else + diff2 + } + } + } + + private def compareULongs(l1: Long, l2: Long): Int = { + val diff = compareUInts((l1 >> 32).toInt, (l2 >> 32).toInt) + if (diff == 0) + compareUInts(l1.toInt, l2.toInt) + else + diff + } + + private def compareUInts(i1: Int, i2: Int): Int = + /* bit messier due to java's insistence on signed values: if both + * have same sign, normal comparison (by subtraction) works fine; + * but if signs don't agree need to resolve differently + */ + if (i1 < 0) { + if (i2 < 0) (i1 - i2) else 1 + } else { + if (i2 < 0) -1 else (i1 - i2) + } + +} + +object UUIDComparator { + val comparator = new UUIDComparator +} diff --git a/akka-persistence-query/src/main/scala/akka/persistence/query/Offset.scala b/akka-persistence-query/src/main/scala/akka/persistence/query/Offset.scala index 579994c7dd..7c3bccda41 100644 --- a/akka-persistence-query/src/main/scala/akka/persistence/query/Offset.scala +++ b/akka-persistence-query/src/main/scala/akka/persistence/query/Offset.scala @@ -6,6 +6,8 @@ package akka.persistence.query import java.util.UUID +import akka.util.UUIDComparator + object Offset { // factories to aid discoverability @@ -44,7 +46,7 @@ final case class TimeBasedUUID(value: UUID) extends Offset with Ordered[TimeBase throw new IllegalArgumentException("UUID " + value + " is not a time-based UUID") } - override def compare(other: TimeBasedUUID): Int = value.compareTo(other.value) + override def compare(other: TimeBasedUUID): Int = UUIDComparator.comparator.compare(value, other.value) } /** diff --git a/akka-persistence-query/src/test/scala/akka/persistence/query/OffsetSpec.scala b/akka-persistence-query/src/test/scala/akka/persistence/query/OffsetSpec.scala new file mode 100644 index 0000000000..f1c4e6b83e --- /dev/null +++ b/akka-persistence-query/src/test/scala/akka/persistence/query/OffsetSpec.scala @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019 Lightbend Inc. + */ + +package akka.persistence.query + +import java.util.UUID + +import org.scalatest.{ Matchers, WordSpecLike } + +import scala.util.Random + +class OffsetSpec extends WordSpecLike with Matchers { + + "TimeBasedUUID offset" must { + + "be ordered correctly" in { + val uuid1 = TimeBasedUUID(UUID.fromString("49225740-2019-11ea-a752-ffae2393b6e4")) //2019-12-16T15:32:36.148Z[UTC] + val uuid2 = TimeBasedUUID(UUID.fromString("91be23d0-2019-11ea-a752-ffae2393b6e4")) //2019-12-16T15:34:37.965Z[UTC] + val uuid3 = TimeBasedUUID(UUID.fromString("91f95810-2019-11ea-a752-ffae2393b6e4")) //2019-12-16T15:34:38.353Z[UTC] + uuid1.value.timestamp() should be < uuid2.value.timestamp() + uuid2.value.timestamp() should be < uuid3.value.timestamp() + List(uuid2, uuid1, uuid3).sorted shouldEqual List(uuid1, uuid2, uuid3) + List(uuid3, uuid2, uuid1).sorted shouldEqual List(uuid1, uuid2, uuid3) + + } + } + + "Sequence offset" must { + + "be ordered correctly" in { + val sequenceBasedList = List(1L, 2L, 3L).map(Sequence) + Random.shuffle(sequenceBasedList).sorted shouldEqual sequenceBasedList + } + } +}