From f2d4eba4b8439a9b4845c15442eb76e464e91297 Mon Sep 17 00:00:00 2001 From: Leo Gomes Date: Tue, 5 Apr 2011 02:40:58 +0200 Subject: [PATCH] JBRULES-2776 Left and Right Unlinking. --- .../integrationtests/LRUnlinkingTest.java | 118 +++++++++ .../StatelessSessionTest.java | 19 +- .../integrationtests/test_LRUnlinking.drl | 19 ++ .../org/drools/RuleBaseConfiguration.java | 49 ++++ .../drools/common/PropagationContextImpl.java | 45 ++++ .../marshalling/impl/InputMarshaller.java | 13 +- .../java/org/drools/reteoo/BetaMemory.java | 34 ++- .../main/java/org/drools/reteoo/BetaNode.java | 63 ++++- .../main/java/org/drools/reteoo/JoinNode.java | 62 +++++ .../org/drools/reteoo/ObjectTypeNode.java | 67 +++++- .../java/org/drools/reteoo/Unlinkable.java | 39 +++ .../org/drools/spi/PropagationContext.java | 19 +- .../org/drools/RuleBaseConfigurationTest.java | 16 ++ .../org/drools/reteoo/MockObjectSource.java | 4 + .../org/drools/reteoo/ObjectTypeNodeTest.java | 2 +- .../reteoo/test/JUnitNodeTestRunner.java | 3 +- .../org/drools/reteoo/test/NodeSuiteTest.java | 3 +- .../drools/reteoo/test/ReteDslTestEngine.java | 226 +++++++++++++++--- .../reteoo/test/ReteDslTestEngineTest.java | 34 ++- .../drools/reteoo/test/SingleTestCase.java | 1 + .../drools/reteoo/test/dsl/ConfigStep.java | 66 +++++ .../drools/reteoo/test/dsl/NodeTestCase.java | 64 +++-- .../ExistsNodeAssertRetractTest.nodeTestCase | 2 +- .../NotNodeAssertRetractTest.nodeTestCase | 2 +- .../test/RightUnlinkingComplex.nodeTestCase | 138 +++++++++++ .../test/RightUnlinkingModify.nodeTestCase | 70 ++++++ 26 files changed, 1085 insertions(+), 93 deletions(-) create mode 100644 drools-compiler/src/test/java/org/drools/integrationtests/LRUnlinkingTest.java create mode 100644 drools-compiler/src/test/resources/org/drools/integrationtests/test_LRUnlinking.drl create mode 100644 drools-core/src/main/java/org/drools/reteoo/Unlinkable.java create mode 100644 drools-core/src/test/java/org/drools/reteoo/test/dsl/ConfigStep.java create mode 100644 drools-core/src/test/resources/org/drools/reteoo/test/RightUnlinkingComplex.nodeTestCase create mode 100644 drools-core/src/test/resources/org/drools/reteoo/test/RightUnlinkingModify.nodeTestCase diff --git a/drools-compiler/src/test/java/org/drools/integrationtests/LRUnlinkingTest.java b/drools-compiler/src/test/java/org/drools/integrationtests/LRUnlinkingTest.java new file mode 100644 index 00000000000..e2f4c26439e --- /dev/null +++ b/drools-compiler/src/test/java/org/drools/integrationtests/LRUnlinkingTest.java @@ -0,0 +1,118 @@ +package org.drools.integrationtests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import org.drools.Person; +import org.drools.RuleBase; +import org.drools.RuleBaseConfiguration; +import org.drools.RuleBaseFactory; +import org.drools.WorkingMemory; +import org.drools.common.InternalFactHandle; +import org.drools.compiler.PackageBuilder; +import org.drools.rule.Package; +import org.junit.Test; + +public class LRUnlinkingTest { + + @Test + public void multipleJoinsUsingSameOTN() throws Exception { + + final PackageBuilder builder = new PackageBuilder(); + builder.addPackageFromDrl( new InputStreamReader( getClass().getResourceAsStream( "test_LRUnlinking.drl" ) ) ); + final Package pkg = builder.getPackage(); + + final RuleBaseConfiguration conf = new RuleBaseConfiguration(); + conf.setLRUnlinkingEnabled( true ); + RuleBase ruleBase = RuleBaseFactory.newRuleBase( conf ); + + ruleBase.addPackage( pkg ); + ruleBase = SerializationHelper.serializeObject( ruleBase ); + + final WorkingMemory wmOne = ruleBase.newStatefulSession(); + final WorkingMemory wmTwo = ruleBase.newStatefulSession(); + + final List listOne = new ArrayList(); + final List listTwo = new ArrayList(); + + wmOne.setGlobal( "results", + listOne ); + wmTwo.setGlobal( "results", + listTwo ); + + Person name = new Person(); + Person likes = new Person(); + Person age = new Person(); + Person hair = new Person(); + Person happy = new Person(); + Person match = new Person(); + + name.setName( "Ana" ); + likes.setLikes( "Chocolate" ); + age.setAge( 30 ); + hair.setHair( "brown" ); + happy.setHappy( true ); + + match.setName( "Leo" ); + match.setLikes( "Chocolate" ); + match.setAge( 30 ); + match.setHair( "brown" ); + match.setHappy( true ); + + // WM One - first round of inserts + wmOne.insert( name ); + wmOne.insert( likes ); + wmOne.insert( age ); + + wmOne.fireAllRules(); + + assertEquals( "Should not have fired", + 0, + listOne.size() ); + + // WM Two - first round o inserts + wmTwo.insert( name ); + wmTwo.insert( likes ); + wmTwo.insert( age ); + + wmTwo.fireAllRules(); + + assertEquals( "Should not have fired", + 0, + listTwo.size() ); + + wmOne.insert( hair ); + wmOne.insert( happy ); + InternalFactHandle matchHandle = (InternalFactHandle) wmOne.insert( match ); + + wmOne.fireAllRules(); + + assertTrue( "Should have fired", + listOne.size() > 0); + + assertEquals("Should have inserted the match Person", + matchHandle.getObject(), + listOne.get( 0 )); + + wmTwo.fireAllRules(); + + assertEquals( "Should not have fired", + 0, + listTwo.size() ); + + wmTwo.insert( hair ); + wmTwo.insert( happy ); + wmTwo.insert( match ); + + wmTwo.fireAllRules(); + + assertTrue( "Should have fired", + listTwo.size() > 0); + + } + +} diff --git a/drools-compiler/src/test/java/org/drools/integrationtests/StatelessSessionTest.java b/drools-compiler/src/test/java/org/drools/integrationtests/StatelessSessionTest.java index 905ec08f0c4..d86b0967715 100644 --- a/drools-compiler/src/test/java/org/drools/integrationtests/StatelessSessionTest.java +++ b/drools-compiler/src/test/java/org/drools/integrationtests/StatelessSessionTest.java @@ -1,20 +1,20 @@ package org.drools.integrationtests; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import static org.junit.Assert.*; - import org.drools.Cheese; import org.drools.Cheesery; import org.drools.KnowledgeBase; @@ -36,16 +36,13 @@ import org.drools.command.runtime.BatchExecutionCommandImpl; import org.drools.compiler.PackageBuilder; import org.drools.definition.KnowledgePackage; -import org.drools.impl.StatefulKnowledgeSessionImpl; import org.drools.io.Resource; import org.drools.io.ResourceFactory; import org.drools.rule.Package; import org.drools.runtime.ExecutionResults; -import org.drools.runtime.StatefulKnowledgeSession; import org.drools.runtime.StatelessKnowledgeSession; -import org.drools.runtime.StatelessKnowledgeSessionResults; -import org.drools.runtime.rule.impl.FlatQueryResults; import org.drools.spi.GlobalResolver; +import org.junit.Test; public class StatelessSessionTest { final List list = new ArrayList(); diff --git a/drools-compiler/src/test/resources/org/drools/integrationtests/test_LRUnlinking.drl b/drools-compiler/src/test/resources/org/drools/integrationtests/test_LRUnlinking.drl new file mode 100644 index 00000000000..e54b41d2027 --- /dev/null +++ b/drools-compiler/src/test/resources/org/drools/integrationtests/test_LRUnlinking.drl @@ -0,0 +1,19 @@ +package org.drools; + +global java.util.List results; + +rule "Test LR unlinking with multiple joins on the same object type" + when + $p1 : Person ($name : name != null) + $p2 : Person ($likes : likes != null) + $p3 : Person ($age : age != null) + $p4 : Person ($hair : hair != null) + $p5 : Person ($happy : happy != null) + $p6 : Person (name != $name, + likes == $likes, + age == $age, + hair == $hair, + happy == $happy) + then + results.add( $p6 ); +end \ No newline at end of file diff --git a/drools-core/src/main/java/org/drools/RuleBaseConfiguration.java b/drools-core/src/main/java/org/drools/RuleBaseConfiguration.java index 70b8d1972ff..cc78562dc18 100755 --- a/drools-core/src/main/java/org/drools/RuleBaseConfiguration.java +++ b/drools-core/src/main/java/org/drools/RuleBaseConfiguration.java @@ -27,6 +27,7 @@ import java.util.Properties; import org.drools.builder.conf.ClassLoaderCacheOption; +import org.drools.builder.conf.LRUnlinkingOption; import org.drools.common.AgendaGroupFactory; import org.drools.common.ArrayAgendaGroupFactory; import org.drools.common.PriorityQueueAgendaGroupFactory; @@ -102,6 +103,7 @@ * drools.multithreadEvaluation = <true|false> * drools.mbeans = <enabled|disabled> * drools.classLoaderCacheEnabled = <true|false> + * drools.lrUnlinkingEnabled = <true|false> * */ public class RuleBaseConfiguration @@ -133,6 +135,7 @@ public class RuleBaseConfiguration private String consequenceExceptionHandler; private String ruleBaseUpdateHandler; private boolean classLoaderCacheEnabled; + private boolean lrUnlinkingEnabled; private EventProcessingOption eventProcessingMode; @@ -184,6 +187,7 @@ public void writeExternal(ObjectOutput out) throws IOException { out.writeInt( maxThreads ); out.writeObject( eventProcessingMode ); out.writeBoolean( classLoaderCacheEnabled ); + out.writeBoolean( lrUnlinkingEnabled ); } public void readExternal(ObjectInput in) throws IOException, @@ -211,6 +215,7 @@ public void readExternal(ObjectInput in) throws IOException, maxThreads = in.readInt(); eventProcessingMode = (EventProcessingOption) in.readObject(); classLoaderCacheEnabled = in.readBoolean(); + lrUnlinkingEnabled = in.readBoolean(); } /** @@ -302,6 +307,8 @@ public void setProperty(String name, setMBeansEnabled( MBeansOption.isEnabled( value ) ); } else if ( name.equals( ClassLoaderCacheOption.PROPERTY_NAME ) ) { setClassLoaderCacheEnabled( StringUtils.isEmpty( value ) ? true : Boolean.valueOf( value ) ); + } else if ( name.equals( LRUnlinkingOption.PROPERTY_NAME ) ) { + setLRUnlinkingEnabled( StringUtils.isEmpty( value ) ? false : Boolean.valueOf( value ) ); } } @@ -355,6 +362,8 @@ public String getProperty(String name) { return isMBeansEnabled() ? "enabled" : "disabled"; } else if ( name.equals( ClassLoaderCacheOption.PROPERTY_NAME ) ) { return Boolean.toString( isClassLoaderCacheEnabled() ); + } else if ( name.equals( LRUnlinkingOption.PROPERTY_NAME ) ) { + return Boolean.toString( isLRUnlinkingEnabled() ); } return null; @@ -451,6 +460,10 @@ private void init(Properties properties, setClassLoaderCacheEnabled( Boolean.valueOf( this.chainedProperties.getProperty( ClassLoaderCacheOption.PROPERTY_NAME, "true" ) ) ); + + setLRUnlinkingEnabled( Boolean.valueOf( this.chainedProperties.getProperty( LRUnlinkingOption.PROPERTY_NAME, + "false" ) ) ); + } /** @@ -478,6 +491,9 @@ private void checkCanChange() { public void setSequential(boolean sequential) { this.sequential = sequential; + if (sequential && isLRUnlinkingEnabled()) { + throw new IllegalArgumentException( "Sequential mode cannot be used when Left & Right unlinking is enabled." ); + } } public boolean isSequential() { @@ -648,6 +664,9 @@ public void setSequentialAgenda(final SequentialAgenda sequentialAgenda) { public void setMultithreadEvaluation(boolean enableMultithread) { checkCanChange(); this.multithread = enableMultithread; + if (multithread && isLRUnlinkingEnabled()) { + throw new IllegalArgumentException( "Multithread evaluation cannot be used when Left & Right Unlinking is enabled." ); + } } /** @@ -693,6 +712,32 @@ public void setClassLoaderCacheEnabled(final boolean classLoaderCacheEnabled) { this.classLoaderCacheEnabled = classLoaderCacheEnabled; this.classLoader.setCachingEnabled( this.classLoaderCacheEnabled ); } + + /** + * @return whether or not Left & Right Unlinking is enabled. + */ + public boolean isLRUnlinkingEnabled() { + return this.lrUnlinkingEnabled; + } + + /** + * Enable Left & Right Unlinking. It will also disable sequential mode + * and multithread evaluation as these are incompatible with L&R unlinking. + * @param enabled + */ + public void setLRUnlinkingEnabled(boolean enabled) { + checkCanChange(); // throws an exception if a change isn't possible; + this.lrUnlinkingEnabled = enabled; + + if ( enabled && isSequential() ) { + throw new IllegalArgumentException( "Sequential mode cannot be used when Left & Right Unlinking is enabled." ); + } + + if ( enabled && isMultithreadEvaluation() ) { + throw new IllegalArgumentException( "Multithread evaluation cannot be used when Left & Right Unlinking is enabled." ); + } + } + public List> getWorkDefinitions() { if ( this.workDefinitions == null ) { @@ -1080,6 +1125,8 @@ public T getOption(Class option) { return (T) (this.isMBeansEnabled() ? MBeansOption.ENABLED : MBeansOption.DISABLED); } else if ( ClassLoaderCacheOption.class.equals( option ) ) { return (T) (this.isClassLoaderCacheEnabled() ? ClassLoaderCacheOption.ENABLED : ClassLoaderCacheOption.DISABLED); + } else if ( LRUnlinkingOption.class.equals( option ) ) { + return (T) (this.isLRUnlinkingEnabled() ? LRUnlinkingOption.ENABLED : LRUnlinkingOption.DISABLED); } return null; @@ -1122,6 +1169,8 @@ public void setOption(T option) { setMBeansEnabled( ((MBeansOption) option).isEnabled() ); } else if ( option instanceof ClassLoaderCacheOption ) { setClassLoaderCacheEnabled( ((ClassLoaderCacheOption) option).isClassLoaderCacheEnabled() ); + } else if ( option instanceof LRUnlinkingOption ) { + setLRUnlinkingEnabled( ((LRUnlinkingOption) option).isLRUnlinkingEnabled() ); } } diff --git a/drools-core/src/main/java/org/drools/common/PropagationContextImpl.java b/drools-core/src/main/java/org/drools/common/PropagationContextImpl.java index 3b4775c2f02..9eab52e0346 100644 --- a/drools-core/src/main/java/org/drools/common/PropagationContextImpl.java +++ b/drools-core/src/main/java/org/drools/common/PropagationContextImpl.java @@ -21,7 +21,9 @@ import java.io.ObjectOutput; import org.drools.FactHandle; +import org.drools.core.util.ObjectHashSet; import org.drools.reteoo.LeftTuple; +import org.drools.reteoo.ObjectTypeNode; import org.drools.rule.EntryPoint; import org.drools.rule.Rule; import org.drools.spi.PropagationContext; @@ -49,6 +51,13 @@ public class PropagationContextImpl private EntryPoint entryPoint; private int originOffset; + + private ObjectHashSet propagationAttempts; + + private ObjectTypeNode currentPropagatingOTN; + + private boolean shouldPropagateAll; + public PropagationContextImpl() { @@ -68,6 +77,7 @@ public PropagationContextImpl(final long number, this.dormantActivations = 0; this.entryPoint = EntryPoint.DEFAULT; this.originOffset = -1; + this.shouldPropagateAll = true; } public PropagationContextImpl(final long number, @@ -99,6 +109,9 @@ public void readExternal(ObjectInput in) throws IOException, this.leftTuple = (LeftTuple) in.readObject(); this.entryPoint = (EntryPoint) in.readObject(); this.originOffset = in.readInt(); + this.propagationAttempts = (ObjectHashSet) in.readObject(); + this.currentPropagatingOTN = (ObjectTypeNode) in.readObject(); + this.shouldPropagateAll = in.readBoolean(); } public void writeExternal(ObjectOutput out) throws IOException { @@ -110,6 +123,9 @@ public void writeExternal(ObjectOutput out) throws IOException { out.writeObject( this.leftTuple ); out.writeObject( this.entryPoint ); out.writeInt( this.originOffset ); + out.writeObject( this.propagationAttempts ); + out.writeObject( this.currentPropagatingOTN ); + out.writeObject( this.shouldPropagateAll ); } public long getPropagationNumber() { @@ -188,6 +204,35 @@ public int getOriginOffset() { public void setOriginOffset(int originOffset) { this.originOffset = originOffset; } + + public ObjectHashSet getPropagationAttemptsMemory() { + + if (this.propagationAttempts == null) { + this.propagationAttempts = new ObjectHashSet(); + } + + return this.propagationAttempts; + } + + public boolean isPropagating(ObjectTypeNode otn) { + return this.currentPropagatingOTN != null + && this.currentPropagatingOTN.equals( otn ); + } + + public void setCurrentPropagatingOTN(ObjectTypeNode otn) { + this.currentPropagatingOTN = otn; + } + + public void setShouldPropagateAll(Object node) { + this.shouldPropagateAll = getPropagationAttemptsMemory().contains( node ); + } + + public boolean shouldPropagateAll() { + return this.shouldPropagateAll; + } + + + @Override public String toString() { diff --git a/drools-core/src/main/java/org/drools/marshalling/impl/InputMarshaller.java b/drools-core/src/main/java/org/drools/marshalling/impl/InputMarshaller.java index 130b6d67668..ace6465562e 100644 --- a/drools-core/src/main/java/org/drools/marshalling/impl/InputMarshaller.java +++ b/drools-core/src/main/java/org/drools/marshalling/impl/InputMarshaller.java @@ -439,6 +439,7 @@ public static void readRightTuple(MarshallerReaderContext context, } } memory.getRightTupleMemory().add( rightTuple ); + memory.linkLeft(); } } @@ -469,7 +470,7 @@ public static void readLeftTuple(LeftTuple parentLeftTuple, switch ( sink.getType() ) { case NodeTypeEnums.JoinNode : { BetaMemory memory = (BetaMemory) context.wm.getNodeMemory( (BetaNode) sink ); - memory.getLeftTupleMemory().add( parentLeftTuple ); + addToLeftMemory( parentLeftTuple, memory ); while ( stream.readShort() == PersisterEnums.RIGHT_TUPLE ) { LeftTupleSink childSink = (LeftTupleSink) sinks.get( stream.readInt() ); @@ -503,7 +504,7 @@ public static void readLeftTuple(LeftTuple parentLeftTuple, BetaMemory memory = (BetaMemory) context.wm.getNodeMemory( (BetaNode) sink ); int type = stream.readShort(); if ( type == PersisterEnums.LEFT_TUPLE_NOT_BLOCKED ) { - memory.getLeftTupleMemory().add( parentLeftTuple ); + addToLeftMemory( parentLeftTuple, memory ); while ( stream.readShort() == PersisterEnums.LEFT_TUPLE ) { LeftTupleSink childSink = (LeftTupleSink) sinks.get( stream.readInt() ); @@ -529,7 +530,7 @@ public static void readLeftTuple(LeftTuple parentLeftTuple, BetaMemory memory = (BetaMemory) context.wm.getNodeMemory( (BetaNode) sink ); int type = stream.readShort(); if ( type == PersisterEnums.LEFT_TUPLE_NOT_BLOCKED ) { - memory.getLeftTupleMemory().add( parentLeftTuple ); + addToLeftMemory( parentLeftTuple, memory ); } else { int factHandleId = stream.readInt(); RightTupleKey key = new RightTupleKey( factHandleId, @@ -661,6 +662,12 @@ public static void readLeftTuple(LeftTuple parentLeftTuple, } } } + + private static void addToLeftMemory(LeftTuple parentLeftTuple, + BetaMemory memory) { + memory.getLeftTupleMemory().add( parentLeftTuple ); + memory.linkRight(); + } public static void readActivations(MarshallerReaderContext context) throws IOException { ObjectInputStream stream = context.stream; diff --git a/drools-core/src/main/java/org/drools/reteoo/BetaMemory.java b/drools-core/src/main/java/org/drools/reteoo/BetaMemory.java index cd68c99ec12..d93928237d9 100644 --- a/drools-core/src/main/java/org/drools/reteoo/BetaMemory.java +++ b/drools-core/src/main/java/org/drools/reteoo/BetaMemory.java @@ -26,7 +26,7 @@ public class BetaMemory implements - Externalizable { + Externalizable, Unlinkable { private static final long serialVersionUID = 510l; @@ -36,6 +36,10 @@ public class BetaMemory private ContextEntry[] context; private Object behaviorContext; private boolean open; + + /* Let's start with only right unlinked. */ + private boolean isLeftUnlinked = false; + private boolean isRightUnlinked = true; public BetaMemory() { } @@ -56,6 +60,8 @@ public void readExternal(ObjectInput in) throws IOException, context = (ContextEntry[]) in.readObject(); behaviorContext = (Object) in.readObject(); open = ( boolean ) in.readBoolean(); + isLeftUnlinked = in.readBoolean(); + isRightUnlinked = in.readBoolean(); } public void writeExternal(ObjectOutput out) throws IOException { @@ -65,6 +71,8 @@ public void writeExternal(ObjectOutput out) throws IOException { out.writeObject( context ); out.writeObject( behaviorContext ); out.writeBoolean( open ); + out.writeBoolean( isLeftUnlinked ); + out.writeBoolean( isRightUnlinked ); } public RightTupleMemory getRightTupleMemory() { @@ -105,5 +113,27 @@ public void setOpen(boolean open) { this.open = open; } - + public boolean isLeftUnlinked() { + return this.isLeftUnlinked; + } + + public boolean isRightUnlinked() { + return this.isRightUnlinked; + } + + public void linkLeft() { + this.isLeftUnlinked = false; + } + + public void linkRight() { + this.isRightUnlinked = false; + } + + public void unlinkLeft() { + this.isLeftUnlinked = true; + } + + public void unlinkRight() { + this.isRightUnlinked = true; + } } diff --git a/drools-core/src/main/java/org/drools/reteoo/BetaNode.java b/drools-core/src/main/java/org/drools/reteoo/BetaNode.java index 029b8d8a3e4..016bfb20344 100644 --- a/drools-core/src/main/java/org/drools/reteoo/BetaNode.java +++ b/drools-core/src/main/java/org/drools/reteoo/BetaNode.java @@ -91,10 +91,13 @@ public abstract class BetaNode extends LeftTupleSource protected boolean objectMemory = true; // hard coded to true protected boolean tupleMemoryEnabled; protected boolean concurrentRightTupleMemory = false; - + + /** @see LRUnlinkingOption */ + protected boolean lrUnlinkingEnabled = false; private boolean indexedUnificationJoin; + // ------------------------------------------------------------ // Constructors // ------------------------------------------------------------ @@ -144,6 +147,7 @@ public void readExternal(ObjectInput in) throws IOException, objectMemory = in.readBoolean(); tupleMemoryEnabled = in.readBoolean(); concurrentRightTupleMemory = in.readBoolean(); + lrUnlinkingEnabled = in.readBoolean(); setUnificationJoin(); super.readExternal( in ); } @@ -168,6 +172,7 @@ public void writeExternal(ObjectOutput out) throws IOException { out.writeBoolean( objectMemory ); out.writeBoolean( tupleMemoryEnabled ); out.writeBoolean( concurrentRightTupleMemory ); + out.writeBoolean( lrUnlinkingEnabled ); super.writeExternal( out ); } @@ -279,10 +284,23 @@ public void attach(final InternalWorkingMemory[] workingMemories) { PropagationContext.RULE_ADDITION, null, null, - null ); - this.rightInput.updateSink( this, - propagationContext, - workingMemory ); + null); + + /* FIXME: This should be generalized at BetaNode level and the + * instanceof should be removed! + * + * When L&R Unlinking is enabled, we only need to update the side + * that is initially linked. If there are tuples to be propagated, + * they will trigger the update (thus, population) of the other side. + * */ + if (!lrUnlinkingEnabled || + (lrUnlinkingEnabled && !(this instanceof JoinNode) )) { + + this.rightInput.updateSink( this, + propagationContext, + workingMemory ); + } + this.leftInput.updateSink( this, propagationContext, workingMemory ); @@ -554,4 +572,39 @@ public RightTuple createRightTuple(InternalFactHandle handle, } } + protected boolean leftUnlinked(final PropagationContext context, + final InternalWorkingMemory workingMemory, final BetaMemory memory) { + + // If left input is unlinked, don't do anything. + if(memory.isLeftUnlinked()) { + return true; + } + + if (memory.isRightUnlinked()) { + memory.linkRight(); + context.setShouldPropagateAll( this ); + // updates the right input memory before going on. + this.rightInput.updateSink(this, context, workingMemory); + } + + return false; + } + + protected boolean rightUnlinked(final PropagationContext context, + final InternalWorkingMemory workingMemory, final BetaMemory memory) { + + if (memory.isRightUnlinked()) { + return true; + } + + if (memory.isLeftUnlinked()) { + + memory.linkLeft(); + context.setShouldPropagateAll( this ); + // updates the left input memory before going on. + this.leftInput.updateSink(this, context, workingMemory); + } + + return false; + } } diff --git a/drools-core/src/main/java/org/drools/reteoo/JoinNode.java b/drools-core/src/main/java/org/drools/reteoo/JoinNode.java index 22f87a834e3..ec308c713fd 100644 --- a/drools-core/src/main/java/org/drools/reteoo/JoinNode.java +++ b/drools-core/src/main/java/org/drools/reteoo/JoinNode.java @@ -56,12 +56,22 @@ public JoinNode(final int id, binder, behaviors ); this.tupleMemoryEnabled = context.isTupleMemoryEnabled(); + this.lrUnlinkingEnabled = context.getRuleBase().getConfiguration().isLRUnlinkingEnabled(); } public void assertLeftTuple(final LeftTuple leftTuple, final PropagationContext context, final InternalWorkingMemory workingMemory) { final BetaMemory memory = (BetaMemory) workingMemory.getNodeMemory( this ); + + + if(lrUnlinkingEnabled && + leftUnlinked(context, + workingMemory, + memory)) { + return; + } + memory.setOpen( true ); RightTupleMemory rightMemory = memory.getRightTupleMemory(); @@ -116,6 +126,15 @@ public void assertObject(final InternalFactHandle factHandle, final PropagationContext context, final InternalWorkingMemory workingMemory) { final BetaMemory memory = (BetaMemory) workingMemory.getNodeMemory( this ); + + if (lrUnlinkingEnabled && + rightUnlinked(context, + workingMemory, + memory)) { + context.getPropagationAttemptsMemory().add( this ); + return; + } + RightTuple rightTuple = createRightTuple( factHandle, this ); @@ -159,6 +178,13 @@ public void retractRightTuple(final RightTuple rightTuple, final PropagationContext context, final InternalWorkingMemory workingMemory) { final BetaMemory memory = (BetaMemory) workingMemory.getNodeMemory( this ); + + if (lrUnlinkingEnabled && + memory.isRightUnlinked()) { + return; + } + + behavior.retractRightTuple( memory.getBehaviorContext(), rightTuple, workingMemory ); @@ -175,6 +201,12 @@ public void retractLeftTuple(final LeftTuple leftTuple, final PropagationContext context, final InternalWorkingMemory workingMemory) { final BetaMemory memory = (BetaMemory) workingMemory.getNodeMemory( this ); + + if (lrUnlinkingEnabled && + memory.isLeftUnlinked()) { + return; + } + memory.getLeftTupleMemory().remove( leftTuple ); if ( leftTuple.firstChild != null ) { this.sink.propagateRetractLeftTuple( leftTuple, @@ -398,6 +430,36 @@ public void updateSink(final LeftTupleSink sink, this.constraints.resetTuple( memory.getContext() ); } } + + @Override + public void modifyLeftTuple(InternalFactHandle factHandle, + ModifyPreviousTuples modifyPreviousTuples, + PropagationContext context, InternalWorkingMemory workingMemory) { + + BetaMemory memory = (BetaMemory) workingMemory.getNodeMemory(this); + + if (lrUnlinkingEnabled && + memory.isLeftUnlinked()) + return; + + super.modifyLeftTuple(factHandle, modifyPreviousTuples, context, workingMemory); + } + + @Override + public void modifyObject(InternalFactHandle factHandle, + ModifyPreviousTuples modifyPreviousTuples, + PropagationContext context, InternalWorkingMemory workingMemory) { + + + BetaMemory memory = (BetaMemory) workingMemory.getNodeMemory(this); + + if (lrUnlinkingEnabled && + memory.isRightUnlinked()) + return; + + super.modifyObject(factHandle, modifyPreviousTuples, context, workingMemory); + } + public short getType() { return NodeTypeEnums.JoinNode; diff --git a/drools-core/src/main/java/org/drools/reteoo/ObjectTypeNode.java b/drools-core/src/main/java/org/drools/reteoo/ObjectTypeNode.java index 3422a9e91e0..53f4b565243 100644 --- a/drools-core/src/main/java/org/drools/reteoo/ObjectTypeNode.java +++ b/drools-core/src/main/java/org/drools/reteoo/ObjectTypeNode.java @@ -93,6 +93,9 @@ public class ObjectTypeNode extends ObjectSource private CompiledNetwork compiledNetwork; + /** @see LRUnlinkingOption */ + private boolean lrUnlinkingEnabled = false; + public ObjectTypeNode() { } @@ -114,6 +117,7 @@ public ObjectTypeNode(final int id, source, context.getRuleBase().getConfiguration().getAlphaNodeHashingThreshold() ); this.objectType = objectType; + this.lrUnlinkingEnabled = context.getRuleBase().getConfiguration().isLRUnlinkingEnabled(); setObjectMemoryEnabled( context.isObjectTypeNodeMemoryEnabled() ); } @@ -131,6 +135,7 @@ public void readExternal(ObjectInput in) throws IOException, skipOnModify = in.readBoolean(); objectMemoryEnabled = in.readBoolean(); expirationOffset = in.readLong(); + lrUnlinkingEnabled = in.readBoolean(); } public void writeExternal(ObjectOutput out) throws IOException { @@ -139,6 +144,7 @@ public void writeExternal(ObjectOutput out) throws IOException { out.writeBoolean( skipOnModify ); out.writeBoolean( objectMemoryEnabled ); out.writeLong( expirationOffset ); + out.writeBoolean( lrUnlinkingEnabled ); } /** @@ -182,6 +188,8 @@ public void assertObject(final InternalFactHandle factHandle, context, workingMemory ); } else { + + context.setCurrentPropagatingOTN( this ); this.sink.propagateAssertObject( factHandle, context, workingMemory ); @@ -265,15 +273,64 @@ public void modifyObject(InternalFactHandle factHandle, public void updateSink(final ObjectSink sink, final PropagationContext context, final InternalWorkingMemory workingMemory) { + if (lrUnlinkingEnabled) { + // Update sink taking into account L&R unlinking peculiarities + updateLRUnlinking(sink, context, workingMemory); + + } else { + // Regular updateSink + final ObjectHashSet memory = (ObjectHashSet) workingMemory.getNodeMemory( this ); + Iterator it = memory.iterator(); + + for ( ObjectEntry entry = (ObjectEntry) it.next(); entry != null; entry = (ObjectEntry) it.next() ) { + sink.assertObject( (InternalFactHandle) entry.getValue(), + context, + workingMemory ); + } + } + + } + + /** + * When L&R Unlinking is enabled, updateSink() is used to populate + * a node's memory, but it has to take into account if it's propagating. + */ + private void updateLRUnlinking(final ObjectSink sink, + final PropagationContext context, + final InternalWorkingMemory workingMemory) { + final ObjectHashSet memory = (ObjectHashSet) workingMemory.getNodeMemory( this ); - Iterator it = memory.iterator(); - for ( ObjectEntry entry = (ObjectEntry) it.next(); entry != null; entry = (ObjectEntry) it.next() ) { - sink.assertObject( (InternalFactHandle) entry.getValue(), - context, - workingMemory ); + + Iterator it = memory.iterator(); + + + InternalFactHandle ctxHandle = (InternalFactHandle)context.getFactHandle(); + + if (!context.isPropagating( this ) || + (context.isPropagating( this ) && context.shouldPropagateAll())){ + + for ( ObjectEntry entry = (ObjectEntry) it.next(); entry != null; entry = (ObjectEntry) it.next() ) { + // Assert everything + sink.assertObject( (InternalFactHandle) entry.getValue(), + context, + workingMemory ); + } + + } else { + + for ( ObjectEntry entry = (ObjectEntry) it.next(); entry != null; entry = (ObjectEntry) it.next() ) { + InternalFactHandle handle = (InternalFactHandle) entry.getValue(); + // Exclude the current fact propagation + if (handle.getId() != ctxHandle.getId()) { + sink.assertObject( handle, + context, + workingMemory ); + } + } } } + /** * Rete needs to know that this ObjectTypeNode has been added */ diff --git a/drools-core/src/main/java/org/drools/reteoo/Unlinkable.java b/drools-core/src/main/java/org/drools/reteoo/Unlinkable.java new file mode 100644 index 00000000000..470a4bbb9c7 --- /dev/null +++ b/drools-core/src/main/java/org/drools/reteoo/Unlinkable.java @@ -0,0 +1,39 @@ +/** + * Copyright 2010 JBoss Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.drools.reteoo; + +/** + * A marker interface to specify unlinkable node memories. + * + * @author lgomes + */ +public interface Unlinkable { + + /** Whether or not the left side is unlinked. */ + boolean isLeftUnlinked(); + + /** Whether or not the right side is unlinked. */ + boolean isRightUnlinked(); + + void linkLeft(); + + void linkRight(); + + void unlinkLeft(); + + void unlinkRight(); + +} diff --git a/drools-core/src/main/java/org/drools/spi/PropagationContext.java b/drools-core/src/main/java/org/drools/spi/PropagationContext.java index db7bfe73096..c2ee4afc93c 100644 --- a/drools-core/src/main/java/org/drools/spi/PropagationContext.java +++ b/drools-core/src/main/java/org/drools/spi/PropagationContext.java @@ -18,10 +18,12 @@ import java.io.Externalizable; +import org.drools.FactHandle; +import org.drools.core.util.ObjectHashSet; import org.drools.reteoo.LeftTuple; +import org.drools.reteoo.ObjectTypeNode; import org.drools.rule.EntryPoint; import org.drools.rule.Rule; -import org.drools.FactHandle; public interface PropagationContext extends @@ -59,5 +61,20 @@ public interface PropagationContext public EntryPoint getEntryPoint(); + /** When L&R unlinking is active, we need to keep + * track of the OTN that triggered this propagation. */ + public void setCurrentPropagatingOTN(ObjectTypeNode otn); + + public boolean isPropagating(ObjectTypeNode otn); + + public boolean shouldPropagateAll(); + + public void setShouldPropagateAll(Object node); + + /** Keeps a list of nodes to which a propagation attempt fail + * because the node was unlinked. */ + public ObjectHashSet getPropagationAttemptsMemory(); + + } diff --git a/drools-core/src/test/java/org/drools/RuleBaseConfigurationTest.java b/drools-core/src/test/java/org/drools/RuleBaseConfigurationTest.java index e6d60e11140..bf0b3bb1a9f 100644 --- a/drools-core/src/test/java/org/drools/RuleBaseConfigurationTest.java +++ b/drools-core/src/test/java/org/drools/RuleBaseConfigurationTest.java @@ -120,5 +120,21 @@ public void testSequential() { assertEquals( SequentialAgenda.DYNAMIC, cfg.getSequentialAgenda() ); assertTrue( cfg.getAgendaGroupFactory() instanceof PriorityQueueAgendaGroupFactory ); } + + @Test + public void testLRUnlinking() { + RuleBaseConfiguration cfg = new RuleBaseConfiguration(); + assertEquals( false, + cfg.isLRUnlinkingEnabled() ); + + Properties properties = new Properties(); + properties.setProperty( "drools.lrUnlinkingEnabled", + "true" ); + cfg = new RuleBaseConfiguration( properties ); + + assertEquals( true, + cfg.isLRUnlinkingEnabled() ); + } + } diff --git a/drools-core/src/test/java/org/drools/reteoo/MockObjectSource.java b/drools-core/src/test/java/org/drools/reteoo/MockObjectSource.java index 6430ae588b9..622045150e7 100644 --- a/drools-core/src/test/java/org/drools/reteoo/MockObjectSource.java +++ b/drools-core/src/test/java/org/drools/reteoo/MockObjectSource.java @@ -75,6 +75,10 @@ public int getUdated() { public void addFact(final InternalFactHandle handle) { this.facts.add( handle ); } + + public void removeFact(final InternalFactHandle handle) { + this.facts.remove( handle ); + } public void updateSink(final ObjectSink sink, final PropagationContext context, diff --git a/drools-core/src/test/java/org/drools/reteoo/ObjectTypeNodeTest.java b/drools-core/src/test/java/org/drools/reteoo/ObjectTypeNodeTest.java index 15f95b1b296..d5d5d7ee626 100644 --- a/drools-core/src/test/java/org/drools/reteoo/ObjectTypeNodeTest.java +++ b/drools-core/src/test/java/org/drools/reteoo/ObjectTypeNodeTest.java @@ -341,7 +341,7 @@ public void testUpdateSink() throws FactException { sink2.getAsserted().size() ); objectTypeNode.updateSink( sink2, - null, + new PropagationContextImpl(), workingMemory ); assertEquals( 2, diff --git a/drools-core/src/test/java/org/drools/reteoo/test/JUnitNodeTestRunner.java b/drools-core/src/test/java/org/drools/reteoo/test/JUnitNodeTestRunner.java index e33f80ae655..3487426f66c 100755 --- a/drools-core/src/test/java/org/drools/reteoo/test/JUnitNodeTestRunner.java +++ b/drools-core/src/test/java/org/drools/reteoo/test/JUnitNodeTestRunner.java @@ -46,7 +46,8 @@ public JUnitNodeTestRunner(Class< ? > clazz) { this.descr = Description.createSuiteDescription( "Node test case suite" ); for ( NodeTestCase tcase : testCases ) { - Description tcaseDescr = Description.createSuiteDescription( tcase.getName() ); + Description tcaseDescr = Description.createSuiteDescription( tcase.getFileName() != null ? + tcase.getFileName() : tcase.getName() ); tcase.setDescription( tcaseDescr ); this.descr.addChild( tcaseDescr ); for ( NodeTestDef ntest : tcase.getTests() ) { diff --git a/drools-core/src/test/java/org/drools/reteoo/test/NodeSuiteTest.java b/drools-core/src/test/java/org/drools/reteoo/test/NodeSuiteTest.java index 90e44d127dd..b4f301905cf 100755 --- a/drools-core/src/test/java/org/drools/reteoo/test/NodeSuiteTest.java +++ b/drools-core/src/test/java/org/drools/reteoo/test/NodeSuiteTest.java @@ -43,11 +43,12 @@ public List getTestCases() throws Exception { for ( File file : base.listFiles( new FilenameFilter() { public boolean accept(File arg0, String arg1) { - return arg1.endsWith( ".nodeTestCase" ); + return arg1.endsWith( NodeTestCase.SUFFIX ); } } ) ) { InputStream is = new FileInputStream( file ); NodeTestCase tcase = ReteDslTestEngine.compile( is ); + tcase.setFileName(file.getName()); if ( tcase.hasErrors() ) { throw new IllegalArgumentException( "Error parsing and loading testcase: " + file.getAbsolutePath() + "\n" + tcase.getErrors().toString() ); } diff --git a/drools-core/src/test/java/org/drools/reteoo/test/ReteDslTestEngine.java b/drools-core/src/test/java/org/drools/reteoo/test/ReteDslTestEngine.java index 48cea9c5c3f..423c33012a9 100644 --- a/drools-core/src/test/java/org/drools/reteoo/test/ReteDslTestEngine.java +++ b/drools-core/src/test/java/org/drools/reteoo/test/ReteDslTestEngine.java @@ -22,11 +22,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import junit.framework.AssertionFailedError; + import org.antlr.runtime.ANTLRInputStream; import org.antlr.runtime.ANTLRReaderStream; import org.antlr.runtime.ANTLRStringStream; @@ -48,6 +51,7 @@ import org.drools.core.util.ObjectHashMap.ObjectEntry; import org.drools.core.util.RightTupleList; import org.drools.reteoo.AccumulateNode; +import org.drools.reteoo.AccumulateNode.AccumulateMemory; import org.drools.reteoo.BetaMemory; import org.drools.reteoo.BetaNode; import org.drools.reteoo.LeftTuple; @@ -62,11 +66,11 @@ import org.drools.reteoo.RightTupleMemory; import org.drools.reteoo.RuleTerminalNode; import org.drools.reteoo.Sink; -import org.drools.reteoo.AccumulateNode.AccumulateMemory; import org.drools.reteoo.builder.BuildContext; import org.drools.reteoo.test.dsl.AccumulateNodeStep; import org.drools.reteoo.test.dsl.BetaNodeStep; import org.drools.reteoo.test.dsl.BindingStep; +import org.drools.reteoo.test.dsl.ConfigStep; import org.drools.reteoo.test.dsl.DSLMock; import org.drools.reteoo.test.dsl.DslStep; import org.drools.reteoo.test.dsl.EvalNodeStep; @@ -78,6 +82,8 @@ import org.drools.reteoo.test.dsl.MockitoHelper; import org.drools.reteoo.test.dsl.NodeTestCase; import org.drools.reteoo.test.dsl.NodeTestCaseResult; +import org.drools.reteoo.test.dsl.NodeTestCaseResult.NodeTestResult; +import org.drools.reteoo.test.dsl.NodeTestCaseResult.Result; import org.drools.reteoo.test.dsl.NodeTestDef; import org.drools.reteoo.test.dsl.NotNodeStep; import org.drools.reteoo.test.dsl.ObjectTypeNodeStep; @@ -86,14 +92,12 @@ import org.drools.reteoo.test.dsl.RuleTerminalNodeStep; import org.drools.reteoo.test.dsl.Step; import org.drools.reteoo.test.dsl.WithStep; -import org.drools.reteoo.test.dsl.NodeTestCaseResult.NodeTestResult; -import org.drools.reteoo.test.dsl.NodeTestCaseResult.Result; import org.drools.reteoo.test.parser.NodeTestDSLLexer; import org.drools.reteoo.test.parser.NodeTestDSLParser; -import org.drools.reteoo.test.parser.NodeTestDSLTree; import org.drools.reteoo.test.parser.NodeTestDSLParser.compilation_unit_return; import org.drools.rule.MVELDialectRuntimeData; import org.drools.rule.Rule; +import org.drools.reteoo.test.parser.NodeTestDSLTree; import org.drools.spi.PropagationContext; import org.junit.runner.Description; import org.junit.runner.notification.Failure; @@ -103,6 +107,9 @@ public class ReteDslTestEngine { + public static final String WORKING_MEMORY = "WorkingMemory"; + public static final String BUILD_CONTEXT = "BuildContext"; + private static final String CONFIG = "Config"; private static final String OBJECT_TYPE_NODE = "ObjectTypeNode"; private static final String LEFT_INPUT_ADAPTER_NODE = "LeftInputAdapterNode"; private static final String BINDING = "Binding"; @@ -127,6 +134,8 @@ public ReteDslTestEngine() { this.steps = new HashMap(); + this.steps.put( CONFIG, + new ConfigStep() ); this.steps.put( OBJECT_TYPE_NODE, new ObjectTypeNodeStep( this.reteTesterHelper ) ); this.steps.put( LEFT_INPUT_ADAPTER_NODE, @@ -169,30 +178,41 @@ public NodeTestCaseResult run(NodeTestCase testCase, NodeTestCaseResult result = new NodeTestCaseResult( testCase ); for ( NodeTestDef test : testCase.getTests() ) { notifier.fireTestStarted( test.getDescription() ); - NodeTestResult testResult = run( testCase, - test ); - switch ( testResult.result ) { - case SUCCESS : - notifier.fireTestFinished( test.getDescription() ); - break; - case ERROR : - case FAILURE : - notifier.fireTestFailure( new Failure( test.getDescription(), - new AssertionError( testResult.errorMsgs ) ) ); - break; + NodeTestResult testResult = createTestResult( test, + null ); + + try { + testResult = run( testCase, + test ); + + switch ( testResult.result ) { + case SUCCESS : + notifier.fireTestFinished( test.getDescription() ); + break; + case ERROR : + case FAILURE : + notifier.fireTestFailure( new Failure( test.getDescription(), + new AssertionError( testResult.errorMsgs ) ) ); + break; + } + + } catch ( Throwable e ) { + notifier.fireTestFailure( new Failure( test.getDescription(), + e ) ); } + result.add( testResult ); } + return result; } private NodeTestResult run(NodeTestCase testCase, NodeTestDef test) { Map context = createContext( testCase ); - NodeTestResult result = new NodeTestResult( test, - Result.NOT_EXECUTED, - context, - new LinkedList() ); + NodeTestResult result = createTestResult( test, + context ); + try { // run setup run( context, @@ -207,12 +227,23 @@ private NodeTestResult run(NodeTestCase testCase, testCase.getTearDown(), result ); result.result = Result.SUCCESS; - } catch ( Exception e ) { + } catch ( Throwable e ) { result.result = Result.ERROR; - result.errorMsgs.add( e.getMessage() ); + result.errorMsgs.add( e.toString() ); } return result; } + + private NodeTestResult createTestResult(NodeTestDef test, + Map context) { + + NodeTestResult result = new NodeTestResult( test, + Result.NOT_EXECUTED, + context, + new LinkedList() ); + return result; + } + private Map createContext(NodeTestCase testCase) { Map context = new HashMap(); @@ -225,6 +256,7 @@ private Map createContext(NodeTestCase testCase) { conf ); BuildContext buildContext = new BuildContext( rbase, rbase.getReteooBuilder().getIdGenerator() ); + Rule rule = new Rule("rule1", "org.pkg1", null); org.drools.rule.Package pkg = new org.drools.rule.Package( "org.pkg1" ); pkg.getDialectRuntimeRegistry().setDialectData( "mvel", new MVELDialectRuntimeData() ); @@ -233,13 +265,13 @@ private Map createContext(NodeTestCase testCase) { buildContext.setRule( rule ); rbase.addPackage( pkg ); - context.put( "BuildContext", + context.put( BUILD_CONTEXT, buildContext ); context.put( "ClassFieldAccessorStore", this.reteTesterHelper.getStore() ); InternalWorkingMemory wm = (InternalWorkingMemory) rbase.newStatefulSession( true ); - context.put( "WorkingMemory", + context.put( WORKING_MEMORY, wm ); return context; } @@ -247,7 +279,7 @@ private Map createContext(NodeTestCase testCase) { public Map run(Map context, List steps, NodeTestResult result) { - InternalWorkingMemory wm = (InternalWorkingMemory) context.get( "WorkingMemory" ); + InternalWorkingMemory wm = (InternalWorkingMemory) context.get( WORKING_MEMORY ); for ( DslStep step : steps ) { String name = step.getName(); Object object = this.steps.get( name ); @@ -312,6 +344,11 @@ private void betaNode(DslStep step, BetaNode node, Map context, InternalWorkingMemory wm) { + + final boolean lrUnlinkingEnabled = ((BuildContext) context + .get( BUILD_CONTEXT )).getRuleBase().getConfiguration() + .isLRUnlinkingEnabled(); + try { List cmds = step.getCommands(); List handles = (List) context.get( "Handles" ); @@ -337,9 +374,14 @@ private void betaNode(DslStep step, LeftTupleMemory leftMemory = memory.getLeftTupleMemory(); if ( expectedLeftTuples.isEmpty() && leftMemory.size() != 0 ) { - throw new AssertionError( "line " + step.getLine() + ": left Memory expected [] actually " + leftMemory ); - } else if ( expectedLeftTuples.isEmpty() && leftMemory.size() == 0 ) { - return; + throw new AssertionFailedError( "line " + step.getLine() + + ": left Memory expected [] actually " + + print( leftMemory, + lrUnlinkingEnabled ) ); + } else if ( expectedLeftTuples.isEmpty() + && leftMemory.size() == 0 ) { + continue; + } // we always lookup from the first element, in case it's indexed @@ -359,11 +401,23 @@ private void betaNode(DslStep step, for ( LeftTuple leftTuple = getFirst(memory.getLeftTupleMemory(), firstTuple); leftTuple != null; leftTuple = (LeftTuple) leftTuple.getNext() ) { leftTuples.add( leftTuple ); } - List> actualLeftTuples = new ArrayList>( leftTuples.size() ); - for ( LeftTuple leftTuple : leftTuples ) { - List tupleHandles = Arrays.asList( leftTuple.toFactHandles() ); - actualLeftTuples.add( tupleHandles ); + + if ( lrUnlinkingEnabled ) { + // When L&R Unlinking is active, we need to sort the + // tuples here, + // because we might have asserted things in the wrong + // order, + // since linking a node's side means populating its + // memory + // from the OTN which stores things in a hash-set, so + // insertion order is not kept. + Collections.sort( leftTuples, + new LeftTupleComparator() ); + } + + List> actualLeftTuples = getHandlesList( leftTuples ); + if ( !expectedLeftTuples.equals( actualLeftTuples ) ) { throw new AssertionError( "line " + step.getLine() + ": left Memory expected " + expectedLeftTuples + " actually " + actualLeftTuples ); @@ -382,9 +436,9 @@ private void betaNode(DslStep step, RightTupleMemory rightMemory = memory.getRightTupleMemory(); if ( expectedFactHandles.isEmpty() && rightMemory.size() != 0 ) { - throw new AssertionError( "line " + step.getLine() + ": right Memory expected [] actually " + rightMemory ); + throw new AssertionError( "line " + step.getLine() + ": right Memory expected [] actually " + print( rightMemory )); } else if ( expectedFactHandles.isEmpty() && rightMemory.size() == 0 ) { - return; + continue; } RightTuple first = new RightTuple( (InternalFactHandle) expectedFactHandles.get( 0 ) ); @@ -394,12 +448,14 @@ private void betaNode(DslStep step, } if ( expectedFactHandles.size() != actualRightTuples.size() ) { - throw new AssertionError( "line " + step.getLine() + ": right Memory expected " + expectedFactHandles + " actually " + actualRightTuples ); + throw new AssertionError( "line " + step.getLine() + ": right Memory expected " + print( expectedFactHandles ) + + " actually " + print( actualRightTuples )); } for ( int i = 0, length = actualRightTuples.size(); i < length; i++ ) { if ( expectedFactHandles.get( i ) != actualRightTuples.get( i ).getFactHandle() ) { - throw new AssertionError( "line " + step.getLine() + ": right Memory expected " + expectedFactHandles + " actually " + actualRightTuples ); + throw new AssertionError( "line " + step.getLine() + ": right Memory expected " + print( expectedFactHandles ) + + " actually " + print( actualRightTuples )); } } @@ -435,6 +491,82 @@ private RightTuple getFirst(RightTupleMemory memory, RightTuple rightTuple) { return null; } + private List> getHandlesList( + List leftTuples) { + List> actualLeftTuples = new ArrayList>( + leftTuples.size() ); + for ( LeftTuple leftTuple : leftTuples ) { + List tupleHandles = Arrays.asList( leftTuple + .toFactHandles() ); + actualLeftTuples.add( tupleHandles ); + } + return actualLeftTuples; + } + + private String print(LeftTupleMemory leftMemory, + boolean lrUnlinkingEnabled) { + + List tuples = new ArrayList(); + Iterator it = leftMemory.iterator(); + for ( LeftTuple tuple = (LeftTuple) it.next(); tuple != null; tuple = (LeftTuple) it + .next() ) { + tuples.add( tuple ); + } + + if ( lrUnlinkingEnabled ) { + // Necessary only when L&R unlinking are active. + Collections.sort( tuples, + new LeftTupleComparator() ); + } + + return print( getHandlesList( tuples ) ); + } + + private String print(RightTupleMemory memory) { + + List tuples = new ArrayList(); + Iterator it = memory.iterator(); + + for ( RightTuple tuple = (RightTuple) it.next(); tuple != null; tuple = (RightTuple) it + .next() ) { + tuples.add( tuple ); + } + + return "[" + print( tuples ) + "]"; + } + + /** Provides better error messages. */ + protected String print(List< ? > tuples) { + + StringBuilder b = new StringBuilder(); + + for ( java.util.Iterator iterator = tuples.iterator(); iterator + .hasNext(); ) { + + Object tuple = (Object) iterator.next(); + + if ( tuple instanceof List< ? > ) { + b.append( "[" ); + b.append( print( (List< ? >) tuple ) ); + b.append( "]" ); + } else if ( tuple instanceof InternalFactHandle ) { + InternalFactHandle h = (InternalFactHandle) tuple; + b.append( "h" ).append( h.getId() - 1 ); + } else if ( tuple instanceof RightTuple ) { + InternalFactHandle h = (InternalFactHandle) ((RightTuple) tuple) + .getFactHandle(); + b.append( "h" ).append( h.getId() - 1 ); + } + + if ( iterator.hasNext() ) b.append( ", " ); + } + + if ( b.length() == 0 ) return "[]"; + + return b.toString(); + } + + private void riaNode(DslStep step, RightInputAdapterNode node, Map context, @@ -847,6 +979,30 @@ private static NodeTestDSLParser getParser(final String source) throws IOExcepti NodeTestDSLParser parser = new NodeTestDSLParser( new CommonTokenStream( lexer ) ); return parser; } + + private static final class LeftTupleComparator implements Comparator { + + public int compare(LeftTuple o1, + LeftTuple o2) { + + InternalFactHandle[] h1 = o1.getFactHandles(); + InternalFactHandle[] h2 = o2.getFactHandles(); + + // Handles have to be compared in the inverse order. + for ( int i = (h1.length - 1); i >= 0; i-- ) { + + int diff = h1[i].getId() - h2[i].getId(); + + // Will continue comparing handles until + // a difference is found. + if ( diff != 0 ) return diff; + } + + return 0; + } + } + + public static class EmptyNotifier extends RunNotifier { public static final EmptyNotifier INSTANCE = new EmptyNotifier(); diff --git a/drools-core/src/test/java/org/drools/reteoo/test/ReteDslTestEngineTest.java b/drools-core/src/test/java/org/drools/reteoo/test/ReteDslTestEngineTest.java index af2b564b245..2f7a3de45ef 100644 --- a/drools-core/src/test/java/org/drools/reteoo/test/ReteDslTestEngineTest.java +++ b/drools-core/src/test/java/org/drools/reteoo/test/ReteDslTestEngineTest.java @@ -16,28 +16,26 @@ package org.drools.reteoo.test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Set; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import static org.junit.Assert.*; import org.drools.FactHandle; import org.drools.Person; -import org.drools.RuleBaseConfiguration; import org.drools.RuleBaseFactory; import org.drools.WorkingMemory; import org.drools.base.ClassObjectType; import org.drools.common.DefaultFactHandle; import org.drools.common.InternalFactHandle; import org.drools.common.InternalWorkingMemory; -import org.drools.common.NodeMemory; import org.drools.common.PropagationContextImpl; import org.drools.core.util.LeftTupleList; import org.drools.reteoo.BetaMemory; @@ -47,15 +45,16 @@ import org.drools.reteoo.ObjectTypeNode; import org.drools.reteoo.ReteooRuleBase; import org.drools.reteoo.ReteooWorkingMemory; -import org.drools.reteoo.RuleTerminalNode; +import org.drools.reteoo.builder.BuildContext; import org.drools.reteoo.test.dsl.DslStep; -import org.drools.reteoo.test.dsl.NodeTestDef; import org.drools.reteoo.test.dsl.NodeTestCase; import org.drools.reteoo.test.dsl.NodeTestCaseResult; import org.drools.reteoo.test.dsl.NodeTestCaseResult.NodeTestResult; import org.drools.reteoo.test.dsl.NodeTestCaseResult.Result; +import org.drools.reteoo.test.dsl.NodeTestDef; import org.drools.rule.Declaration; import org.drools.spi.PropagationContext; +import org.junit.Test; public class ReteDslTestEngineTest { @@ -791,6 +790,21 @@ public void testNotNodeStep() throws IOException { NodeTestResult result = executeTest( str ); Map map = result.context; } + + public void testConfigOptions() throws IOException { + String str = "TestCase 'testOTN'\nTest 'dummy'\n"; + str += "Config:\n"; + str += " drools.lrUnlinkingEnabled, true;\n"; + str += "ObjectTypeNode:\n"; + str += " otn1, java.lang.Integer;\n"; + + NodeTestResult result = executeTest( str ); + Map map = result.context; + + BuildContext buildCtx = (BuildContext) map.get( ReteDslTestEngine.BUILD_CONTEXT ); + assertTrue(buildCtx.getRuleBase().getConfiguration().isLRUnlinkingEnabled()); + } + private void print(DslStep[] steps) { for ( DslStep command : steps ) { diff --git a/drools-core/src/test/java/org/drools/reteoo/test/SingleTestCase.java b/drools-core/src/test/java/org/drools/reteoo/test/SingleTestCase.java index 7fe3c414d05..2f94cfd724f 100755 --- a/drools-core/src/test/java/org/drools/reteoo/test/SingleTestCase.java +++ b/drools-core/src/test/java/org/drools/reteoo/test/SingleTestCase.java @@ -60,6 +60,7 @@ public boolean accept(File arg0, } for( NodeTestDef test : tcase.getTests() ) { if( test.getName().equals( testName ) ) { + result.setFileName(file.getName()); result.setName( tcase.getName() ); result.setImports( tcase.getImports() ); result.setSetup( tcase.getSetup() ); diff --git a/drools-core/src/test/java/org/drools/reteoo/test/dsl/ConfigStep.java b/drools-core/src/test/java/org/drools/reteoo/test/dsl/ConfigStep.java new file mode 100644 index 00000000000..38d35bd1942 --- /dev/null +++ b/drools-core/src/test/java/org/drools/reteoo/test/dsl/ConfigStep.java @@ -0,0 +1,66 @@ +package org.drools.reteoo.test.dsl; + +import static org.drools.reteoo.test.ReteDslTestEngine.BUILD_CONTEXT; +import static org.drools.reteoo.test.ReteDslTestEngine.WORKING_MEMORY; + +import java.util.List; +import java.util.Map; + +import org.drools.RuleBaseConfiguration; +import org.drools.common.InternalWorkingMemory; +import org.drools.reteoo.ReteooRuleBase; +import org.drools.reteoo.builder.BuildContext; + +/** + *

+ * A step in the setup of a nodeTestCase, it allows any configuration parameters + * to be passed.
Note that the RuleBase and Working Memory are recreated + * and changed in the context, so the configuration step should be the first one + * in the setup of your nodeTestCase or you may face inconsistent behavior. + *

+ * Usage: + * + *
+ * Setup
+ *     Config:
+ *         drools.lrUnlinkingEnabled, true;
+ *     ObjectTypeNode:
+ *         otnLeft1, org.drools.Person;
+ *     LeftInputAdapterNode:
+ *         lian0, otnLeft1;
+ *     ObjectTypeNode:
+ *         otnRight1, org.drools.Person;
+ *     Binding:
+ *          p1, 0, org.drools.Person, age;
+ *     JoinNode:
+ *         join1, lian0, otnRight1;
+ *         age, !=, p1;
+ *     Facts:
+ *         new org.drools.Person('darth', 35), new org.drools.Person('bobba', 36),
+ *
+ * + */ +public class ConfigStep implements Step { + + public void execute(Map context, List args) { + + RuleBaseConfiguration conf = new RuleBaseConfiguration(); + + for (String[] configOption : args) { + conf.setProperty(configOption[0], configOption[1]); + } + + ReteooRuleBase rbase = new ReteooRuleBase("ID", conf); + BuildContext buildContext = new BuildContext(rbase, rbase + .getReteooBuilder().getIdGenerator()); + + InternalWorkingMemory wm = (InternalWorkingMemory) rbase + .newStatefulSession(true); + + // Overwrite values now taking into account the configuration options. + context.put(BUILD_CONTEXT, buildContext); + context.put(WORKING_MEMORY, wm); + + } + +} diff --git a/drools-core/src/test/java/org/drools/reteoo/test/dsl/NodeTestCase.java b/drools-core/src/test/java/org/drools/reteoo/test/dsl/NodeTestCase.java index d26015ffc0d..f612654efd7 100755 --- a/drools-core/src/test/java/org/drools/reteoo/test/dsl/NodeTestCase.java +++ b/drools-core/src/test/java/org/drools/reteoo/test/dsl/NodeTestCase.java @@ -21,59 +21,70 @@ import org.junit.runner.Description; - /** * A class to describe a test case for reteoo nodes */ public class NodeTestCase { - - private String name; - private List imports; - private List setup; - private List tearDown; - private List tests; - private List errors; - private Description description; - + + public static final String SUFFIX = ".nodeTestCase"; + + private String name; + private List imports; + private List setup; + private List tearDown; + private List tests; + private List errors; + private Description description; + private String fileName; + public NodeTestCase() { - this(""); + this( "" ); } - - public NodeTestCase( String name ) { + + public NodeTestCase(String name) { this.name = name; this.imports = new ArrayList(); this.setup = new ArrayList(); this.tearDown = new ArrayList(); this.tests = new ArrayList(); } - + public String getName() { return name; } + public void setName(String name) { this.name = name; } - public void addImport( String clazz ) { + + public void addImport(String clazz) { this.imports.add( clazz ); } + public List getImports() { return this.imports; } + public List getSetup() { return setup; } + public void addSetupStep(DslStep step) { this.setup.add( step ); } + public List getTearDown() { return tearDown; } + public void addTearDownStep(DslStep step) { this.tearDown.add( step ); } + public List getTests() { return tests; } + public void addTest(NodeTestDef test) { this.tests.add( test ); } @@ -87,7 +98,7 @@ public void setErrors(List errors) { } public boolean hasErrors() { - return this.errors != null && ! this.errors.isEmpty(); + return this.errors != null && !this.errors.isEmpty(); } public Description getDescription() { @@ -109,4 +120,25 @@ public void setSetup(List setup) { public void setTearDown(List tearDown) { this.tearDown = tearDown; } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + + this.fileName = removeSuffix( fileName ); + } + + private String removeSuffix(String name) { + // removes the suffix, if present. + if ( name.endsWith( SUFFIX ) ) { + return name.substring( 0, + name.indexOf( SUFFIX ) ); + } + + return name; + + } + } diff --git a/drools-core/src/test/resources/org/drools/reteoo/test/ExistsNodeAssertRetractTest.nodeTestCase b/drools-core/src/test/resources/org/drools/reteoo/test/ExistsNodeAssertRetractTest.nodeTestCase index b679ac1138b..afce07fdebf 100644 --- a/drools-core/src/test/resources/org/drools/reteoo/test/ExistsNodeAssertRetractTest.nodeTestCase +++ b/drools-core/src/test/resources/org/drools/reteoo/test/ExistsNodeAssertRetractTest.nodeTestCase @@ -131,7 +131,7 @@ Test "Exists node assert/retract test" otnRight1, [h5]; exists1: leftMemory, []; //h0 and h1 are blocked, by h5, so removed - rightMemory, [h5, h6]; + rightMemory, [h5]; join2: leftMemory, [[h0], [h1]]; assert: diff --git a/drools-core/src/test/resources/org/drools/reteoo/test/NotNodeAssertRetractTest.nodeTestCase b/drools-core/src/test/resources/org/drools/reteoo/test/NotNodeAssertRetractTest.nodeTestCase index 3d7f7f875c3..edaf30e3a49 100644 --- a/drools-core/src/test/resources/org/drools/reteoo/test/NotNodeAssertRetractTest.nodeTestCase +++ b/drools-core/src/test/resources/org/drools/reteoo/test/NotNodeAssertRetractTest.nodeTestCase @@ -198,7 +198,7 @@ Test "assert retract test" otnRight1, [h5]; not1: leftMemory, []; //h0 and h1 are blocked, by h5, so removed - rightMemory, [h5, h6]; + rightMemory, [h5]; join2: leftMemory, []; assert: diff --git a/drools-core/src/test/resources/org/drools/reteoo/test/RightUnlinkingComplex.nodeTestCase b/drools-core/src/test/resources/org/drools/reteoo/test/RightUnlinkingComplex.nodeTestCase new file mode 100644 index 00000000000..532396bb8cb --- /dev/null +++ b/drools-core/src/test/resources/org/drools/reteoo/test/RightUnlinkingComplex.nodeTestCase @@ -0,0 +1,138 @@ +TestCase "RightUnlinkingComplex" + +/* +* A bit more complex, this test case shows that memories get properly populated +* when there are loops like the one between Join1 and Join2 here. +*/ +Setup + Config: + drools.lrUnlinkingEnabled, true; + ObjectTypeNode: + otnLeft1, org.drools.Person; + LeftInputAdapterNode: + lian0, otnLeft1; + ObjectTypeNode: + otnRight1, org.drools.Person; + ObjectTypeNode: + otnRight2, org.drools.Person; + Binding: + p1, 0, org.drools.Person, age; + Binding: + p2, 0, org.drools.Person, likes; + JoinNode: + join1, lian0, otnRight1; + age, !=, p1; + JoinNode: + join2, join1, otnRight1; + likes, ==, p2; + JoinNode: + join3, join2, otnRight2; + age, !=, p1; + Facts: + new org.drools.Person('darth', 35, 'stilton'), new org.drools.Person('bobba', 36, 'brie'), + new org.drools.Person('yoda', 37, 'cammembert'), new org.drools.Person('luke', 38, 'minas'), + new org.drools.Person('dave', 33, 'prato'), new org.drools.Person('bob', 32, 'stilton'), + new org.drools.Person('dave', 31, 'parmigiano'), new org.drools.Person('bob', 30, 'minas'); + + +Test "RightUnlinkingComplex" + + assert: + otnRight1, [h1]; + + join1: + leftMemory, []; + rightMemory, []; // right side is unlinked. + assert: + otnLeft1, [h0]; // a left propagation will link the right side and trigger + // its population by calling updateSink in the OTN. + join1: + leftMemory, [[h0]]; + rightMemory, [h1]; + join2: + leftMemory, [[h0, h1]]; + rightMemory, [h1]; + join3: + leftMemory, []; + rightMemory, []; + + assert: + otnRight2, [h3]; + join3: + leftMemory, []; + rightMemory, []; // right memory remains empty, since node is unlinked. + + assert: + otnRight1, [h5]; // age is != and likes == + join1: + leftMemory, [[h0]]; + rightMemory, [h1, h5]; + join2: + leftMemory, [[h0,h1],[h0,h5]]; + rightMemory, [h1]; + rightMemory, [h5]; + join3: + leftMemory, [[h0,h1,h5], [h0,h5,h5]]; + rightMemory, [h3]; // populated, since right side is now linked. + + assert: + otnRight2, [h7]; + join3: + rightMemory, [h3,h7]; // now right memory is linked + // and can be asserted normally. + + retract: + otnRight1, [h5]; + join1: + leftMemory, [[h0]]; + rightMemory, [h1]; + join2: + leftMemory, [[h0,h1]]; + rightMemory, [h1]; + join3: + leftMemory, []; + rightMemory, [h3,h7]; + + // Now, even if the left memory is empty, the right side will stay + // linked to avoid churn. + assert: + otnRight2, [h2]; + join3: + leftMemory, []; + rightMemory, [h3,h7,h2]; + + // Trying a modification that triggers propagation + With: + h1, likes = 'stilton'; + modify: + otnRight1, [h1]; + join1: + leftMemory, [[h0]]; + rightMemory, [h1]; + join2: + leftMemory, [[h0,h1]]; + rightMemory, [h1]; + join3: + leftMemory, [[h0,h1,h1]]; + rightMemory, [h3,h7,h2]; + + // Trying a modification that triggers retraction + With: + h0, age = 36; + modify: + otnLeft1, [h0]; + join1: + leftMemory, [[h0]]; + rightMemory, [h1]; + join2: + leftMemory, []; + rightMemory, [h1]; + join3: + leftMemory, []; + rightMemory, [h3,h7,h2]; + + + + + + diff --git a/drools-core/src/test/resources/org/drools/reteoo/test/RightUnlinkingModify.nodeTestCase b/drools-core/src/test/resources/org/drools/reteoo/test/RightUnlinkingModify.nodeTestCase new file mode 100644 index 00000000000..6f54fa4fb20 --- /dev/null +++ b/drools-core/src/test/resources/org/drools/reteoo/test/RightUnlinkingModify.nodeTestCase @@ -0,0 +1,70 @@ +TestCase "RightUnlinkingModify" + +Setup + Config: + drools.lrUnlinkingEnabled, true; + ObjectTypeNode: + otnLeft1, org.drools.Person; + LeftInputAdapterNode: + lian0, otnLeft1; + ObjectTypeNode: + otnRight1, org.drools.Person; + ObjectTypeNode: + otnRight2, org.drools.Person; + ObjectTypeNode: + otnRight3, org.drools.Person; + Binding: + p1, 0, org.drools.Person, age; + JoinNode: + join1, lian0, otnRight1; + age, !=, p1; + JoinNode: + join2, join1, otnRight2; + age, !=, p1; + JoinNode: + join3, join1, otnRight3; + age, !=, p1; + Facts: + new org.drools.Person('darth', 35), new org.drools.Person('bobba', 36), + new org.drools.Person('yoda', 37), new org.drools.Person('luke', 38), + new org.drools.Person('dave', 33), new org.drools.Person('bob', 32), + new org.drools.Person('dave', 31), new org.drools.Person('bob', 30); + + +Test "RightUnlinkingModify" + + assert: + otnRight1, [h0]; + otnRight2, [h1]; + join1: + rightMemory, []; + join2: + rightMemory, []; + + assert: + otnLeft1, [h2]; + join1: + leftMemory, [[h2]]; + rightMemory, [h0]; + join2: + leftMemory, [[h2,h0]]; + rightMemory, [h1]; + + With: + h2, age = 35; + modify: + otnLeft1, [h2]; + + // With the modify, [h2,h0] don't match anymore + join1: + leftMemory, [[h2]]; + rightMemory, [h0]; + join2: + leftMemory, []; // left tuple is retracted + rightMemory, [h1]; // right side is not unlinked + + assert: + otnRight2, [h3]; + join2: + rightMemory, [h1,h3]; +