HHH-4577 - 2L query cache: Low performance of flush and commit due many unnecessary (pre)invalidate calls on UpdateTimestampsCache

This commit is contained in:
Steve Ebersole 2013-09-16 15:38:17 -05:00
parent 7bca11a504
commit 8dae133bba
5 changed files with 218 additions and 172 deletions

View File

@ -177,6 +177,7 @@ public abstract class EntityAction
*
* @param session The session being deserialized
*/
@Override
public void afterDeserialize(SessionImplementor session) {
if ( this.session != null || this.persister != null ) {
throw new IllegalStateException( "already attached to a session." );

View File

@ -48,27 +48,16 @@ public final class QueuedOperationCollectionAction extends CollectionAction {
* @param session The session
*/
public QueuedOperationCollectionAction(
final PersistentCollection collection,
final CollectionPersister persister,
final Serializable id,
final SessionImplementor session) {
final PersistentCollection collection,
final CollectionPersister persister,
final Serializable id,
final SessionImplementor session) {
super( persister, collection, id, session );
}
@Override
public void execute() throws HibernateException {
final Serializable id = getKey();
final SessionImplementor session = getSession();
final CollectionPersister persister = getPersister();
final PersistentCollection collection = getCollection();
persister.processQueuedOps( collection, id, session );
getPersister().processQueuedOps( getCollection(), getKey(), getSession() );
}
}

View File

@ -38,8 +38,6 @@ import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.jboss.logging.Logger;
import org.hibernate.AssertionFailure;
import org.hibernate.HibernateException;
import org.hibernate.PropertyValueException;
@ -59,30 +57,30 @@ import org.hibernate.action.spi.BeforeTransactionCompletionProcess;
import org.hibernate.action.spi.Executable;
import org.hibernate.cache.CacheException;
import org.hibernate.engine.internal.NonNullableTransientDependencies;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.type.Type;
/**
* Responsible for maintaining the queue of actions related to events. The ActionQueue holds the DML operations queued
* as part of a session's transactional-write-behind semantics. DML operations are queued here until a flush forces them
* to be executed against the database.
* Responsible for maintaining the queue of actions related to events.
*
* The ActionQueue holds the DML operations queued as part of a session's transactional-write-behind semantics. The
* DML operations are queued here until a flush forces them to be executed against the database.
*
* @author Steve Ebersole
* @author Gail Badner
* @author Anton Marsden
*/
public class ActionQueue {
static final CoreMessageLogger LOG = Logger.getMessageLogger( CoreMessageLogger.class, ActionQueue.class.getName() );
private static final int INIT_QUEUE_LIST_SIZE = 5;
private static final CoreMessageLogger LOG = CoreLogging.messageLogger( ActionQueue.class );
private SessionImplementor session;
private UnresolvedEntityInsertActions unresolvedInsertions;
// Object insertions, updates, and deletions have list semantics because
// they must happen in the right order so as to respect referential
// integrity
private UnresolvedEntityInsertActions unresolvedInsertions;
private final ExecutableList<AbstractEntityInsertAction> insertions;
private final ExecutableList<EntityDeleteAction> deletions;
private final ExecutableList<EntityUpdateAction> updates;
@ -111,20 +109,21 @@ public class ActionQueue {
this.session = session;
unresolvedInsertions = new UnresolvedEntityInsertActions();
insertions = new ExecutableList<AbstractEntityInsertAction>( INIT_QUEUE_LIST_SIZE, new InsertActionSorter() );
deletions = new ExecutableList<EntityDeleteAction>( INIT_QUEUE_LIST_SIZE );
updates = new ExecutableList<EntityUpdateAction>( INIT_QUEUE_LIST_SIZE );
insertions = new ExecutableList<AbstractEntityInsertAction>( ExecutableList.INIT_QUEUE_LIST_SIZE, new InsertActionSorter() );
deletions = new ExecutableList<EntityDeleteAction>( ExecutableList.INIT_QUEUE_LIST_SIZE );
updates = new ExecutableList<EntityUpdateAction>( ExecutableList.INIT_QUEUE_LIST_SIZE );
collectionCreations = new ExecutableList<CollectionRecreateAction>( INIT_QUEUE_LIST_SIZE );
collectionRemovals = new ExecutableList<CollectionRemoveAction>( INIT_QUEUE_LIST_SIZE );
collectionUpdates = new ExecutableList<CollectionUpdateAction>( INIT_QUEUE_LIST_SIZE );
collectionQueuedOps = new ExecutableList<QueuedOperationCollectionAction>( INIT_QUEUE_LIST_SIZE );
collectionCreations = new ExecutableList<CollectionRecreateAction>( ExecutableList.INIT_QUEUE_LIST_SIZE );
collectionRemovals = new ExecutableList<CollectionRemoveAction>( ExecutableList.INIT_QUEUE_LIST_SIZE );
collectionUpdates = new ExecutableList<CollectionUpdateAction>( ExecutableList.INIT_QUEUE_LIST_SIZE );
collectionQueuedOps = new ExecutableList<QueuedOperationCollectionAction>( ExecutableList.INIT_QUEUE_LIST_SIZE );
// Important: these lists are in execution order
List<ExecutableList<?>> tmp = new ArrayList<ExecutableList<?>>( 7 );
tmp.add( insertions );
tmp.add( updates );
tmp.add( collectionQueuedOps ); // do before actions are handled in the other collection queues
// do before actions are handled in the other collection queues
tmp.add( collectionQueuedOps );
tmp.add( collectionRemovals );
tmp.add( collectionUpdates );
tmp.add( collectionCreations );
@ -138,48 +137,22 @@ public class ActionQueue {
}
public void clear() {
for ( ExecutableList<?> l : executableLists ) {
l.clear();
}
unresolvedInsertions.clear();
}
/**
* Adds an entity insert action
*
* @param action The action representing the entity insertion
*/
public void addAction(EntityInsertAction action) {
LOG.tracev( "Adding an EntityInsertAction for [{0}] object", action.getEntityName() );
addInsertAction( action );
}
public void addAction(EntityDeleteAction action) {
deletions.add( action );
}
public void addAction(EntityUpdateAction action) {
updates.add( action );
}
public void addAction(CollectionRecreateAction action) {
collectionCreations.add( action );
}
public void addAction(CollectionRemoveAction action) {
collectionRemovals.add( action );
}
public void addAction(CollectionUpdateAction action) {
collectionUpdates.add( action );
}
public void addAction(QueuedOperationCollectionAction action) {
collectionQueuedOps.add( action );
}
public void addAction(EntityIdentityInsertAction insert) {
LOG.tracev( "Adding an EntityIdentityInsertAction for [{0}] object", insert.getEntityName() );
addInsertAction( insert );
}
private void addInsertAction(AbstractEntityInsertAction insert) {
if ( insert.isEarlyInsert() ) {
// For early inserts, must execute inserts before finding non-nullable transient entities.
@ -195,7 +168,7 @@ public class ActionQueue {
else {
if ( LOG.isTraceEnabled() ) {
LOG.tracev( "Adding insert with non-nullable, transient entities; insert=[{0}], dependencies=[{1}]", insert,
nonNullableTransientDependencies.toLoggableString( insert.getSession() ) );
nonNullableTransientDependencies.toLoggableString( insert.getSession() ) );
}
unresolvedInsertions.addUnresolvedEntityInsertAction( insert, nonNullableTransientDependencies );
}
@ -218,6 +191,87 @@ public class ActionQueue {
}
}
/**
* Adds an entity (IDENTITY) insert action
*
* @param action The action representing the entity insertion
*/
public void addAction(EntityIdentityInsertAction action) {
LOG.tracev( "Adding an EntityIdentityInsertAction for [{0}] object", action.getEntityName() );
addInsertAction( action );
}
/**
* Adds an entity delete action
*
* @param action The action representing the entity deletion
*/
public void addAction(EntityDeleteAction action) {
deletions.add( action );
}
/**
* Adds an entity update action
*
* @param action The action representing the entity update
*/
public void addAction(EntityUpdateAction action) {
updates.add( action );
}
/**
* Adds a collection (re)create action
*
* @param action The action representing the (re)creation of a collection
*/
public void addAction(CollectionRecreateAction action) {
collectionCreations.add( action );
}
/**
* Adds a collection remove action
*
* @param action The action representing the removal of a collection
*/
public void addAction(CollectionRemoveAction action) {
collectionRemovals.add( action );
}
/**
* Adds a collection update action
*
* @param action The action representing the update of a collection
*/
public void addAction(CollectionUpdateAction action) {
collectionUpdates.add( action );
}
/**
* Adds an action relating to a collection queued operation (extra lazy).
*
* @param action The action representing the queued operation
*/
public void addAction(QueuedOperationCollectionAction action) {
collectionQueuedOps.add( action );
}
/**
* Adds an action defining a cleanup relating to a bulk operation (HQL/JPQL or Criteria based update/delete)
*
* @param action The action representing the queued operation
*/
public void addAction(BulkOperationCleanupAction action) {
registerCleanupActions( action );
}
private void registerCleanupActions(Executable executable) {
beforeTransactionProcesses.register( executable.getBeforeTransactionCompletionProcess() );
if ( session.getFactory().getSettings().isQueryCacheEnabled() ) {
invalidateSpaces( executable.getPropertySpaces() );
}
afterTransactionProcesses.register( executable.getAfterTransactionCompletionProcess() );
}
/**
* Are there unresolved entity insert actions that depend on non-nullable associations with a transient entity?
*
@ -242,10 +296,6 @@ public class ActionQueue {
unresolvedInsertions.checkNoUnresolvedActionsAfterOperation();
}
public void addAction(BulkOperationCleanupAction cleanupAction) {
registerCleanupActions( cleanupAction );
}
public void registerProcess(AfterTransactionCompletionProcess process) {
afterTransactionProcesses.register( process );
}
@ -272,6 +322,7 @@ public class ActionQueue {
if ( !unresolvedInsertions.isEmpty() ) {
throw new IllegalStateException( "About to execute actions, but there are unresolved entity insert actions." );
}
for ( ExecutableList<?> l : executableLists ) {
executeActions( l );
}
@ -289,6 +340,12 @@ public class ActionQueue {
prepareActions( collectionQueuedOps );
}
private void prepareActions(ExecutableList<?> queue) throws HibernateException {
for ( Executable executable : queue ) {
executable.beforeExecutions();
}
}
/**
* Performs cleanup of any held cache softlocks.
*
@ -305,11 +362,21 @@ public class ActionQueue {
beforeTransactionProcesses.beforeTransactionCompletion();
}
/**
* Check whether any insertion or deletion actions are currently queued.
*
* @return {@code true} if insertions or deletions are currently queued; {@code false} otherwise.
*/
public boolean areInsertionsOrDeletionsQueued() {
return !insertions.isEmpty() || !unresolvedInsertions.isEmpty() || !deletions.isEmpty();
}
/**
* Check whether the given tables/query-spaces are to be executed against given the currently queued actions.
*
* @param tables The table/query-spaces to check.
* @return True if we contain pending actions against any of the given tables; false otherwise.
*
* @return {@code true} if we contain pending actions against any of the given tables; {@code false} otherwise.
*/
public boolean areTablesToBeUpdated(@SuppressWarnings("rawtypes") Set tables) {
if ( tables.isEmpty() ) {
@ -323,22 +390,12 @@ public class ActionQueue {
return areTablesToBeUpdated( unresolvedInsertions, tables );
}
/**
* Check whether any insertion or deletion actions are currently queued.
*
* @return True if insertions or deletions are currently queued; false otherwise.
*/
public boolean areInsertionsOrDeletionsQueued() {
return !insertions.isEmpty() || !unresolvedInsertions.isEmpty() || !deletions.isEmpty();
}
private static boolean areTablesToBeUpdated(ExecutableList<?> actions, @SuppressWarnings("rawtypes") Set tableSpaces) {
if ( actions.isEmpty() ) {
return false;
}
for ( Serializable actionSpace : actions.getPropertySpaces() ) {
for ( Serializable actionSpace : actions.getQuerySpaces() ) {
if ( tableSpaces.contains( actionSpace ) ) {
LOG.debugf( "Changes must be flushed to space: %s", actionSpace );
return true;
@ -362,18 +419,18 @@ public class ActionQueue {
}
/**
* Executes a list of Executables
* Perform {@link org.hibernate.action.spi.Executable#execute()} on each element of the list
*
* @param list
* @param list The list of Executable elements to be performed
*
* @throws HibernateException
*/
private <E extends Executable & Comparable<?> & Serializable> void executeActions(ExecutableList<E> list) throws HibernateException {
// todo : consider ways to improve the double iteration of Executables here:
// 1) we explicitly iterate list here to perform Executable#execute()
// 2) ExecutableList#getQuerySpaces also iterates the Executables to collect query spaces.
try {
for ( E e : list ) {
// Preserves the try/finally behavior of an individual execution as per execute(Executable), for what
// it's worth
try {
e.execute();
}
@ -388,7 +445,7 @@ public class ActionQueue {
// Strictly speaking, only a subset of the list may have been processed if a RuntimeException occurs.
// We still invalidate all spaces. I don't see this as a big deal - after all, RuntimeExceptions are
// unexpected.
Set<Serializable> propertySpaces = list.getPropertySpaces();
Set<Serializable> propertySpaces = list.getQuerySpaces();
invalidateSpaces( propertySpaces.toArray( new Serializable[propertySpaces.size()] ) );
}
}
@ -398,7 +455,7 @@ public class ActionQueue {
}
/**
* @param executable
* @param executable The action to execute
*/
public <E extends Executable & Comparable<?>> void execute(E executable) {
try {
@ -409,21 +466,13 @@ public class ActionQueue {
}
}
private void registerCleanupActions(Executable executable) {
beforeTransactionProcesses.register( executable.getBeforeTransactionCompletionProcess() );
if ( session.getFactory().getSettings().isQueryCacheEnabled() ) {
invalidateSpaces( executable.getPropertySpaces() );
}
afterTransactionProcesses.register( executable.getAfterTransactionCompletionProcess() );
}
/**
* This method is now called once per execution of an ExecutableList or once for execution of an Execution.
*
* @param spaces
* @param spaces The spaces to invalidate
*/
private void invalidateSpaces(Serializable... spaces) {
if ( spaces != null && spaces.length > 0 ) { // HHH-6286
if ( spaces != null && spaces.length > 0 ) {
for ( Serializable s : spaces ) {
afterTransactionProcesses.addSpaceToInvalidate( (String) s );
}
@ -432,12 +481,6 @@ public class ActionQueue {
}
}
private void prepareActions(ExecutableList<?> queue) throws HibernateException {
for ( Executable executable : queue ) {
executable.beforeExecutions();
}
}
/**
* Returns a string representation of the object.
*
@ -445,10 +488,15 @@ public class ActionQueue {
*/
@Override
public String toString() {
return new StringBuilder().append( "ActionQueue[insertions=" ).append( insertions ).append( " updates=" ).append( updates ).append( " deletions=" )
.append( deletions ).append( " collectionCreations=" ).append( collectionCreations ).append( " collectionRemovals=" )
.append( collectionRemovals ).append( " collectionUpdates=" ).append( collectionUpdates ).append( " collectionQueuedOps=" )
.append( collectionQueuedOps ).append( " unresolvedInsertDependencies=" ).append( unresolvedInsertions ).append( "]" ).toString();
return "ActionQueue[insertions=" + insertions
+ " updates=" + updates
+ " deletions=" + deletions
+ " collectionCreations=" + collectionCreations
+ " collectionRemovals=" + collectionRemovals
+ " collectionUpdates=" + collectionUpdates
+ " collectionQueuedOps=" + collectionQueuedOps
+ " unresolvedInsertDependencies=" + unresolvedInsertions
+ "]";
}
public int numberOfCollectionRemovals() {
@ -571,8 +619,10 @@ public class ActionQueue {
return rtn;
}
/**
* Encapsulates behavior needed for after transaction processing
*/
private static class BeforeTransactionCompletionProcessQueue {
private SessionImplementor session;
// Concurrency handling required when transaction completion process is dynamically registered
// inside event listener (HHH-7478).
@ -605,8 +655,10 @@ public class ActionQueue {
}
}
/**
* Encapsulates behavior needed for after transaction processing
*/
private static class AfterTransactionCompletionProcessQueue {
private SessionImplementor session;
private Set<String> querySpacesToInvalidate = new HashSet<String>();
// Concurrency handling required when transaction completion process is dynamically registered

View File

@ -38,22 +38,28 @@ import java.util.Set;
import org.hibernate.action.spi.Executable;
/**
* Encapsulates state relating to each executable list. Lazily sorts the list and caches the sorted state. Lazily
* calculates the spaces affected by the actions in the list, and caches this too.
*
* Specialized encapsulating of the state pertaining to each Executable list.
* <p/>
* Lazily sorts the list and caches the sorted state.
* <p/>
* Lazily calculates the querySpaces affected by the actions in the list, and caches this too.
*
* @author Steve Ebersole
* @author Anton Marsden
* @param <E>
*
* @param <E> Intersection type describing Executable implementations
*/
@SuppressWarnings("rawtypes")
public class ExecutableList<E extends Executable & Comparable & Serializable> implements Serializable, Iterable<E>, Externalizable {
public static final int INIT_QUEUE_LIST_SIZE = 5;
/**
* Provides a sorting interface for ExecutableList.
*
* @author Anton Marsden
* @param <E>
*/
public interface Sorter<E extends Executable> {
public static interface Sorter<E extends Executable> {
/**
* Sorts the list.
@ -61,13 +67,12 @@ public class ExecutableList<E extends Executable & Comparable & Serializable> im
void sort(List<E> l);
}
private final ExecutableList.Sorter<E> sorter;
private final ArrayList<E> executables;
private final Sorter<E> sorter;
private boolean sorted;
private transient Set<Serializable> spaces;
private transient Set<Serializable> querySpaces;
/**
* Creates a new ExecutableList.
@ -79,16 +84,16 @@ public class ExecutableList<E extends Executable & Comparable & Serializable> im
/**
* Creates a new ExecutableList using the specified Sorter.
*
* @param sorter
* @param sorter The Sorter to use; may be {@code null}
*/
public ExecutableList(ExecutableList.Sorter<E> sorter) {
this( 10, sorter ); // use the standard ArrayList initialCapacity
this( INIT_QUEUE_LIST_SIZE, sorter );
}
/**
* Creates a new ExecutableList with the specified initialCapacity.
*
* @param initialCapacity
* @param initialCapacity The initial capacity for instantiating the internal List
*/
ExecutableList(int initialCapacity) {
this( initialCapacity, null );
@ -96,16 +101,16 @@ public class ExecutableList<E extends Executable & Comparable & Serializable> im
/**
* Creates a new ExecutableList with the specified initialCapacity and Sorter.
*
* @param initialCapacity
* @param sorter
*
* @param initialCapacity The initial capacity for instantiating the internal List
* @param sorter The Sorter to use; may be {@code null}
*/
ExecutableList(int initialCapacity, ExecutableList.Sorter<E> sorter) {
this.sorter = sorter;
this.executables = new ArrayList<E>( initialCapacity );
// a non-null spaces value would add to the spaces as the list is added to,
// a non-null querySpaces value would add to the querySpaces as the list is added to,
// but we would like this data to be lazily initialized.
this.spaces = null;
this.querySpaces = null;
this.sorted = true;
}
@ -117,20 +122,22 @@ public class ExecutableList<E extends Executable & Comparable & Serializable> im
}
/**
* Removes the entry at position idx in the list.
* Removes the entry at position index in the list.
*
* @param idx
* @param index The index of the element to remove
*
* @return the entry that was removed
*/
public E remove(int idx) {
if ( idx < executables.size() - 1 ) {
public E remove(int index) {
if ( index < executables.size() - 1 ) {
sorted = false;
}
E e = executables.remove( idx );
// clear the spaces cache if the removed Executable had property spaces
final E e = executables.remove( index );
// clear the querySpaces cache if the removed Executable had property querySpaces
if ( e.getPropertySpaces() != null && e.getPropertySpaces().length > 0 ) {
spaces = null;
querySpaces = null;
}
return e;
}
@ -139,24 +146,23 @@ public class ExecutableList<E extends Executable & Comparable & Serializable> im
* Clears the list of executions.
*/
public void clear() {
// Note: another option here is to replace the list with a new one
executables.clear();
spaces = null;
querySpaces = null;
sorted = true;
}
/**
* Removes the last n entries from the list.
*
* @param n
* @param n The number of elements to remove.
*/
public void removeLastN(int n) {
if ( n > 0 ) {
int size = executables.size();
for ( Executable e : executables.subList( size - n, size ) ) {
if ( e.getPropertySpaces() != null && e.getPropertySpaces().length > 0 ) {
// spaces could now be incorrect
spaces = null;
// querySpaces could now be incorrect
querySpaces = null;
break;
}
}
@ -165,23 +171,21 @@ public class ExecutableList<E extends Executable & Comparable & Serializable> im
}
/**
* Lazily constructs the spaces affected by the actions in the list.
* Lazily constructs the querySpaces affected by the actions in the list.
*
* @return the spaces affected by the actions in this list
* @return the querySpaces affected by the actions in this list
*/
public Set<Serializable> getPropertySpaces() {
if ( spaces == null ) {
spaces = new HashSet<Serializable>();
public Set<Serializable> getQuerySpaces() {
if ( querySpaces == null ) {
querySpaces = new HashSet<Serializable>();
for ( E e : executables ) {
Serializable[] propertySpaces = e.getPropertySpaces();
if ( spaces != null && propertySpaces != null ) {
for ( Serializable s : propertySpaces ) {
spaces.add( s );
}
if ( querySpaces != null && propertySpaces != null ) {
Collections.addAll( querySpaces, propertySpaces );
}
}
}
return spaces;
return querySpaces;
}
/**
@ -196,11 +200,9 @@ public class ExecutableList<E extends Executable & Comparable & Serializable> im
// no longer sorted
sorted = false;
Serializable[] propertySpaces = o.getPropertySpaces();
// we can cheaply keep spaces in sync once they are cached
if ( spaces != null && propertySpaces != null ) {
for ( Serializable s : propertySpaces ) {
spaces.add( s );
}
// we can cheaply keep querySpaces in sync once they are cached
if ( querySpaces != null && propertySpaces != null ) {
Collections.addAll( querySpaces, propertySpaces );
}
}
return added;
@ -214,6 +216,7 @@ public class ExecutableList<E extends Executable & Comparable & Serializable> im
if ( sorted ) {
return;
}
if ( sorter != null ) {
sorter.sort( executables );
}
@ -231,11 +234,12 @@ public class ExecutableList<E extends Executable & Comparable & Serializable> im
}
/**
* @param idx
* @return the element at index idx
* @param index The index of the element to retrieve
*
* @return The element at specified index
*/
public E get(int idx) {
return executables.get( idx );
public E get(int index) {
return executables.get( index );
}
/**
@ -251,7 +255,7 @@ public class ExecutableList<E extends Executable & Comparable & Serializable> im
/**
* Serializes the list out to oos.
*
* @param oos
* @param oos The stream to which to serialize our state
*/
@Override
public void writeExternal(ObjectOutput oos) throws IOException {
@ -262,15 +266,15 @@ public class ExecutableList<E extends Executable & Comparable & Serializable> im
}
/**
* Deserializes the list into this object from in.
* De-serializes the list into this object from in.
*
* @param in
* @param in The stream from which to read our serial state
*/
@SuppressWarnings("unchecked")
@Override
@SuppressWarnings("unchecked")
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
sorted = false;
spaces = null;
querySpaces = null;
int size = in.readInt();
executables.ensureCapacity( size );
if ( size > 0 ) {
@ -282,9 +286,9 @@ public class ExecutableList<E extends Executable & Comparable & Serializable> im
}
/**
* Re-attaches the executables to the session after deserialization.
* Re-attaches the Executable elements to the session after deserialization.
*
* @param session
* @param session The session to which to attach the Executable elements
*/
public void afterDeserialize(SessionImplementor session) {
for ( E e : executables ) {

View File

@ -181,13 +181,13 @@ public class ExecutableListTest extends BaseUnitTestCase {
@Test
public void testGetSpaces() {
l.add( action1 );
Set<Serializable> ss = l.getPropertySpaces();
Set<Serializable> ss = l.getQuerySpaces();
Assert.assertEquals( 1, ss.size() );
Assert.assertTrue( ss.contains( "a" ) );
l.add( action2 );
l.add( action3 );
l.add( action4 );
Set<Serializable> ss2 = l.getPropertySpaces();
Set<Serializable> ss2 = l.getQuerySpaces();
Assert.assertEquals( 4, ss2.size() );
Assert.assertTrue( ss2.contains( "a" ) );
Assert.assertTrue( ss2.contains( "b" ) );
@ -196,11 +196,11 @@ public class ExecutableListTest extends BaseUnitTestCase {
Assert.assertTrue( ss == ss2 ); // same Set (cached)
// now remove action4
l.remove( 3 );
ss2 = l.getPropertySpaces();
ss2 = l.getQuerySpaces();
Assert.assertTrue( ss == ss2 ); // same Set (action4 has no spaces)
Assert.assertEquals( 4, ss2.size() );
l.remove( 2 );
ss2 = l.getPropertySpaces();
ss2 = l.getQuerySpaces();
Assert.assertTrue( ss != ss2 ); // Different Set because it has been rebuilt. This would be incorrect if
// Set.clear() was used
}