/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.brooklyn.entity.group; import static org.apache.brooklyn.test.Asserts.assertEqualsIgnoringOrder; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.entity.EntityLocal; import org.apache.brooklyn.api.entity.EntitySpec; import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.api.sensor.Sensor; import org.apache.brooklyn.api.sensor.SensorEvent; import org.apache.brooklyn.api.sensor.SensorEventListener; import org.apache.brooklyn.core.entity.AbstractEntity; import org.apache.brooklyn.core.entity.Entities; import org.apache.brooklyn.core.entity.EntityPredicates; import org.apache.brooklyn.core.sensor.Sensors; import org.apache.brooklyn.core.test.entity.TestApplication; import org.apache.brooklyn.core.test.entity.TestEntity; import org.apache.brooklyn.test.Asserts; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.time.Duration; import org.apache.brooklyn.util.time.Time; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; public class DynamicGroupTest { private static final Logger LOG = LoggerFactory.getLogger(DynamicGroupTest.class); private static final int TIMEOUT_MS = 50*1000; private static final int VERY_SHORT_WAIT_MS = 100; private TestApplication app; private DynamicGroup group; private TestEntity e1; private TestEntity e2; @BeforeMethod(alwaysRun=true) public void setUp() { app = TestApplication.Factory.newManagedInstanceForTests(); group = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)); e1 = app.createAndManageChild(EntitySpec.create(TestEntity.class)); e2 = app.createAndManageChild(EntitySpec.create(TestEntity.class)); } @AfterMethod(alwaysRun=true) public void tearDown() throws Exception { if (app != null) Entities.destroyAll(app.getManagementContext()); } @Test public void testGroupWithNoFilterReturnsNoMembers() throws Exception { assertTrue(group.getMembers().isEmpty()); } @Test public void testGroupWithNonMatchingFilterReturnsNoMembers() throws Exception { group.setEntityFilter(Predicates.alwaysFalse()); assertTrue(group.getMembers().isEmpty()); } @Test public void testGroupWithMatchingFilterReturnsOnlyMatchingMembers() throws Exception { group.setEntityFilter(EntityPredicates.idEqualTo(e1.getId())); assertEqualsIgnoringOrder(group.getMembers(), ImmutableList.of(e1)); } @Test public void testCanUsePredicateAsFilter() throws Exception { Predicate<Entity> predicate = Predicates.<Entity>equalTo(e1); group.setEntityFilter(predicate); assertEqualsIgnoringOrder(group.getMembers(), ImmutableSet.of(e1)); } @Test public void testGroupWithMatchingFilterReturnsEverythingThatMatches() throws Exception { group.setEntityFilter(Predicates.alwaysTrue()); assertEqualsIgnoringOrder(group.getMembers(), ImmutableSet.of(e1, e2, app, group)); } @Test public void testGroupDetectsNewlyManagedMatchingMember() throws Exception { group.setEntityFilter(EntityPredicates.displayNameEqualTo("myname")); final Entity e3 = app.addChild(EntitySpec.create(TestEntity.class).displayName("myname")); Asserts.succeedsEventually(new Runnable() { public void run() { assertEqualsIgnoringOrder(group.getMembers(), ImmutableSet.of(e3)); }}); } @Test public void testGroupUsesNewFilter() throws Exception { final Entity e3 = app.addChild(EntitySpec.create(TestEntity.class).displayName("myname")); group.setEntityFilter(EntityPredicates.displayNameEqualTo("myname")); assertEqualsIgnoringOrder(group.getMembers(), ImmutableSet.of(e3)); } @Test public void testGroupDetectsChangedEntities() throws Exception { final AttributeSensor<String> MY_ATTRIBUTE = Sensors.newStringSensor("test.myAttribute", "My test attribute"); group.setEntityFilter(EntityPredicates.attributeEqualTo(MY_ATTRIBUTE, "yes")); group.addSubscription(null, MY_ATTRIBUTE); assertEqualsIgnoringOrder(group.getMembers(), ImmutableSet.of()); // When changed (such that subscription spots it), then entity added e1.sensors().set(MY_ATTRIBUTE, "yes"); Asserts.succeedsEventually(new Runnable() { public void run() { assertEqualsIgnoringOrder(group.getMembers(), ImmutableSet.of(e1)); }}); // When it stops matching, entity is removed e1.sensors().set(MY_ATTRIBUTE, "no"); Asserts.succeedsEventually(new Runnable() { public void run() { assertEqualsIgnoringOrder(group.getMembers(), ImmutableSet.of()); }}); } @Test public void testGroupDetectsChangedEntitiesMatchingFilter() throws Exception { final AttributeSensor<String> MY_ATTRIBUTE = Sensors.newStringSensor("test.myAttribute", "My test attribute"); group.setEntityFilter(new Predicate<Entity>() { @Override public boolean apply(Entity input) { if (!(input.getAttribute(MY_ATTRIBUTE) == "yes")) return false; if (input.equals(e1)) { LOG.info("testGroupDetectsChangedEntitiesMatchingFilter scanned e1 when MY_ATTRIBUTE is yes; not a bug, but indicates things may be running slowly"); return false; } return true; }}); group.addSubscription(null, MY_ATTRIBUTE, new Predicate<SensorEvent<?>>() { @Override public boolean apply(SensorEvent<?> input) { return !e1.equals(input.getSource()); }}); assertEqualsIgnoringOrder(group.getMembers(), ImmutableSet.of()); // Does not subscribe to things which do not match predicate filter, // so event from e1 should normally be ignored // but pending rescans may cause it to pick up e1, so we ignore e1 in the entity filter also e1.sensors().set(MY_ATTRIBUTE, "yes"); e2.sensors().set(MY_ATTRIBUTE, "yes"); Asserts.succeedsEventually(new Runnable() { public void run() { assertEqualsIgnoringOrder(group.getMembers(), ImmutableSet.of(e2)); }}); } @Test public void testGroupRemovesUnmanagedEntity() throws Exception { group.setEntityFilter(EntityPredicates.idEqualTo(e1.getId())); assertEqualsIgnoringOrder(group.getMembers(), ImmutableSet.of(e1)); Entities.unmanage(e1); Asserts.succeedsEventually(new Runnable() { public void run() { assertEqualsIgnoringOrder(group.getMembers(), ImmutableSet.of()); }}); } // TODO Previously did Entities.unmanage(e1), but that now causes the group to be told // (to preserve referential integrity). Now doing Entities.unmanage(e3) instead. // Note that group.stop is now deprecated, so can delete this test when the method // is deleted! @Test public void testStoppedGroupIgnoresComingAndGoingsOfEntities() throws Exception { Entity e3 = new AbstractEntity() {}; group.setEntityFilter(Predicates.instanceOf(TestEntity.class)); assertEqualsIgnoringOrder(group.getMembers(), ImmutableSet.of(e1, e2)); group.stop(); e3.setParent(app); Entities.manage(e3); Asserts.succeedsContinually(MutableMap.of("timeout", VERY_SHORT_WAIT_MS), new Runnable() { public void run() { assertEquals(ImmutableSet.copyOf(group.getMembers()), ImmutableSet.of(e1, e2)); }}); Entities.unmanage(e3); Asserts.succeedsContinually(MutableMap.of("timeout", VERY_SHORT_WAIT_MS), new Runnable() { public void run() { assertEqualsIgnoringOrder(ImmutableSet.copyOf(group.getMembers()), ImmutableSet.of(e1, e2)); }}); } @Test public void testUnmanagedGroupIgnoresComingAndGoingsOfEntities() { Entity e3 = new AbstractEntity() {}; group.setEntityFilter(Predicates.instanceOf(TestEntity.class)); assertEqualsIgnoringOrder(group.getMembers(), ImmutableSet.of(e1, e2)); Entities.unmanage(group); e3.setParent(app); Entities.manage(e3); Asserts.succeedsContinually(MutableMap.of("timeout", VERY_SHORT_WAIT_MS), new Runnable() { public void run() { assertEqualsIgnoringOrder(ImmutableSet.copyOf(group.getMembers()), ImmutableSet.of(e1, e2)); }}); } // Motivated by strange behavior observed testing load-balancing policy, but this passed... // // Note that addMember/removeMember is now async for when member-entity is managed/unmanaged, // so to avoid race where entity is already unmanaged by the time addMember does its stuff, // we wait for it to really be added. @Test public void testGroupAddsAndRemovesManagedAndUnmanagedEntitiesExactlyOnce() throws Exception { final int NUM_CYCLES = 100; group.setEntityFilter(Predicates.instanceOf(TestEntity.class)); final Set<TestEntity> entitiesNotified = Sets.newConcurrentHashSet(); final AtomicInteger addedNotifications = new AtomicInteger(0); final AtomicInteger removedNotifications = new AtomicInteger(0); final List<Exception> exceptions = new CopyOnWriteArrayList<Exception>(); app.subscriptions().subscribe(group, DynamicGroup.MEMBER_ADDED, new SensorEventListener<Entity>() { public void onEvent(SensorEvent<Entity> event) { try { TestEntity val = (TestEntity) event.getValue(); LOG.debug("Notified of member added: member={}, thread={}", val.getId(), Thread.currentThread().getName()); assertEquals(group, event.getSource()); assertTrue(entitiesNotified.add(val)); addedNotifications.incrementAndGet(); } catch (Throwable t) { LOG.error("Error on event $event", t); exceptions.add(new Exception("Error on event $event", t)); } }}); app.subscriptions().subscribe(group, DynamicGroup.MEMBER_REMOVED, new SensorEventListener<Entity>() { public void onEvent(SensorEvent<Entity> event) { try { TestEntity val = (TestEntity) event.getValue(); LOG.debug("Notified of member removed: member={}, thread={}", val.getId(), Thread.currentThread().getName()); assertEquals(group, event.getSource()); assertTrue(entitiesNotified.remove(val)); removedNotifications.incrementAndGet(); } catch (Throwable t) { LOG.error("Error on event $event", t); exceptions.add(new Exception("Error on event $event", t)); } } }); for (int i = 0; i < NUM_CYCLES; i++) { final TestEntity entity = app.createAndManageChild(EntitySpec.create(TestEntity.class)); LOG.debug("Created: entity {}", i); Asserts.succeedsEventually(new Runnable() { public void run() { assertTrue(entitiesNotified.contains(entity)); } }); LOG.debug("Contained in entitiesNotified: entity {}", i); Entities.unmanage(entity); LOG.debug("Unmanaged: entity {}", i); } Asserts.succeedsEventually(ImmutableMap.of("timeout", Duration.of(10, TimeUnit.SECONDS)), new Runnable() { public void run() { int added = addedNotifications.get(), removed = removedNotifications.get(), notifications = added + removed; assertTrue(notifications == (NUM_CYCLES * 2) || exceptions.size() > 0, "addedNotifications=" + added + ", removedNotifications=" + removed + ", cycles=" + NUM_CYCLES * 2 + ", exceptions.size=" + exceptions.size()); } }); if (!exceptions.isEmpty()) { throw exceptions.get(0); } assertEquals(removedNotifications.get() + addedNotifications.get(), NUM_CYCLES*2); } // The entityAdded/entityRemoved is now async for when member-entity is managed/unmanaged, // but it should always be called sequentially (i.e. semantics of a single-threaded executor). // Test is deliberately slow in processing entityAdded/removed calls, to try to cause // concurrent calls if they are going to happen at all. @Test(groups="Integration") public void testEntityAddedAndRemovedCalledSequentially() throws Exception { final int NUM_CYCLES = 10; final Set<Entity> knownMembers = Sets.newLinkedHashSet(); final AtomicInteger notificationCount = new AtomicInteger(0); final AtomicInteger concurrentCallsCount = new AtomicInteger(0); final List<Exception> exceptions = new CopyOnWriteArrayList<Exception>(); DynamicGroupImpl group2 = new DynamicGroupImpl() { @Override protected void onEntityAdded(Entity item) { try { onCall("Member added: member="+item); assertTrue(knownMembers.add(item)); } catch (Throwable t) { exceptions.add(new Exception("Error detected adding "+item, t)); throw Exceptions.propagate(t); } } @Override protected void onEntityRemoved(Entity item) { try { onCall("Member removed: member="+item); assertTrue(knownMembers.remove(item)); } catch (Throwable t) { exceptions.add(new Exception("Error detected adding "+item, t)); throw Exceptions.propagate(t); } } private void onCall(String msg) { LOG.debug(msg+", thread="+Thread.currentThread().getName()); try { assertEquals(concurrentCallsCount.incrementAndGet(), 1); Time.sleep(100); } finally { concurrentCallsCount.decrementAndGet(); } notificationCount.incrementAndGet(); } }; group2.config().set(DynamicGroup.ENTITY_FILTER, Predicates.instanceOf(TestEntity.class)); app.addChild(group2); Entities.manage(group2); for (int i = 0; i < NUM_CYCLES; i++) { TestEntity entity = app.createAndManageChild(EntitySpec.create(TestEntity.class)); Entities.unmanage(entity); } Asserts.succeedsEventually(new Runnable() { public void run() { assertTrue(notificationCount.get() == (NUM_CYCLES*2) || exceptions.size() > 0); }}); if (exceptions.size() > 0) { throw exceptions.get(0); } assertEquals(notificationCount.get(), NUM_CYCLES*2); } // See Deadlock in https://github.com/brooklyncentral/brooklyn/issues/378 // TODO Now that entities are auto-managed, this test is no longer appropriate. // Should it be re-written or deleted? @Test(groups="WIP") public void testDoesNotDeadlockOnManagedAndMemberAddedConcurrently() throws Exception { final CountDownLatch rescanReachedLatch = new CountDownLatch(1); final CountDownLatch entityAddedReachedLatch = new CountDownLatch(1); final CountDownLatch rescanLatch = new CountDownLatch(1); final CountDownLatch entityAddedLatch = new CountDownLatch(1); final TestEntity e3 = app.addChild(EntitySpec.create(TestEntity.class)); final DynamicGroupImpl group2 = new DynamicGroupImpl() { @Override public void rescanEntities() { rescanReachedLatch.countDown(); try { rescanLatch.await(); } catch (InterruptedException e) { Exceptions.propagate(e); } super.rescanEntities(); } @Override protected void onEntityAdded(Entity item) { entityAddedReachedLatch.countDown(); try { entityAddedLatch.await(); } catch (InterruptedException e) { Exceptions.propagate(e); } super.onEntityAdded(item); } }; group2.config().set(DynamicGroup.ENTITY_FILTER, Predicates.<Object>equalTo(e3)); app.addChild(group2); Thread t1 = new Thread(new Runnable() { @Override public void run() { Entities.manage(group2); }}); Thread t2 = new Thread(new Runnable() { @Override public void run() { Entities.manage(e3); }}); t1.start(); try { assertTrue(rescanReachedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); t2.start(); assertTrue(entityAddedReachedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); entityAddedLatch.countDown(); rescanLatch.countDown(); t2.join(TIMEOUT_MS); t1.join(TIMEOUT_MS); assertFalse(t1.isAlive()); assertFalse(t2.isAlive()); } finally { t1.interrupt(); t2.interrupt(); } Asserts.succeedsEventually(new Runnable() { public void run() { assertEqualsIgnoringOrder(group2.getMembers(), ImmutableSet.of(e3)); }}); } // See deadlock in https://issues.apache.org/jira/browse/BROOKLYN-66 @Test public void testDoesNotDeadlockOnUnmanageWhileOtherMemberBeingAdded() throws Exception { final CountDownLatch removingMemberReachedLatch = new CountDownLatch(1); final CountDownLatch addingMemberReachedLatch = new CountDownLatch(1); final CountDownLatch addingMemberContinueLatch = new CountDownLatch(1); final AtomicBoolean addingMemberDoLatching = new AtomicBoolean(false); final List<Entity> membersAdded = new CopyOnWriteArrayList<Entity>(); final DynamicGroupImpl group2 = new DynamicGroupImpl() { private final BasicSensorSupport interceptedSensors = new BasicSensorSupport() { @Override public <T> void emit(Sensor<T> sensor, T val) { // intercept inside AbstractGroup.addMember, while it still holds lock on members if (sensor == AbstractGroup.MEMBER_ADDED && addingMemberDoLatching.get()) { addingMemberReachedLatch.countDown(); try { addingMemberContinueLatch.await(); } catch (InterruptedException e) { throw Exceptions.propagate(e); } } super.emit(sensor, val); } }; @Override public BasicSensorSupport sensors() { return interceptedSensors; } @Override public boolean removeMember(final Entity member) { removingMemberReachedLatch.countDown(); return super.removeMember(member); } }; group2.config().set(DynamicGroup.MEMBER_DELEGATE_CHILDREN, true); app.addChild(group2); Entities.manage(group2); app.subscriptions().subscribe(group2, AbstractGroup.MEMBER_ADDED, new SensorEventListener<Entity>() { @Override public void onEvent(SensorEvent<Entity> event) { membersAdded.add(event.getValue()); }}); final TestEntity e2 = app.createAndManageChild(EntitySpec.create(TestEntity.class)); final TestEntity e3 = app.createAndManageChild(EntitySpec.create(TestEntity.class)); group2.addMember(e2); assertContainsEventually(membersAdded, e2); addingMemberDoLatching.set(true); Thread t1 = new Thread(new Runnable() { @Override public void run() { try { addingMemberReachedLatch.await(); } catch (InterruptedException e) { throw Exceptions.propagate(e); } Entities.unmanage(e2); }}); Thread t2 = new Thread(new Runnable() { @Override public void run() { group2.addMember(e3); }}); t1.start(); t2.start(); try { removingMemberReachedLatch.await(); addingMemberContinueLatch.countDown(); t1.join(TIMEOUT_MS); t2.join(TIMEOUT_MS); assertFalse(t1.isAlive()); assertFalse(t2.isAlive()); } finally { t1.interrupt(); t2.interrupt(); } } private <T> void assertContainsEventually(final Collection<? extends T> vals, final T val) { Asserts.succeedsEventually(new Runnable() { public void run() { assertTrue(vals.contains(val)); }}); } }