Repository: incubator-brooklyn
Updated Branches:
  refs/heads/master 3610d8a5f -> 3cfda7e8f


Adds Listener support for usage/metering info


Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo
Commit: 
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/beb87db7
Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/beb87db7
Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/beb87db7

Branch: refs/heads/master
Commit: beb87db7c78dc57258626ce0dc53e5cece39846d
Parents: c323b00
Author: Aled Sage <[email protected]>
Authored: Wed Nov 5 22:44:40 2014 +0000
Committer: Aled Sage <[email protected]>
Committed: Mon Nov 10 11:34:30 2014 +0000

----------------------------------------------------------------------
 .../management/internal/LocalUsageManager.java  |  55 +++++-
 .../internal/NonDeploymentUsageManager.java     |  22 +++
 .../management/internal/UsageManager.java       |  29 +++
 .../usage/ApplicationUsageTrackingTest.java     | 179 +++++++++++++++++++
 .../usage/LocationUsageTrackingTest.java        | 108 ++++++++---
 .../usage/RecordingUsageListener.java           |  70 ++++++++
 6 files changed, 430 insertions(+), 33 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/beb87db7/core/src/main/java/brooklyn/management/internal/LocalUsageManager.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/brooklyn/management/internal/LocalUsageManager.java 
b/core/src/main/java/brooklyn/management/internal/LocalUsageManager.java
index 65efe44..1bb4a8e 100644
--- a/core/src/main/java/brooklyn/management/internal/LocalUsageManager.java
+++ b/core/src/main/java/brooklyn/management/internal/LocalUsageManager.java
@@ -20,9 +20,12 @@ package brooklyn.management.internal;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -37,10 +40,13 @@ import brooklyn.location.basic.LocationConfigKeys;
 import brooklyn.location.basic.LocationInternal;
 import brooklyn.management.usage.ApplicationUsage;
 import brooklyn.management.usage.LocationUsage;
