UPDATE FROM 2015-10-30
based on Roland Kuhn Awnser:
Akka Streams is using asynchronous message passing between Actors to
implement stream processing stages. Passing data across an
asynchronous boundary has an overhead that you are seeing here: your
computation seems to take only about 160ns (derived from the
single-threaded measurement) while the streaming solution takes
roughly 1µs per element, which is dominated by the message passing.
Another misconception is that saying “stream” implies parallelism: in
your code all computation runs sequentially in a single Actor (the map
stage), so no benefit can be expected over the primitive
single-threaded solution.
In order to benefit from the parallelism afforded by Akka Streams you
need to have multiple processing stages that each perform tasks of
1µs per element, see also the docs.
I did some changes. My code now looks like:
object MultiThread {
implicit val actorSystem = ActorSystem("Sys")
implicit val materializer = ActorMaterializer()
var counter = 0
var oldProgess = 0
//RunnableFlow: in -> flow -> sink
val in = Source(() => Iterator.continually((1254785478l, "name", 48, 23.09f)))
val flow = Flow[(Long, String, Int, Float)].map(p => SharedFunctions.transform2(SharedFunctions.transform(p)))
val tupleToEvent = Flow[(Long, String, Int, Float)].map(SharedFunctions.transform)
val eventToFactorial = Flow[Event].map(SharedFunctions.transform2)
val eventChef: Flow[(Long, String, Int, Float), Int, Unit] = Flow() { implicit builder =>
import FlowGraph.Implicits._
val dispatchTuple = builder.add(Balance[(Long, String, Int, Float)](4))
val mergeEvents = builder.add(Merge[Int](4))
dispatchTuple.out(0) ~> tupleToEvent ~> eventToFactorial ~> mergeEvents.in(0)
dispatchTuple.out(1) ~> tupleToEvent ~> eventToFactorial ~> mergeEvents.in(1)
dispatchTuple.out(2) ~> tupleToEvent ~> eventToFactorial ~> mergeEvents.in(2)
dispatchTuple.out(3) ~> tupleToEvent ~> eventToFactorial ~> mergeEvents.in(3)
(dispatchTuple.in, mergeEvents.out)
}
val sink = Sink.foreach[Int]{
v => counter += 1
oldProgess = SharedFunctions.printProgress(oldProgess, SharedFunctions.maxEventCount, counter,
DateTime.now.getMillis - SharedFunctions.startTime.getMillis)
if(counter == SharedFunctions.maxEventCount) endAkka()
}
def endAkka() = {
val duration = new Duration(SharedFunctions.startTime, DateTime.now)
println("Time: " + duration.getMillis + " || Data: " + counter)
actorSystem.shutdown
actorSystem.awaitTermination
System.exit(-1)
}
def main(args: Array[String]) {
println("MultiThread started: " + SharedFunctions.startTime)
in.via(flow).runWith(sink)
// in.via(eventChef).runWith(sink)
}
}
I not sure if I get something totally wrong, but still my implementation with akka-streams is much slower (now even slower as before) but what I found out is: If I increase the work for example by doing some division the implementation with akka-streams gets faster. So If I get it right (correct me otherwise) it seems there is too much overhead in my example. So you only get a benefit from akka-streams if the code has to do heavy work?
I'm relatively new in both scala & akka-stream. I wrote a little test project which creates some events until a counter has reached a specific number. For each event the factorial for one field of the event is being computed. I implemented this twice. One time with akka-stream and one time without akka-stream (single threaded) and compared the runtime.
I didn't expect that: When I create a single event the runtime of both programs are nearly the same. But if I create 70,000,000 events the implementation without akka-streams is much faster. Here are my results (the following data is based on 24 measurements):
Single event without akka-streams: 403 (+- 2)ms
Single event with akka-streams: 444 (+-13)ms
70Mio events without akka-streams: 11778 (+-70)ms
70Mio events with akka-steams: 75424(+-2959)ms
So my Question is: What is going on? Why is my implementation with akka-stream slower?
here my code:
Implementation with Akka
object MultiThread {
implicit val actorSystem = ActorSystem("Sys")
implicit val materializer = ActorMaterializer()
var counter = 0
var oldProgess = 0
//RunnableFlow: in -> flow -> sink
val in = Source(() => Iterator.continually((1254785478l, "name", 48, 23.09f)))
val flow = Flow[(Long, String, Int, Float)].map(p => SharedFunctions.transform2(SharedFunctions.transform(p)))
val sink = Sink.foreach[Int]{
v => counter += 1
oldProgess = SharedFunctions.printProgress(oldProgess, SharedFunctions.maxEventCount, counter,
DateTime.now.getMillis - SharedFunctions.startTime.getMillis)
if(counter == SharedFunctions.maxEventCount) endAkka()
}
def endAkka() = {
val duration = new Duration(SharedFunctions.startTime, DateTime.now)
println("Time: " + duration.getMillis + " || Data: " + counter)
actorSystem.shutdown
actorSystem.awaitTermination
System.exit(-1)
}
def main(args: Array[String]) {
import scala.concurrent.ExecutionContext.Implicits.global
println("MultiThread started: " + SharedFunctions.startTime)
in.via(flow).runWith(sink).onComplete(_ => endAkka())
}
}
Implementation without Akka
object SingleThread {
def main(args: Array[String]) {
println("SingleThread started at: " + SharedFunctions.startTime)
println("0%")
val i = createEvent(0)
val duration = new Duration(SharedFunctions.startTime, DateTime.now());
println("Time: " + duration.getMillis + " || Data: " + i)
}
def createEventWorker(oldProgress: Int, count: Int, randDate: Long, name: String, age: Int, myFloat: Float): Int = {
if (count == SharedFunctions.maxEventCount) count
else {
val e = SharedFunctions.transform((randDate, name, age, myFloat))
SharedFunctions.transform2(e)
val p = SharedFunctions.printProgress(oldProgress, SharedFunctions.maxEventCount, count,
DateTime.now.getMillis - SharedFunctions.startTime.getMillis)
createEventWorker(p, count + 1, 1254785478l, "name", 48, 23.09f)
}
}
def createEvent(count: Int): Int = {
createEventWorker(0, count, 1254785478l, "name", 48, 23.09f)
}
}
SharedFunctions
object SharedFunctions {
val maxEventCount = 70000000
val startTime = DateTime.now
def transform(t : (Long, String, Int, Float)) : Event = new Event(t._1 ,t._2,t._3,t._4)
def transform2(e : Event) : Int = factorial(e.getAgeYrs)
def calculatePercentage(totalValue: Long, currentValue: Long) = Math.round((currentValue * 100) / totalValue)
def printProgress(oldProgress : Int, fileSize: Long, currentSize: Int, t: Long) = {
val cProgress = calculatePercentage(fileSize, currentSize)
if (oldProgress != cProgress) println(s"$oldProgress% | $t ms")
cProgress
}
private def factorialWorker(n1: Int, n2: Int): Int = {
if (n1 == 0) n2
else factorialWorker(n1 -1, n2*n1)
}
def factorial (n : Int): Int = {
factorialWorker(n, 1)
}
}
Implementation Event
/**
* Autogenerated by Avro
*
* DO NOT EDIT DIRECTLY
*/
#SuppressWarnings("all")
#org.apache.avro.specific.AvroGenerated
public class Event extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord {
public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"Event\",\"namespace\":\"week2P2\",\"fields\":[{\"name\":\"timestampMS\",\"type\":\"long\"},{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"ageYrs\",\"type\":\"int\"},{\"name\":\"sizeCm\",\"type\":\"float\"}]}");
public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; }
#Deprecated public long timestampMS;
#Deprecated public CharSequence name;
#Deprecated public int ageYrs;
#Deprecated public float sizeCm;
/**
* Default constructor. Note that this does not initialize fields
* to their default values from the schema. If that is desired then
* one should use <code>newBuilder()</code>.
*/
public Event() {}
/**
* All-args constructor.
*/
public Event(Long timestampMS, CharSequence name, Integer ageYrs, Float sizeCm) {
this.timestampMS = timestampMS;
this.name = name;
this.ageYrs = ageYrs;
this.sizeCm = sizeCm;
}
public org.apache.avro.Schema getSchema() { return SCHEMA$; }
// Used by DatumWriter. Applications should not call.
public Object get(int field$) {
switch (field$) {
case 0: return timestampMS;
case 1: return name;
case 2: return ageYrs;
case 3: return sizeCm;
default: throw new org.apache.avro.AvroRuntimeException("Bad index");
}
}
// Used by DatumReader. Applications should not call.
#SuppressWarnings(value="unchecked")
public void put(int field$, Object value$) {
switch (field$) {
case 0: timestampMS = (Long)value$; break;
case 1: name = (CharSequence)value$; break;
case 2: ageYrs = (Integer)value$; break;
case 3: sizeCm = (Float)value$; break;
default: throw new org.apache.avro.AvroRuntimeException("Bad index");
}
}
/**
* Gets the value of the 'timestampMS' field.
*/
public Long getTimestampMS() {
return timestampMS;
}
/**
* Sets the value of the 'timestampMS' field.
* #param value the value to set.
*/
public void setTimestampMS(Long value) {
this.timestampMS = value;
}
/**
* Gets the value of the 'name' field.
*/
public CharSequence getName() {
return name;
}
/**
* Sets the value of the 'name' field.
* #param value the value to set.
*/
public void setName(CharSequence value) {
this.name = value;
}
/**
* Gets the value of the 'ageYrs' field.
*/
public Integer getAgeYrs() {
return ageYrs;
}
/**
* Sets the value of the 'ageYrs' field.
* #param value the value to set.
*/
public void setAgeYrs(Integer value) {
this.ageYrs = value;
}
/**
* Gets the value of the 'sizeCm' field.
*/
public Float getSizeCm() {
return sizeCm;
}
/**
* Sets the value of the 'sizeCm' field.
* #param value the value to set.
*/
public void setSizeCm(Float value) {
this.sizeCm = value;
}
/** Creates a new Event RecordBuilder */
public static Event.Builder newBuilder() {
return new Event.Builder();
}
/** Creates a new Event RecordBuilder by copying an existing Builder */
public static Event.Builder newBuilder(Event.Builder other) {
return new Event.Builder(other);
}
/** Creates a new Event RecordBuilder by copying an existing Event instance */
public static Event.Builder newBuilder(Event other) {
return new Event.Builder(other);
}
/**
* RecordBuilder for Event instances.
*/
public static class Builder extends org.apache.avro.specific.SpecificRecordBuilderBase<Event>
implements org.apache.avro.data.RecordBuilder<Event> {
private long timestampMS;
private CharSequence name;
private int ageYrs;
private float sizeCm;
/** Creates a new Builder */
private Builder() {
super(Event.SCHEMA$);
}
/** Creates a Builder by copying an existing Builder */
private Builder(Event.Builder other) {
super(other);
if (isValidValue(fields()[0], other.timestampMS)) {
this.timestampMS = data().deepCopy(fields()[0].schema(), other.timestampMS);
fieldSetFlags()[0] = true;
}
if (isValidValue(fields()[1], other.name)) {
this.name = data().deepCopy(fields()[1].schema(), other.name);
fieldSetFlags()[1] = true;
}
if (isValidValue(fields()[2], other.ageYrs)) {
this.ageYrs = data().deepCopy(fields()[2].schema(), other.ageYrs);
fieldSetFlags()[2] = true;
}
if (isValidValue(fields()[3], other.sizeCm)) {
this.sizeCm = data().deepCopy(fields()[3].schema(), other.sizeCm);
fieldSetFlags()[3] = true;
}
}
/** Creates a Builder by copying an existing Event instance */
private Builder(Event other) {
super(Event.SCHEMA$);
if (isValidValue(fields()[0], other.timestampMS)) {
this.timestampMS = data().deepCopy(fields()[0].schema(), other.timestampMS);
fieldSetFlags()[0] = true;
}
if (isValidValue(fields()[1], other.name)) {
this.name = data().deepCopy(fields()[1].schema(), other.name);
fieldSetFlags()[1] = true;
}
if (isValidValue(fields()[2], other.ageYrs)) {
this.ageYrs = data().deepCopy(fields()[2].schema(), other.ageYrs);
fieldSetFlags()[2] = true;
}
if (isValidValue(fields()[3], other.sizeCm)) {
this.sizeCm = data().deepCopy(fields()[3].schema(), other.sizeCm);
fieldSetFlags()[3] = true;
}
}
/** Gets the value of the 'timestampMS' field */
public Long getTimestampMS() {
return timestampMS;
}
/** Sets the value of the 'timestampMS' field */
public Event.Builder setTimestampMS(long value) {
validate(fields()[0], value);
this.timestampMS = value;
fieldSetFlags()[0] = true;
return this;
}
/** Checks whether the 'timestampMS' field has been set */
public boolean hasTimestampMS() {
return fieldSetFlags()[0];
}
/** Clears the value of the 'timestampMS' field */
public Event.Builder clearTimestampMS() {
fieldSetFlags()[0] = false;
return this;
}
/** Gets the value of the 'name' field */
public CharSequence getName() {
return name;
}
/** Sets the value of the 'name' field */
public Event.Builder setName(CharSequence value) {
validate(fields()[1], value);
this.name = value;
fieldSetFlags()[1] = true;
return this;
}
/** Checks whether the 'name' field has been set */
public boolean hasName() {
return fieldSetFlags()[1];
}
/** Clears the value of the 'name' field */
public Event.Builder clearName() {
name = null;
fieldSetFlags()[1] = false;
return this;
}
/** Gets the value of the 'ageYrs' field */
public Integer getAgeYrs() {
return ageYrs;
}
/** Sets the value of the 'ageYrs' field */
public Event.Builder setAgeYrs(int value) {
validate(fields()[2], value);
this.ageYrs = value;
fieldSetFlags()[2] = true;
return this;
}
/** Checks whether the 'ageYrs' field has been set */
public boolean hasAgeYrs() {
return fieldSetFlags()[2];
}
/** Clears the value of the 'ageYrs' field */
public Event.Builder clearAgeYrs() {
fieldSetFlags()[2] = false;
return this;
}
/** Gets the value of the 'sizeCm' field */
public Float getSizeCm() {
return sizeCm;
}
/** Sets the value of the 'sizeCm' field */
public Event.Builder setSizeCm(float value) {
validate(fields()[3], value);
this.sizeCm = value;
fieldSetFlags()[3] = true;
return this;
}
/** Checks whether the 'sizeCm' field has been set */
public boolean hasSizeCm() {
return fieldSetFlags()[3];
}
/** Clears the value of the 'sizeCm' field */
public Event.Builder clearSizeCm() {
fieldSetFlags()[3] = false;
return this;
}
#Override
public Event build() {
try {
Event record = new Event();
record.timestampMS = fieldSetFlags()[0] ? this.timestampMS : (Long) defaultValue(fields()[0]);
record.name = fieldSetFlags()[1] ? this.name : (CharSequence) defaultValue(fields()[1]);
record.ageYrs = fieldSetFlags()[2] ? this.ageYrs : (Integer) defaultValue(fields()[2]);
record.sizeCm = fieldSetFlags()[3] ? this.sizeCm : (Float) defaultValue(fields()[3]);
return record;
} catch (Exception e) {
throw new org.apache.avro.AvroRuntimeException(e);
}
}
}
}
Akka Streams is using asynchronous message passing between Actors to implement stream processing stages. Passing data across an asynchronous boundary has an overhead that you are seeing here: your computation seems to take only about 160ns (derived from the single-threaded measurement) while the streaming solution takes roughly 1µs per element, which is dominated by the message passing.
Another misconception is that saying “stream” implies parallelism: in your code all computation runs sequentially in a single Actor (the map stage), so no benefit can be expected over the primitive single-threaded solution.
In order to benefit from the parallelism afforded by Akka Streams you need to have multiple processing stages that each perform tasks of >1µs per element, see also the docs.
In addition to Roland's explanation, which I agree with fully, it should be understood that akka Streams are not just a concurrent programming framework. Streams also provide back pressure which means Events are only generated by the Source when there is demand to process them in the Sink. This communication of demand adds some overhead at each processing step.
Therefore your single-thread and multi-thread comparison is not "apples-to-apples".
If you want raw multi-threaded execution performance then Futures/Actors are a better way to go.
Related
I'm trying to write some code that uses a Scanner input to ask for a LibraryPatron's name and use the contains() method I have to check if the LibraryPatron is part of the list. I am not having issues regarding how the contains() method works, but rather how to use it to check based off of the object's name. Thanks in advance.
Here is my LinkedList class (called BagList in this case), which already has a contains() method:
package libraryPatrons;
public final class BagList<T> implements BagInterface<T> {
Node firstNode;
int numPatrons;
public BagList() {
firstNode = null;
numPatrons = 0;
}
public int getCurrentSize() {
return numPatrons;
}
/** Sees whether this bag is empty.
* #return True if the bag is empty, or false if not. */
public boolean isEmpty() {
return firstNode == null;
}
/** Adds a new entry to this bag.
* #param newEntry The object to be added as a new entry.
* #return True if the addition is successful, or false if not. */
public boolean add(T newEntry) {
Node newNode = new Node (newEntry);
newNode.next = firstNode;
firstNode = newNode;
++numPatrons;
return true;
}
/** Removes one unspecified entry from this bag, if possible.
* #return The removed entry if the removal was successful, or null. */
public T remove() {
T result = null;
if (firstNode != null) {
result = firstNode.data;
firstNode = firstNode.next;
--numPatrons;
}
return result;
}
private Node getReferenceTo (T anEntry) {
Node currentNode = firstNode;
boolean found = false;
while (!found && currentNode != null) {
if (anEntry.equals(currentNode.data)) {
found = true;
} else {
currentNode = currentNode.next;
}
}
return currentNode;
}
/** Removes one occurrence of a given entry from this bag.
* #param anEntry The entry to be removed.
* #return True if the removal was successful, or false if not. */
public boolean remove(T anEntry) {
boolean result = false;
Node currentNode = getReferenceTo (anEntry);
if (currentNode != null) {
currentNode.data = firstNode.data;
firstNode = firstNode.next;
--numPatrons;
result = true;
}
return result;
}
/** Removes all entries from this bag. */
public void clear() {
while (!isEmpty()) {
remove();
}
numPatrons = 0;
}
/** Counts the number of times a given entry appears in this bag.
* #param anEntry The entry to be counted.
* #return The number of times anEntry appears in the bag. */
public int getFrequencyOf(T anEntry) {
Node currentNode = firstNode;
int frequency = 0;
while (currentNode != null) {
if (anEntry.equals(currentNode.data))
++frequency;
currentNode = currentNode.next;
}
return frequency;
}
/** Tests whether this bag contains a given entry.
* #param anEntry The entry to locate.
* #return True if the bag contains anEntry, or false if not. */
public boolean contains(T anEntry) {
Node currentNode = getReferenceTo (anEntry);
return !(currentNode == null);
}
/** Retrieves all entries that are in this bag.
* #return A newly allocated array of all the entries in the bag.
* Note: If the bag is empty, the returned array is empty. */
public T[] toArray() {
T[] newArray = (T[]) new Object[numPatrons];
int index = 0;
Node currentNode = firstNode;
while (currentNode != null) {
newArray[index] = currentNode.data;
++index;
currentNode = currentNode.next;
}
return newArray;
}
private class Node {
private T data;
private Node next;
private Node (T dataPortion) {
this(dataPortion, null);
}
private Node (T dataPortion, Node nextNode) {
this.data = dataPortion;
this.next = nextNode;
}
}
}
Here is my Main class:
package libraryPatrons;
import java.util.Scanner;
class Main {
public static void main (String args[]) {
LibraryPatron patron1 = new LibraryPatron("Bob", "3814872910", "Adelaide Way", "Belmont", "94002");
LibraryPatron patron2 = new LibraryPatron("Les", "3860165016", "Chevy St", "Belmont", "94002");
LibraryPatron patron3 = new LibraryPatron("Anna", "7926391055", "Davey Glen Rd", "Belmont", "94002");
LibraryPatron patron4 = new LibraryPatron("Amy", "7619356016", "Fernwood Way", "Belmont", "94002");
LibraryPatron patron5 = new LibraryPatron("Tom", "1758563947", "Flasner Ln", "Belmont", "94002");
LibraryPatron patron6 = new LibraryPatron("James", "4729573658", "Marsten Ave", "Belmont", "94002");
LibraryPatron patron7 = new LibraryPatron("Jason", "3858239773", "Middlesex Rd", "Belmont", "94002");
LibraryPatron patron8 = new LibraryPatron("Jess", "3866392656", "Oxford Ct", "Belmont", "94002");
LibraryPatron patron9 = new LibraryPatron("Mike", "7836591904", "Sem Ln", "Belmont", "94002");
LibraryPatron patron10 = new LibraryPatron("Abby", "1960265836", "Tioga Way", "Belmont", "94002");
LibraryPatron patron11 = new LibraryPatron("Dom", "5917485910", "Village Dr", "Belmont", "94002");
LibraryPatron patron12 = new LibraryPatron("Wes", "5810385736", "Willow Ln", "Belmont", "94002");
BagList<LibraryPatron> listOfPatrons = new BagList<LibraryPatron>();
listOfPatrons.add(patron1);
listOfPatrons.add(patron2);
listOfPatrons.add(patron3);
listOfPatrons.add(patron4);
listOfPatrons.add(patron5);
listOfPatrons.add(patron6);
listOfPatrons.add(patron7);
listOfPatrons.add(patron8);
listOfPatrons.add(patron9);
listOfPatrons.add(patron10);
listOfPatrons.add(patron11);
listOfPatrons.add(patron12);
Scanner patronInput = new Scanner(System.in);
String name = patronInput.nextLine();
System.out.println("Does this list of library patrons contain: " + name + "?");
System.out.println(/* contains(objectName)? */);
}
}
In the main() function, I want to ask for a LibraryPatron's name and use the contains() method I have to check if the LibraryPatron is part of the list.
After a few days researching why my Flink application is not working properly I've came to the conclusion that the problem resides in a MinMaxPriorityQueue I am using.
It seems that this structure is not serializable. I've tried several ways to serialize it:
env.getConfig.registerTypeWithKryoSerializer(classOf[MinMaxPriorityQueue[Double]], classOf[JavaSerializer])
env.getConfig.registerTypeWithKryoSerializer(classOf[MinMaxPriorityQueue[java.lang.Double]], classOf[ProtobufSerializer]);
env.getConfig().addDefaultKryoSerializer(MyCustomType.class, TBaseSerializer.class);
all of them without luck.
However I've found this: Serializing Guava's ImmutableTable
Is there an equivalent to MinMaxPriorityQueue, or a way to serialize it?
Update
I've translated Tomasz into scala:
class MinMaxPriorityQueueSerializer extends Serializer[MinMaxPriorityQueue[Object]] {
private[this] val log = LoggerFactory.getLogger(this.getClass)
setImmutable(false)
setAcceptsNull(false)
val OPTIMIZE_POSITIVE = true
override def read(kryo: Kryo, input: Input, aClass: Class[MinMaxPriorityQueue[Object]]): MinMaxPriorityQueue[Object] = {
log.error("Kryo READ")
val comparator: Ordering[Object] = kryo.readClassAndObject(input).asInstanceOf[Ordering[Object]]
val size = input.readInt(OPTIMIZE_POSITIVE)
val queue: MinMaxPriorityQueue[Object] = MinMaxPriorityQueue.orderedBy(comparator)
.expectedSize(size)
.create()
(0 to size).foreach(_ => queue.offer(kryo.readClassAndObject(input)))
queue
}
override def write(kryo: Kryo, output: Output, queue: MinMaxPriorityQueue[Object]): Unit = {
log.error("Kryo WRITE")
kryo.writeClassAndObject(output, queue.comparator)
val declaredSize = queue.size
output.writeInt(declaredSize, OPTIMIZE_POSITIVE)
val actualSize = queue.toArray.foldLeft(0) {
case (z, q) =>
kryo.writeClassAndObject(output, q)
z + 1
}
Preconditions.checkState(
declaredSize == actualSize,
"Declared size (%s) different than actual size (%s)", declaredSize, actualSize)
}
}
And set kryo in flink to use that Serializer:
env.getConfig.addDefaultKryoSerializer(classOf[MinMaxPriorityQueue[Double]], classOf[MinMaxPriorityQueueSerializer])
env.getConfig.registerTypeWithKryoSerializer(classOf[MinMaxPriorityQueue[Double]], classOf[MinMaxPriorityQueueSerializer])
However it seems it gets never called, since I do not see anywhere in the logs the outputs of log.error("Kryo READ") and log.error("Kryo WRITE")
And the transformation still returns an empty MinMaxPriorityQueue, even I am updating it.
Update 2
I've implemented the SerializerTester, but I am getting a bufferUnderflow:
object Main {
def main(args: Array[String]) {
val tester = new MinMaxPriorityQueueSerializerTester()
val inQueue: MinMaxPriorityQueue[java.lang.Double] = MinMaxPriorityQueue.create()
inQueue.add(1.0)
val outputStream = new ByteArrayOutputStream()
tester.serialize(outputStream, inQueue)
val inputStream = new ByteArrayInputStream(outputStream.toByteArray())
val outQueue: MinMaxPriorityQueue[java.lang.Double] = tester.deserialize(inputStream);
System.out.println(inQueue);
System.out.println(outQueue);
}
class MinMaxPriorityQueueSerializerTester {
val kryo = new Kryo
kryo.setInstantiatorStrategy(new StdInstantiatorStrategy)
registerMinMaxSerializer();
// allowForClassesWithoutNoArgConstructor(); // needed to serialize Ordering
def registerMinMaxSerializer() {
kryo.addDefaultSerializer(classOf[MinMaxPriorityQueue[java.lang.Double]], new MinMaxPriorityQueueSerializer());
}
def serialize(out: OutputStream, queue: MinMaxPriorityQueue[java.lang.Double]) {
// try (Output output = new Output(out)) {
val output = new Output(out)
kryo.writeClassAndObject(output, queue)
// kryo.writeObject(output, queue)
//}
output.flush
}
def deserialize(in: InputStream): MinMaxPriorityQueue[java.lang.Double] = {
//try (Input input = new Input(in)) {
val input = new Input(in)
//kryo.readObject(input, classOf[MinMaxPriorityQueue[java.lang.Double]])
kryo.readClassAndObject(input).asInstanceOf[MinMaxPriorityQueue[java.lang.Double]]
//p}
}
}
You can use a custom Kryo Serializer.
Here is a sample one (in Java):
class MinMaxPriorityQueueSerializer extends Serializer<MinMaxPriorityQueue<Object>> {
private static final boolean OPTIMIZE_POSITIVE = true;
protected MinMaxPriorityQueueSerializer() {
setAcceptsNull(false);
setImmutable(false);
}
#Override
public void write(Kryo kryo, Output output, MinMaxPriorityQueue<Object> queue) {
kryo.writeClassAndObject(output, queue.comparator());
int declaredSize = queue.size();
output.writeInt(declaredSize, OPTIMIZE_POSITIVE);
int actualSize = 0;
for (Object element : queue) {
kryo.writeClassAndObject(output, element);
actualSize++;
}
Preconditions.checkState(
declaredSize == actualSize,
"Declared size (%s) different than actual size (%s)", declaredSize, actualSize
);
}
#Override
public MinMaxPriorityQueue<Object> read(Kryo kryo, Input input, Class<MinMaxPriorityQueue<Object>> type) {
#SuppressWarnings("unchecked")
Comparator<Object> comparator = (Comparator<Object>) kryo.readClassAndObject(input);
int size = input.readInt(OPTIMIZE_POSITIVE);
MinMaxPriorityQueue<Object> queue = MinMaxPriorityQueue.orderedBy(comparator)
.expectedSize(size)
.create();
for (int i = 0; i < size; ++i) {
queue.offer(kryo.readClassAndObject(input));
}
return queue;
}
}
Here is how you could use it:
class MinMaxPriorityQueueSerializerTester {
public static void main(String[] args) {
MinMaxPriorityQueueSerializerTester tester = new MinMaxPriorityQueueSerializerTester();
MinMaxPriorityQueue<Integer> inQueue = MinMaxPriorityQueue.<Integer>orderedBy(Comparator.reverseOrder())
.create(Arrays.asList(5, 2, 7, 2, 4));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
tester.serialize(outputStream, inQueue);
ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
#SuppressWarnings("unchecked")
MinMaxPriorityQueue<Integer> outQueue = (MinMaxPriorityQueue<Integer>) tester.deserialize(inputStream);
System.out.println(inQueue);
System.out.println(outQueue);
}
private final Kryo kryo;
public MinMaxPriorityQueueSerializerTester() {
this.kryo = new Kryo();
registerMinMaxSerializer();
allowForClassesWithoutNoArgConstructor(); // needed to serialize Ordering
}
private void registerMinMaxSerializer() {
kryo.addDefaultSerializer(MinMaxPriorityQueue.class, new MinMaxPriorityQueueSerializer());
}
private void allowForClassesWithoutNoArgConstructor() {
((Kryo.DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy())
.setFallbackInstantiatorStrategy(new StdInstantiatorStrategy());
}
public void serialize(OutputStream out, MinMaxPriorityQueue<?> queue) {
try (Output output = new Output(out)) {
kryo.writeObject(output, queue);
}
}
public MinMaxPriorityQueue<?> deserialize(InputStream in) {
try (Input input = new Input(in)) {
return kryo.readObject(input, MinMaxPriorityQueue.class);
}
}
}
I finally give up and tried to use a different Data Structure and make it Serializable with java.io.Serializable.
This Data Structure is an IntervalHeap implemented here, I just made it Serializable in my project.
All works correctly now.
We are experiencing a java.util.ConcurrentModificationException using gwt 2.8.0, while calling iter.next() in the GWT emulated AbstractHashMap class (See stack trace and our CallbackTimer class below). The lowest point in the trace involving our code is on line 118, in the method private void tick(), a call to iter.next().
Drilling into trace, I see AbstractHashMap:
#Override
public Entry<K, V> next() {
checkStructuralChange(AbstractHashMap.this, this);
checkElement(hasNext());
last = current;
Entry<K, V> rv = current.next();
hasNext = computeHasNext();
return rv;
}
calling ConcurrentModificationDetector.checkStructuralChange:
public static void checkStructuralChange(Object host, Iterator<?> iterator) {
if (!API_CHECK) {
return;
}
if (JsUtils.getIntProperty(iterator, MOD_COUNT_PROPERTY)
!= JsUtils.getIntProperty(host, MOD_COUNT_PROPERTY)) {
throw new ConcurrentModificationException();
}
}
My understanding of the purpose of ConcurrentModificationException is to avoid having the collection change while it is being iterated over. I don't think iter.next() would fall into that category. Further, the only places I see the collection changing during iteration do so through the iterator itself. Am I missing something here? Any help would be appreciated!
Our Stack Trace:
java.util.ConcurrentModificationException
at Unknown.Throwable_1_g$(Throwable.java:61)
at Unknown.Exception_1_g$(Exception.java:25)
at Unknown.RuntimeException_1_g$(RuntimeException.java:25)
at Unknown.ConcurrentModificationException_1_g$(ConcurrentModificationException.java:25)
at Unknown.checkStructuralChange_0_g$(ConcurrentModificationDetector.java:54)
at Unknown.next_79_g$(AbstractHashMap.java:106)
at Unknown.next_78_g$(AbstractHashMap.java:105)
at Unknown.next_81_g$(AbstractMap.java:217)
at Unknown.tick_0_g$(CallbackTimer.java:118)
at Unknown.run_47_g$(CallbackTimer.java:41)
at Unknown.fire_0_g$(Timer.java:135)
at Unknown.anonymous(Timer.java:139)
at Unknown.apply_65_g$(Impl.java:239)
at Unknown.entry0_0_g$(Impl.java:291)
at Unknown.anonymous(Impl.java:77)
The source code for CallbackTimer.java is here:
package com.XXXXX.common.gwt.timer;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.common.base.Optional;
import com.google.gwt.user.client.Timer;
/**
* A {#link Timer} wrapper which allows for the registration of callbacks to be invoked after a given number of ticks.
* The timer will only run if at least one {#link TickCallback} is currently registered and will stop running when all
* callbacks have been unregistered.
*
* The intent of this class is to reduce overhead by allowing all callbacks in a GWT application to use the same
* Javascript timer.
*/
public class CallbackTimer
{
private static final Logger LOGGER = Logger.getLogger(CallbackTimer.class.getName());
private static final int MILLIS_IN_SEC = 1000;
private Timer timer;
private Map<Object, TickCallback> callbackRegistry = new HashMap<>();
public CallbackTimer()
{
timer = new Timer()
{
#Override
public void run()
{
try
{
tick();
}
catch(ConcurrentModificationException concurrentModificationException)
{
LOGGER.log(Level.WARNING, "Concurrent Modification Exception in " +
"CallbackTimer.tick()", concurrentModificationException);
}
}
};
}
public void registerCallback(Object key, TickCallback callback)
{
if (callbackRegistry.containsKey(key))
{
LOGGER.fine("Key " + key.toString() + " is being overwritten with a new callback.");
}
callbackRegistry.put(key, callback);
callback.markStartTime();
LOGGER.finer("Key " + key.toString() + " registered.");
if (!timer.isRunning())
{
startTimer();
}
}
public void unregisterCallback(Object key)
{
if (callbackRegistry.containsKey(key))
{
callbackRegistry.remove(key);
LOGGER.finer("Key " + key.toString() + " unregistered.");
if (callbackRegistry.isEmpty())
{
stopTimer();
}
}
else
{
LOGGER.info("Attempted to unregister key " + key.toString() + ", but this key has not been registered.");
}
}
private void unregisterCallback(Iterator<Object> iter, Object key)
{
iter.remove();
LOGGER.finer("Key " + key.toString() + " unregistered.");
if (callbackRegistry.isEmpty())
{
stopTimer();
}
}
public boolean keyIsRegistered(Object key)
{
return callbackRegistry.containsKey(key);
}
public TickCallback getCallback(Object key)
{
if (keyIsRegistered(key))
{
return callbackRegistry.get(key);
}
else
{
LOGGER.fine("Key " + key.toString() + " is not registered; returning null.");
return null;
}
}
private void tick()
{
long fireTimeMillis = System.currentTimeMillis();
Iterator<Object> iter = callbackRegistry.keySet().iterator();
while (iter.hasNext())
{
Object key = iter.next();//Lowest point in stack for our code
TickCallback callback = callbackRegistry.get(key);
if (callback.isFireTime(fireTimeMillis))
{
if (Level.FINEST.equals(LOGGER.getLevel()))
{
LOGGER.finest("Firing callback for key " + key.toString());
}
callback.onTick();
callback.markLastFireTime();
}
if (callback.shouldTerminate())
{
LOGGER.finer("Callback for key " + key.toString() +
" has reached its specified run-for-seconds and will now be unregistered.");
unregisterCallback(iter, key);
}
}
}
private void startTimer()
{
timer.scheduleRepeating(MILLIS_IN_SEC);
LOGGER.finer(this + " started.");
}
private void stopTimer()
{
timer.cancel();
LOGGER.finer(this + " stopped.");
}
/**
* A task to run on a given interval, with the option to specify a maximum number of seconds to run.
*/
public static abstract class TickCallback
{
private long intervalMillis;
private long startedAtMillis;
private long millisRunningAtLastFire;
private Optional<Long> runForMillis;
/**
* #param intervalSeconds
* The number of seconds which must elapse between each invocation of {#link #onTick()}.
* #param runForSeconds
* An optional maximum number of seconds to run for, after which the TickCallback will be eligible
* to be automatically unregistered. Pass {#link Optional#absent()} to specify that the TickCallback
* must be manually unregistered. Make this value the same as {#param intervalSeconds} to run the
* callback only once.
*/
public TickCallback(int intervalSeconds, Optional<Integer> runForSeconds)
{
this.intervalMillis = intervalSeconds * MILLIS_IN_SEC;
this.runForMillis = runForSeconds.isPresent() ?
Optional.of((long)runForSeconds.get() * MILLIS_IN_SEC) : Optional.<Long>absent();
}
private void markStartTime()
{
millisRunningAtLastFire = 0;
startedAtMillis = System.currentTimeMillis();
}
private void markLastFireTime()
{
millisRunningAtLastFire += intervalMillis;
}
private boolean isFireTime(long nowMillis)
{
return nowMillis - (startedAtMillis + millisRunningAtLastFire) >= intervalMillis;
}
private boolean shouldTerminate()
{
return runForMillis.isPresent() && System.currentTimeMillis() - startedAtMillis >= runForMillis.get();
}
/**
* A callback to be run every time intervalSeconds seconds have past since this callback was registered.
*/
public abstract void onTick();
}
}
Update 2017-06-08
I ended up following walen's first suggestion. I did not see where SimpleEventBus was the right tool for this specific job. I did, however, shamelessly steal SEBs method of integrating newly added/removed callbacks:
package com.XXXXX.common.gwt.timer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.common.base.Optional;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Timer;
/**
* A {#link Timer} wrapper which allows for the registration of callbacks to be invoked after a given number of ticks.
* The timer will only run if at least one {#link TickCallback} is currently registered and will stop running when all
* callbacks have been unregistered.
*
* The intent of this class is to reduce overhead by allowing all callbacks in a GWT application to use the same
* Javascript timer.
*/
public class CallbackTimer
{
private static final Logger LOGGER = Logger.getLogger(CallbackTimer.class.getName());
private static final int MILLIS_IN_SEC = 1000;
private Timer timer;
private Map<Object, TickCallback> callbackRegistry = new HashMap<>();
private List<Command> deferredDeltas = new ArrayList<>();
public CallbackTimer()
{
timer = new Timer()
{
#Override
public void run()
{
tick();
}
};
}
public void registerCallback(final Object key, final TickCallback callback)
{
deferredDeltas.add(new Command()
{
#Override
public void execute()
{
activateCallback(key, callback);
}
});
if (!timer.isRunning())
{
startTimer();
}
}
private void activateCallback(Object key, TickCallback callback)
{
if (callbackRegistry.containsKey(key))
{
LOGGER.fine("Key " + key.toString() + " is being overwritten with a new callback.");
}
callbackRegistry.put(key, callback);
callback.markStartTime();
LOGGER.finer("Key " + key.toString() + " registered.");
}
public void unregisterCallback(final Object key)
{
deferredDeltas.add(new Command()
{
#Override
public void execute()
{
deactivateCallback(key);
}
});
}
private void deactivateCallback(Object key)
{
if (callbackRegistry.containsKey(key))
{
callbackRegistry.remove(key);
LOGGER.fine("Key " + key.toString() + " unregistered.");
if (callbackRegistry.isEmpty())
{
stopTimer();
}
}
else
{
LOGGER.info("Attempted to unregister key " + key.toString() + ", but this key has not been registered.");
}
}
private void handleQueuedAddsAndRemoves()
{
for (Command c : deferredDeltas)
{
c.execute();
}
deferredDeltas.clear();
}
public boolean keyIsRegistered(Object key)
{
return callbackRegistry.containsKey(key);
}
private void tick()
{
handleQueuedAddsAndRemoves();
long fireTimeMillis = System.currentTimeMillis();
for (Map.Entry<Object, TickCallback> objectTickCallbackEntry : callbackRegistry.entrySet())
{
Object key = objectTickCallbackEntry.getKey();
TickCallback callback = objectTickCallbackEntry.getValue();
if (callback.isFireTime(fireTimeMillis))
{
if (Level.FINEST.equals(LOGGER.getLevel()))
{
LOGGER.finest("Firing callback for key " + key.toString());
}
callback.onTick();
callback.markLastFireTime();
}
if (callback.shouldTerminate())
{
LOGGER.finer("Callback for key " + key.toString() +
" has reached its specified run-for-seconds and will now be unregistered.");
unregisterCallback(key);
}
}
}
private void startTimer()
{
timer.scheduleRepeating(MILLIS_IN_SEC);
LOGGER.finer(this + " started.");
}
private void stopTimer()
{
timer.cancel();
LOGGER.finer(this + " stopped.");
}
/**
* A task to run on a given interval, with the option to specify a maximum number of seconds to run.
*/
public static abstract class TickCallback
{
private long intervalMillis;
private long startedAtMillis;
private long millisRunningAtLastFire;
private Optional<Long> runForMillis;
/**
* #param intervalSeconds The number of seconds which must elapse between each invocation of {#link #onTick()}.
* #param runForSeconds An optional maximum number of seconds to run for, after which the TickCallback will be
* eligible
* to be automatically unregistered. Pass {#link Optional#absent()} to specify that the TickCallback
* must be manually unregistered. Make this value the same as {#param intervalSeconds} to run the
* callback only once.
*/
protected TickCallback(int intervalSeconds, Optional<Integer> runForSeconds)
{
this.intervalMillis = intervalSeconds * MILLIS_IN_SEC;
this.runForMillis = runForSeconds.isPresent() ?
Optional.of((long) runForSeconds.get() * MILLIS_IN_SEC) : Optional.<Long>absent();
}
private void markStartTime()
{
millisRunningAtLastFire = 0;
startedAtMillis = System.currentTimeMillis();
}
private void markLastFireTime()
{
millisRunningAtLastFire += intervalMillis;
}
private boolean isFireTime(long nowMillis)
{
return nowMillis - (startedAtMillis + millisRunningAtLastFire) >= intervalMillis;
}
private boolean shouldTerminate()
{
return runForMillis.isPresent() && System.currentTimeMillis() - startedAtMillis >= runForMillis.get();
}
/**
* A callback to be run every time intervalSeconds seconds have past since this callback was registered.
*/
public abstract void onTick();
}
}
Your problem seems to be that new items (new keys) are being added to the map at the same time that the tick() method is trying to traverse its keySet.
Modifying a collection in any way while traversing it will throw a ConcurrentModificationException.
Using an iterator only lets you avoid that when removing items, but since there's no iterator.add() method, you can't add items safely.
If this was server-side code, you could use a ConcurrentHashMap, which guarantees that its iterator won't throw an exception in such case (at the expense of not guaranteeing that every item will be traversed, if it was added after iterator creation).
But ConcurrentHashMap is not (yet) supported by GWT's JRE emulation library, so you can't use it in client-side code.
You'll need to come up with a different way of adding items to your CallbackRegistry.
You could, for example, change your registerCallback() method so new items are added to a list / queue instead of the map, and then have the tick() method move those items from the queue to the map after it's done traversing the existing ones.
Alternatively, you could use SimpleEventBus as pointed out in Thomas Broyer's comment.
I am using Guava cache but for some reason my CacheBuilder is not returning already cached object, it always goes for expensive call. I am using this as follows;
EDIT
LoadingCache<ProfileDetails, OutAuthProfile> profileCache;
private void init(int maxCacheSize) {
// Check if we have it in our cache
profileCache = CacheBuilder.newBuilder().maximumSize(maxCacheSize) // maximum
// 100
// records
// can
// be
// cached
.build(new CacheLoader<ProfileDetails, OutAuthProfile>() { // build
// the
// cacheloader
#Override
public OutAuthProfile load(ProfileDetails profileDetails) throws Exception {
// make the expensive call
return getFilterAndSelectFromT24(profileDetails);
}
});
}
/**
* Get the AuthProfile for a provided InAuthProfile. This method will use cache, if not found in cache
* it will call T24 to retrieve the information and store for the next run
* #param profileDetails
* #return
*/
public OutAuthProfile getAuthProfileForSearchDb(ProfileDetails profileDetails) {
try {
return profileCache.get(profileDetails);
} catch (ExecutionException ee) {
ResponseDetails responseDetails = new ResponseDetails();
responseDetails.addError(new Response("EB.SMS-SERVICE-ERROR", CFConstants.RESPONSE_TYPE_FATAL_ERROR, ee
.getMessage(), null));
return new OutAuthProfile(null, null, responseDetails);
}
}
I am calling this as follows;
ProfileDetails profileDetails = T24AuthHelper.getProfile(userName, companyName, t24EntityName);
OutAuthProfile outProfile = authHelper.getAuthProfileForSearchDb(profileDetails);
Following is a basic implementation I am using for equals() and hasCode() in my ProfileDetails object;
public static ProfileDetails getProfile(String userName, String companyName, String t24EntityName) {
return new ProfileDetails(userName, companyName, t24EntityName) {
/**
*
*/
private static final long serialVersionUID = 1L;
#Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof ProfileDetails)) {
return false;
}
ProfileDetails other = (ProfileDetails) obj;
return this.getProfileName() == other.getProfileName() &&
this.getCompany() == other.getCompany() &&
this.getResourceName() == other.getResourceName();
}
#Override
public int hashCode() {
int hashCode = 0;
if (getProfileName()!= null)
hashCode += getProfileName().hashCode();
if (getCompany() != null)
hashCode += getCompany().hashCode();
if (getResourceName() != null)
hashCode += getResourceName().hashCode();
return hashCode;
}
};
}
Tests from guava-testlib;
#Test
public void testProfileEqualsImpl() {
ProfileDetails profile11 = T24AuthHelper.getProfile("Name1", "Company1", "Entity1");
ProfileDetails profile12 = T24AuthHelper.getProfile("Name1", "Company1", "Entity1");
ProfileDetails profile21 = T24AuthHelper.getProfile("Name2", "Company2", "Entity2");
ProfileDetails profile22 = T24AuthHelper.getProfile("Name2", "Company2", "Entity2");
EqualsTester eqTester = new EqualsTester()
.addEqualityGroup(profile11, profile12)
.addEqualityGroup(profile21, profile22);
eqTester.testEquals();
}
When running the solver of my problem, i get the following error message :
Exception executing consequence for rule "addMarks" in com.abcdl.be.solver: [Error: getEndTime(): null]
[Near : {... getEndTime() ....}]
...
The message says that the method getEndTime() in the rule "addMarks" returns null.
Here's the drools file :
// ############################################################################
// Hard constraints
// ############################################################################
rule "RespectDependencies" // Respect all the dependencies in the input file
when
Dependency(respected() == false)
then
scoreHolder.addHardConstraintMatch(kcontext, 0, -1);
end
rule "addMarks" //insert a Mark each time a process chain starts or ends
when
Node($startTime : getStartTime(), $endTime : getEndTime())
then
insertLogical(new Mark($startTime));
insertLogical(new Mark($endTime));
end
rule "resourcesLimit" // At any time, The number of resources used must not exceed the total number of resources available
when
Mark($startTime: time)
Mark(time > $startTime, $endTime : time)
not Mark(time > $startTime, time < $endTime)
$total : Number(intValue > Global.getInstance().getAvailableResources() ) from
accumulate(Node(getEndTime() >=$endTime, getStartTime()<= $startTime, $res : resources), sum($res))
then
scoreHolder.addHardConstraintMatch(kcontext, 1, (Global.getInstance().getAvailableResources() - $total.intValue()) * ($endTime - $startTime));
end
rule "masterDataManagement" // Parallel loading is forbidden
when
$n1 : Node(md != "", $md : md, $id : id)
$n2 : Node(id > $id, md == $md) // We make sure to check only different nodes through the condition "id > $id"
eval(Graph.getInstance().getPaths($n1, $n2).size() == 0)
then
scoreHolder.addHardConstraintMatch(kcontext, 2, -1);
end
// ############################################################################
// Soft constraints
// ############################################################################
rule "MaximizeResources" //Maximize use of available resources at any time
when
Mark($startTime: time)
Mark(time > $startTime, $endTime : time)
not Mark(time > $startTime, time < $endTime)
$total : Number(intValue < Global.getInstance().getAvailableResources() ) from
accumulate(Node(getEndTime() >=$endTime, getStartTime()<= $startTime, $res : resources), sum($res))
then
scoreHolder.addHardConstraintMatch(kcontext, 0, ($total.intValue() - Global.getInstance().getAvailableResources()) * ($endTime - $startTime));
end
rule "MinimizeTotalTime" // Minimize the total process time
when
Problem($totalTime : getTotalTime())
then
scoreHolder.addSoftConstraintMatch(kcontext, 1, -$totalTime);
end
Node is the planning entity and the methods getStartTime() and getEndTime() which return null are defined in the planning entity
The planning entity code :
#PlanningEntity(difficultyComparatorClass = NodeDifficultyComparator.class)
public class Node extends ProcessChain {
private Node parent; // Planning variable: changes during planning, between score calculations
private int id; // Used as an identifier for each node. Different nodes cannot have the same id
public Node(String name, String type, int time, int resources, String md, int id)
{
super(name, "", time, resources, "", type, md);
this.delay = "";
this.id = id;
}
public Node()
{
super();
this.delay = "";
}
#PlanningVariable(valueRangeProviderRefs = {"parentRange"}, nullable = false)
public Node getParent() {
return parent;
}
public void setParent(Node parent) {
this.parent = parent;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String toString()
{
if(this.type.equals("AND"))
return delay;
if(!this.md.isEmpty())
return Tools.excerpt(name+" : "+this.md);
return Tools.excerpt(name);
}
public boolean equals( Object o ) {
if (o == this)
return true;
if (o instanceof Node) {
return
this.name.equals(((Node)o).name);
} else {
return false;
}
}
// ************************************************************************
// Complex methods
// ************************************************************************
public int getStartTime()
{
return Graph.getInstance().getNode2times().get(this).getFirst();
}
public int getEndTime()
{
return Graph.getInstance().getNode2times().get(this).getSecond();
}
#ValueRangeProvider(id = "parentRange")
public Collection<Node> getPossibleParents()
{
Collection<Node> nodes = Graph.getInstance().getNodes();
nodes.remove(this); // We remove this node from the list
nodes.remove(Graph.getInstance().getParents(this)); // We remove its parents from the list
return nodes;
}
/**
* The normal methods {#link #equals(Object)} and {#link #hashCode()} cannot be used because the rule engine already
* requires them (for performance in their original state).
* #see #solutionHashCode()
*/
public boolean solutionEquals(Object o) {
if (this == o) {
return true;
} else if (o instanceof Node) {
Node other = (Node) o;
return new EqualsBuilder()
.append(name, other.name)
.isEquals();
} else {
return false;
}
}
/**
* The normal methods {#link #equals(Object)} and {#link #hashCode()} cannot be used because the rule engine already
* requires them (for performance in their original state).
* #see #solutionEquals(Object)
*/
public int solutionHashCode() {
return new HashCodeBuilder()
.append(name)
.toHashCode();
}
this is very strange cause node2times().get() does not return null for all the nodes in the Graph class. i did a test to make sure :
public class Graph {
private ArrayList<Node> nodes;
...
public void test()
{
for(Node node : nodes)
{
int time = 0;
try{
time = getNode2times().get(node).getFirst();
System.out.print(node+" : "+"Start time = "+time);
}
catch(NullPointerException e)
{
System.out.println("StartTime is null for node : " +node);
}
try{
time = node.getEndTime();
System.out.println(" End time = "+time);
}
catch(NullPointerException e)
{
System.out.println("EndTime is null for node : " +node);
}
}
}
...
}
You are overloading Node.equals() but not Node.hashCode().
You are using a map: Node to times (if I may trust the name you have used).
This violates the contract for using an object as a key in a HashMap.