+import brooklyn.util.exceptions.Exceptions;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Predicate;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
 
 public class LocalUsageManager implements UsageManager {
 
@@ -62,13 +68,19 @@ public class LocalUsageManager implements UsageManager {
     private final LocalManagementContext managementContext;
     
     private final Object mutex = new Object();
+
+    private final List<UsageListener> listeners = 
Lists.newCopyOnWriteArrayList();
     
+    private ExecutorService listenerExecutor = 
Executors.newSingleThreadExecutor(new ThreadFactoryBuilder()
+            .setNameFormat("brooklyn-usagemanager-listener-%d")
+            .build());
+
     public LocalUsageManager(LocalManagementContext managementContext) {
         this.managementContext = checkNotNull(managementContext, 
"managementContext");
     }
 
     @Override
-    public void recordApplicationEvent(Application app, Lifecycle state) {
+    public void recordApplicationEvent(final Application app, final Lifecycle 
state) {
         log.debug("Storing application lifecycle usage event: application {} 
in state {}", new Object[] {app, state});
         ConcurrentMap<String, ApplicationUsage> eventMap = 
managementContext.getStorage().getMap(APPLICATION_USAGE_KEY);
         synchronized (mutex) {
@@ -76,8 +88,21 @@ public class LocalUsageManager implements UsageManager {
             if (usage == null) {
                 usage = new ApplicationUsage(app.getId(), 
app.getDisplayName(), app.getEntityType().getName(), 
((EntityInternal)app).toMetadataRecord());
             }
-            usage.addEvent(new ApplicationUsage.ApplicationEvent(state));      
  
+            final ApplicationUsage.ApplicationEvent event = new 
ApplicationUsage.ApplicationEvent(state);
+            usage.addEvent(event);        
             eventMap.put(app.getId(), usage);
+            
+            for (final UsageListener listener : listeners) {
+                listenerExecutor.execute(new Runnable() {
+                    public void run() {
+                        try {
+                            listener.onApplicationEvent(app.getId(), 
app.getDisplayName(), app.getEntityType().getName(), 
((EntityInternal)app).toMetadataRecord(), event);
+                        } catch (Exception e) {
+                            log.error("Problem notifying listener "+listener+" 
of applicationEvent("+app+", "+state+")", e);
+                            Exceptions.propagateIfFatal(e);
+                        }
+                    }});
+            }
         }
     }
     
@@ -86,7 +111,7 @@ public class LocalUsageManager implements UsageManager {
      * record if one does not already exist).
      */
     @Override
-    public void recordLocationEvent(Location loc, Lifecycle state) {
+    public void recordLocationEvent(final Location loc, final Lifecycle state) 
{
         // TODO This approach (i.e. recording events on manage/unmanage would 
not work for
         // locations that are reused. For example, in a 
FixedListMachineProvisioningLocation
         // the ssh machine location is returned to the pool and handed back 
out again.
@@ -116,7 +141,7 @@ public class LocalUsageManager implements UsageManager {
             Entity caller = (Entity) callerContext;
             String entityTypeName = caller.getEntityType().getName();
             String appId = caller.getApplicationId();
-            LocationUsage.LocationEvent event = new 
LocationUsage.LocationEvent(state, caller.getId(), entityTypeName, appId);
+            final LocationUsage.LocationEvent event = new 
LocationUsage.LocationEvent(state, caller.getId(), entityTypeName, appId);
             
             ConcurrentMap<String, LocationUsage> usageMap = 
managementContext.getStorage().<String, 
LocationUsage>getMap(LOCATION_USAGE_KEY);
             synchronized (mutex) {
@@ -126,6 +151,18 @@ public class LocalUsageManager implements UsageManager {
                 }
                 usage.addEvent(event);
                 usageMap.put(loc.getId(), usage);
+                
+                for (final UsageListener listener : listeners) {
+                    listenerExecutor.execute(new Runnable() {
+                        public void run() {
+                            try {
+                                listener.onLocationEvent(loc.getId(), 
((LocationInternal)loc).toMetadataRecord(), event);
+                            } catch (Exception e) {
+                                log.error("Problem notifying listener 
"+listener+" of locationEvent("+loc+", "+state+")", e);
+                                Exceptions.propagateIfFatal(e);
+                            }
+                        }});
+                }
             }
         } else {
             // normal for high-level locations
@@ -194,4 +231,14 @@ public class LocalUsageManager implements UsageManager {
         }
         return result;
     }
+
+    @Override
+    public void addUsageListener(UsageListener listener) {
+        listeners.add(listener);
+    }
+
+    @Override
+    public void removeUsageListener(UsageListener listener) {
+        listeners.remove(listener);
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/beb87db7/core/src/main/java/brooklyn/management/internal/NonDeploymentUsageManager.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/brooklyn/management/internal/NonDeploymentUsageManager.java
 
b/core/src/main/java/brooklyn/management/internal/NonDeploymentUsageManager.java
index 9eb79d4..f8e6a81 100644
--- 
a/core/src/main/java/brooklyn/management/internal/NonDeploymentUsageManager.java
+++ 
b/core/src/main/java/brooklyn/management/internal/NonDeploymentUsageManager.java
@@ -31,6 +31,10 @@ import com.google.common.base.Predicate;
 
 public class NonDeploymentUsageManager implements UsageManager {
 
+    // TODO All the `isInitialManagementContextReal()` code-checks is a 
code-smell.
+    // Expect we can delete a lot of this once we guarantee that all entities 
are 
+    // instantiated via EntitySpec / EntityManager. Until then, we'll live 
with this.
+    
     private final ManagementContextInternal initialManagementContext;
     
     public NonDeploymentUsageManager(ManagementContextInternal 
initialManagementContext) {
@@ -94,4 +98,22 @@ public class NonDeploymentUsageManager implements 
UsageManager {
             throw new IllegalStateException("Non-deployment context "+this+" 
is not valid for this operation");
         }
     }
+
+    @Override
+    public void addUsageListener(UsageListener listener) {
+        if (isInitialManagementContextReal()) {
+            
initialManagementContext.getUsageManager().addUsageListener(listener);
+        } else {
+            throw new IllegalStateException("Non-deployment context "+this+" 
is not valid for this operation");
+        }
+    }
+
+    @Override
+    public void removeUsageListener(UsageListener listener) {
+        if (isInitialManagementContextReal()) {
+            
initialManagementContext.getUsageManager().removeUsageListener(listener);
+        } else {
+            throw new IllegalStateException("Non-deployment context "+this+" 
is not valid for this operation");
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/beb87db7/core/src/main/java/brooklyn/management/internal/UsageManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/UsageManager.java 
b/core/src/main/java/brooklyn/management/internal/UsageManager.java
index 809ef66..35e602f 100644
--- a/core/src/main/java/brooklyn/management/internal/UsageManager.java
+++ b/core/src/main/java/brooklyn/management/internal/UsageManager.java
@@ -18,13 +18,16 @@
  */
 package brooklyn.management.internal;
 
+import java.util.Map;
 import java.util.Set;
 
 import brooklyn.entity.Application;
 import brooklyn.entity.basic.Lifecycle;
 import brooklyn.location.Location;
 import brooklyn.management.usage.ApplicationUsage;
+import brooklyn.management.usage.ApplicationUsage.ApplicationEvent;
 import brooklyn.management.usage.LocationUsage;
+import brooklyn.management.usage.LocationUsage.LocationEvent;
 
 import com.google.common.annotations.Beta;
 import com.google.common.base.Predicate;
@@ -32,6 +35,19 @@ import com.google.common.base.Predicate;
 @Beta
 public interface UsageManager {
 
+    public interface UsageListener {
+        public static final UsageListener NOOP = new UsageListener() {
+            @Override public void onApplicationEvent(String applicationId, 
String applicationName, String entityType, 
+                    Map<String, String> metadata, ApplicationEvent event) {} 
+            @Override public void onLocationEvent(String locationId, 
Map<String, String> metadata, LocationEvent event) {}
+        };
+        
+        void onApplicationEvent(String applicationId, String applicationName, 
String entityType, 
+                Map<String, String> metadata, ApplicationEvent event);
+        
+        void onLocationEvent(String locationId, Map<String, String> metadata, 
LocationEvent event);
+    }
+
     /**
      * Adds this application event to the usage record for the given app 
(creating the usage 
      * record if one does not already exist).
@@ -66,4 +82,17 @@ public interface UsageManager {
      */
     Set<ApplicationUsage> getApplicationUsage(Predicate<? super 
ApplicationUsage> filter);
 
+    /**
+     * Adds the given listener, to be notified on recording of 
application/location events.
+     * The listener notifications may be asynchronous.
+     * 
+     * As of 0.7.0, the listener is not persisted so will be lost on 
restart/rebind. This
+     * behaviour may change in a subsequent release. 
+     */
+    void addUsageListener(UsageListener listener);
+
+    /**
+     * Removes the given listener.
+     */
+    void removeUsageListener(UsageListener listener);
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/beb87db7/software/base/src/test/java/brooklyn/management/usage/ApplicationUsageTrackingTest.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/brooklyn/management/usage/ApplicationUsageTrackingTest.java
 
b/software/base/src/test/java/brooklyn/management/usage/ApplicationUsageTrackingTest.java
new file mode 100644
index 0000000..e90f2df
--- /dev/null
+++ 
b/software/base/src/test/java/brooklyn/management/usage/ApplicationUsageTrackingTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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 brooklyn.management.usage;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.fail;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.Application;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.location.Location;
+import brooklyn.management.internal.ManagementContextInternal;
+import brooklyn.management.usage.ApplicationUsage.ApplicationEvent;
+import brooklyn.test.Asserts;
+import brooklyn.test.entity.LocalManagementContextForTests;
+import brooklyn.test.entity.TestApplication;
+import brooklyn.util.time.Time;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+
+public class ApplicationUsageTrackingTest {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ApplicationUsageTrackingTest.class);
+
+    protected TestApplication app;
+    protected ManagementContextInternal mgmt;
+
+    protected boolean shouldSkipOnBoxBaseDirResolution() {
+        return true;
+    }
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        mgmt = LocalManagementContextForTests.newInstance();
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        try {
+            if (mgmt != null) Entities.destroyAll(mgmt);
+        } catch (Throwable t) {
+            LOG.error("Caught exception in tearDown method", t);
+        } finally {
+            mgmt = null;
+        }
+    }
+
+    @Test
+    public void testUsageInitiallyEmpty() {
+        Set<ApplicationUsage> usage = 
mgmt.getUsageManager().getApplicationUsage(Predicates.alwaysTrue());
+        assertEquals(usage, ImmutableSet.of());
+    }
+
+    @Test
+    public void testAddAndRemoveUsageListener() throws Exception {
+        final RecordingUsageListener listener = new RecordingUsageListener();
+        mgmt.getUsageManager().addUsageListener(listener);
+        
+        app = TestApplication.Factory.newManagedInstanceForTests(mgmt);
+        app.start(ImmutableList.<Location>of());
+
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = listener.getApplicationEvents();
+                assertEquals(events.size(), 2, "events="+events); // expect 
STARTING and RUNNING
+                
+                String appId = (String) events.get(0).get(1);
+                String appName = (String) events.get(0).get(2);
+                String entityType = (String) events.get(0).get(3);
+                Map<?,?> metadata = (Map<?, ?>) events.get(0).get(4);
+                ApplicationEvent appEvent = (ApplicationEvent) 
events.get(0).get(5);
+                
+                assertEquals(appId, app.getId(), "events="+events);
+                assertNotNull(appName, "events="+events);
+                assertNotNull(entityType, "events="+events);
+                assertNotNull(metadata, "events="+events);
+                assertEquals(appEvent.getState(), Lifecycle.STARTING, 
"events="+events);
+            }});
+
+
+        // Remove the listener; will get no more notifications
+        listener.clearEvents();
+        mgmt.getUsageManager().removeUsageListener(listener);
+        
+        app.start(ImmutableList.<Location>of());
+        Asserts.succeedsContinually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = listener.getLocationEvents();
+                assertEquals(events.size(), 0, "events="+events);
+            }});
+    }
+
+    @Test
+    public void testUsageIncludesStartAndStopEvents() {
+        // Start event
+        long preStart = System.currentTimeMillis();
+        app = TestApplication.Factory.newManagedInstanceForTests(mgmt);
+        app.start(ImmutableList.<Location>of());
+        long postStart = System.currentTimeMillis();
+
+        Set<ApplicationUsage> usages1 = 
mgmt.getUsageManager().getApplicationUsage(Predicates.alwaysTrue());
+        ApplicationUsage usage1 = Iterables.getOnlyElement(usages1);
+        assertApplicationUsage(usage1, app);
+        assertApplicationEvent(usage1.getEvents().get(0), Lifecycle.STARTING, 
preStart, postStart);
+        assertApplicationEvent(usage1.getEvents().get(1), Lifecycle.RUNNING, 
preStart, postStart);
+
+        // Stop events
+        long preStop = System.currentTimeMillis();
+        app.stop();
+        long postStop = System.currentTimeMillis();
+
+        Set<ApplicationUsage> usages2 = 
mgmt.getUsageManager().getApplicationUsage(Predicates.alwaysTrue());
+        ApplicationUsage usage2 = Iterables.getOnlyElement(usages2);
+        assertApplicationUsage(usage2, app);
+        assertApplicationEvent(usage2.getEvents().get(2), Lifecycle.STOPPING, 
preStop, postStop);
+        assertApplicationEvent(usage2.getEvents().get(3), Lifecycle.STOPPED, 
preStop, postStop);
+        
+        // Destroy
+        long preDestroy = System.currentTimeMillis();
+        Entities.unmanage(app);
+        long postDestroy = System.currentTimeMillis();
+        
+        Set<ApplicationUsage> usages3 = 
mgmt.getUsageManager().getApplicationUsage(Predicates.alwaysTrue());
+        ApplicationUsage usage3 = Iterables.getOnlyElement(usages3);
+        assertApplicationUsage(usage3, app);
+        assertApplicationEvent(usage3.getEvents().get(4), Lifecycle.DESTROYED, 
preDestroy, postDestroy);
+        
+        assertEquals(usage3.getEvents().size(), 5, "usage="+usage3);
+    }
+    
+    private void assertApplicationUsage(ApplicationUsage usage, Application 
expectedApp) {
+        assertEquals(usage.getApplicationId(), expectedApp.getId());
+        assertEquals(usage.getApplicationName(), expectedApp.getDisplayName());
+        assertEquals(usage.getEntityType(), 
expectedApp.getEntityType().getName());
+    }
+    
+    private void assertApplicationEvent(ApplicationEvent event, Lifecycle 
expectedState, long preEvent, long postEvent) {
+        // Saw times differ by 1ms - perhaps different threads calling 
currentTimeMillis() can get out-of-order times?!
+        final int TIMING_GRACE = 5;
+        
+        assertEquals(event.getState(), expectedState);
+        long eventTime = event.getDate().getTime();
+        if (eventTime < (preEvent - TIMING_GRACE) || eventTime > (postEvent + 
TIMING_GRACE)) {
+            fail("for "+expectedState+": event=" + 
Time.makeDateString(eventTime) + "("+eventTime + "); "
+                    + "pre=" + Time.makeDateString(preEvent) + " ("+preEvent+ 
"); "
+                    + "post=" + Time.makeDateString(postEvent) + " 
("+postEvent + ")");
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/beb87db7/software/base/src/test/java/brooklyn/management/usage/LocationUsageTrackingTest.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/brooklyn/management/usage/LocationUsageTrackingTest.java
 
b/software/base/src/test/java/brooklyn/management/usage/LocationUsageTrackingTest.java
index c390b04..3ad836d 100644
--- 
a/software/base/src/test/java/brooklyn/management/usage/LocationUsageTrackingTest.java
+++ 
b/software/base/src/test/java/brooklyn/management/usage/LocationUsageTrackingTest.java
@@ -19,7 +19,8 @@
 package brooklyn.management.usage;
 
 import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.fail;
 
 import java.util.List;
 import java.util.Map;
@@ -29,15 +30,17 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import brooklyn.entity.BrooklynAppUnitTestSupport;
+import brooklyn.entity.Entity;
 import brooklyn.entity.basic.Lifecycle;
 import brooklyn.entity.basic.SoftwareProcessEntityTest;
 import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.location.Location;
 import brooklyn.location.LocationSpec;
 import brooklyn.location.NoMachinesAvailableException;
 import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
 import brooklyn.location.basic.SshMachineLocation;
 import brooklyn.management.usage.LocationUsage.LocationEvent;
-import brooklyn.util.time.Duration;
+import brooklyn.test.Asserts;
 import brooklyn.util.time.Time;
 
 import com.google.common.base.Predicates;
@@ -48,8 +51,8 @@ import com.google.common.collect.Iterables;
 public class LocationUsageTrackingTest extends BrooklynAppUnitTestSupport {
 
     private DynamicLocalhostMachineProvisioningLocation loc;
-    
-    @BeforeMethod(alwaysRun=true)
+
+    @BeforeMethod(alwaysRun = true)
     @Override
     public void setUp() throws Exception {
         super.setUp();
@@ -63,46 +66,68 @@ public class LocationUsageTrackingTest extends 
BrooklynAppUnitTestSupport {
     }
 
     @Test
+    public void testAddAndRemoveUsageListener() throws Exception {
+        final RecordingUsageListener listener = new RecordingUsageListener();
+        mgmt.getUsageManager().addUsageListener(listener);
+        
+        
app.createAndManageChild(EntitySpec.create(SoftwareProcessEntityTest.MyService.class));
+        app.start(ImmutableList.of(loc));
+        final SshMachineLocation machine = 
Iterables.getOnlyElement(loc.getAllMachines());
+        
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = listener.getLocationEvents();
+                String locId = (String) events.get(0).get(1);
+                LocationEvent locEvent = (LocationEvent) events.get(0).get(3);
+                Map<?,?> metadata = (Map<?, ?>) events.get(0).get(2);
+                
+                assertEquals(events.size(), 1, "events="+events);
+                assertEquals(locId, machine.getId(), "events="+events);
+                assertNotNull(metadata, "events="+events);
+                assertEquals(locEvent.getApplicationId(), app.getId(), 
"events="+events);
+                assertEquals(locEvent.getState(), Lifecycle.CREATED, 
"events="+events);
+            }});
+
+        // Remove the listener; will get no more notifications
+        listener.clearEvents();
+        mgmt.getUsageManager().removeUsageListener(listener);
+        
+        app.stop();
+        Asserts.succeedsContinually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = listener.getLocationEvents();
+                assertEquals(events.size(), 0, "events="+events);
+            }});
+    }
+
+    @Test
     public void testUsageIncludesStartAndStopEvents() {
         SoftwareProcessEntityTest.MyService entity = 
app.createAndManageChild(EntitySpec.create(SoftwareProcessEntityTest.MyService.class));
-        
+
         // Start the app; expect record of location in use
         long preStart = System.currentTimeMillis();
         app.start(ImmutableList.of(loc));
         long postStart = System.currentTimeMillis();
         SshMachineLocation machine = 
Iterables.getOnlyElement(loc.getAllMachines());
-        
+
         Set<LocationUsage> usages1 = 
mgmt.getUsageManager().getLocationUsage(Predicates.alwaysTrue());
         LocationUsage usage1 = Iterables.getOnlyElement(usages1);
-        List<LocationEvent> events1 = usage1.getEvents();
-        LocationEvent event1 = Iterables.getOnlyElement(events1);
-        
-        assertEquals(usage1.getLocationId(), machine.getId());
-        assertEquals(event1.getApplicationId(), app.getId());
-        assertEquals(event1.getEntityId(), entity.getId());
-        assertEquals(event1.getState(), Lifecycle.CREATED);
-        long event1Time = event1.getDate().getTime();
-        assertTrue(event1Time >= preStart && event1Time <= postStart, 
"event1="+event1Time+"; pre="+preStart+"; post="+postStart);
-        
+        assertLocationUsage(usage1, machine);
+        assertLocationEvent(usage1.getEvents().get(0), entity, 
Lifecycle.CREATED, preStart, postStart);
+
         // Stop the app; expect record of location no longer in use
         long preStop = System.currentTimeMillis();
         app.stop();
         long postStop = System.currentTimeMillis();
-        
+
         Set<LocationUsage> usages2 = 
mgmt.getUsageManager().getLocationUsage(Predicates.alwaysTrue());
         LocationUsage usage2 = Iterables.getOnlyElement(usages2);
-        List<LocationEvent> events2 = usage2.getEvents();
-        LocationEvent event2 = events2.get(1);
-
-        assertEquals(events2.get(0).getDate(), event1.getDate());
-        assertEquals(usage2.getLocationId(), machine.getId());
-        assertEquals(event2.getApplicationId(), app.getId());
-        assertEquals(event2.getEntityId(), entity.getId());
-        assertEquals(event2.getState(), Lifecycle.DESTROYED);
-        long event2Time = event2.getDate().getTime();
-        assertTrue(event2Time >= preStop && event2Time <= postStop, 
"event2="+event2Time+"; pre="+preStop+"; post="+postStop);
+        assertLocationUsage(usage2, machine);
+        assertLocationEvent(usage2.getEvents().get(1), app.getApplicationId(), 
entity.getId(), entity.getEntityType().getName(), Lifecycle.DESTROYED, preStop, 
postStop);
+        
+        assertEquals(usage2.getEvents().size(), 2, "usage="+usage2);
     }
-    
+
     public static class DynamicLocalhostMachineProvisioningLocation extends 
LocalhostMachineProvisioningLocation {
         private static final long serialVersionUID = 4822009936654077946L;
 
@@ -111,7 +136,7 @@ public class LocationUsageTrackingTest extends 
BrooklynAppUnitTestSupport {
             System.out.println("called 
DynamicLocalhostMachineProvisioningLocation.obtain");
             return super.obtain(flags);
         }
-        
+
         @Override
         public void release(SshMachineLocation machine) {
             System.out.println("called 
DynamicLocalhostMachineProvisioningLocation.release");
@@ -120,4 +145,29 @@ public class LocationUsageTrackingTest extends 
BrooklynAppUnitTestSupport {
             super.removeChild(machine);
         }
     }
+    
+    private void assertLocationUsage(LocationUsage usage, Location 
expectedLoc) {
+        assertEquals(usage.getLocationId(), expectedLoc.getId(), 
"usage="+usage);
+        assertNotNull(usage.getMetadata(), "usage="+usage);
+    }
+
+    private void assertLocationEvent(LocationEvent event, Entity 
expectedEntity, Lifecycle expectedState, long preEvent, long postEvent) {
+        assertLocationEvent(event, expectedEntity.getApplicationId(), 
expectedEntity.getId(), expectedEntity.getEntityType().getName(), 
expectedState, preEvent, postEvent);
+    }
+    
+    private void assertLocationEvent(LocationEvent event, String 
expectedAppId, String expectedEntityId, String expectedEntityType, Lifecycle 
expectedState, long preEvent, long postEvent) {
+        // Saw times differ by 1ms - perhaps different threads calling 
currentTimeMillis() can get out-of-order times?!
+        final int TIMING_GRACE = 5;
+        
+        assertEquals(event.getApplicationId(), expectedAppId);
+        assertEquals(event.getEntityId(), expectedEntityId);
+        assertEquals(event.getEntityType(), expectedEntityType);
+        assertEquals(event.getState(), expectedState);
+        long eventTime = event.getDate().getTime();
+        if (eventTime < (preEvent - TIMING_GRACE) || eventTime > (postEvent + 
TIMING_GRACE)) {
+            fail("for "+expectedState+": event=" + 
Time.makeDateString(eventTime) + "("+eventTime + "); "
+                    + "pre=" + Time.makeDateString(preEvent) + " ("+preEvent+ 
"); "
+                    + "post=" + Time.makeDateString(postEvent) + " 
("+postEvent + ")");
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/beb87db7/software/base/src/test/java/brooklyn/management/usage/RecordingUsageListener.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/brooklyn/management/usage/RecordingUsageListener.java
 
b/software/base/src/test/java/brooklyn/management/usage/RecordingUsageListener.java
new file mode 100644
index 0000000..fa5cadc
--- /dev/null
+++ 
b/software/base/src/test/java/brooklyn/management/usage/RecordingUsageListener.java
@@ -0,0 +1,70 @@
+/*
+ * 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 brooklyn.management.usage;
+
+import java.util.List;
+import java.util.Map;
+
+import brooklyn.management.internal.UsageManager.UsageListener;
+import brooklyn.management.usage.ApplicationUsage.ApplicationEvent;
+import brooklyn.management.usage.LocationUsage.LocationEvent;
+import brooklyn.util.collections.MutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+public class RecordingUsageListener implements UsageListener {
+
+    private final List<List<?>> events = Lists.newCopyOnWriteArrayList();
+    
+    @Override
+    public void onApplicationEvent(String applicationId, String 
applicationName, String entityType, 
+            Map<String, String> metadata, ApplicationEvent event) {
+        events.add(MutableList.of("application", applicationId, 
applicationName, entityType, metadata, event));
+    }
+
+    @Override
+    public void onLocationEvent(String locationId, Map<String, String> 
metadata, LocationEvent event) {
+        events.add(MutableList.of("location", locationId, metadata, event));
+    }
+    
+    public void clearEvents() {
+        events.clear();
+    }
+    
+    public List<List<?>> getEvents() {
+        return ImmutableList.copyOf(events);
+    }
+    
+    public List<List<?>> getLocationEvents() {
+        List<List<?>> result = Lists.newArrayList();
+        for (List<?> event : events) {
+            if (event.get(0).equals("location")) result.add(event);
+        }
+        return ImmutableList.copyOf(result);
+    }
+    
+    public List<List<?>> getApplicationEvents() {
+        List<List<?>> result = Lists.newArrayList();
+        for (List<?> event : events) {
+            if (event.get(0).equals("application")) result.add(event);
+        }
+        return ImmutableList.copyOf(result);
+    }
+}

Reply via email to