You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by al...@apache.org on 2014/11/10 16:59:47 UTC

[1/4] incubator-brooklyn git commit: Adds Listener support for usage/metering info

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 <al...@gmail.com>
Authored: Wed Nov 5 22:44:40 2014 +0000
Committer: Aled Sage <al...@gmail.com>
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);
+    }
+}


[2/4] incubator-brooklyn git commit: TypeCoercions: recursively unpack for List

Posted by al...@apache.org.
TypeCoercions: recursively unpack for List<T>

- e.g. if given “1,2” for type List<Integer>, then first pass will
  coerce to List.of(“1”, “2”), and second pass will coerce to
  List.of(1,2)


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

Branch: refs/heads/master
Commit: e3154097506624c6c385a64fc6a2e5c466039b8d
Parents: beb87db
Author: Aled Sage <al...@gmail.com>
Authored: Fri Nov 7 19:44:14 2014 +0000
Committer: Aled Sage <al...@gmail.com>
Committed: Mon Nov 10 11:34:35 2014 +0000

----------------------------------------------------------------------
 .../java/brooklyn/util/flags/TypeCoercions.java     | 16 ++++++++++++++--
 .../brooklyn/util/internal/TypeCoercionsTest.java   |  6 ++++++
 2 files changed, 20 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e3154097/core/src/main/java/brooklyn/util/flags/TypeCoercions.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/util/flags/TypeCoercions.java b/core/src/main/java/brooklyn/util/flags/TypeCoercions.java
index f3caeaf..ec08ad0 100644
--- a/core/src/main/java/brooklyn/util/flags/TypeCoercions.java
+++ b/core/src/main/java/brooklyn/util/flags/TypeCoercions.java
@@ -64,6 +64,7 @@ import brooklyn.util.yaml.Yamls;
 
 import com.google.common.base.CaseFormat;
 import com.google.common.base.Function;
+import com.google.common.base.Objects;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.collect.HashBasedTable;
@@ -235,7 +236,18 @@ public class TypeCoercions {
             Map<Class, Function> adapters = registry.row(targetType);
             for (Map.Entry<Class, Function> entry : adapters.entrySet()) {
                 if (entry.getKey().isInstance(value)) {
-                    return (T) entry.getValue().apply(value);
+                    T result = (T) entry.getValue().apply(value);
+                    
+                    // Check if need to unwrap again (e.g. if want List<Integer> and are given a String "1,2,3"
+                    // then we'll have so far converted to List.of("1", "2", "3"). Call recursively.
+                    // First check that value has changed, to avoid stack overflow!
+                    if (!Objects.equal(value, result) && targetTypeToken.getType() instanceof ParameterizedType) {
+                        // Could duplicate check for `result instanceof Collection` etc; but recursive call
+                        // will be fine as if that doesn't match we'll safely reach `targetType.isInstance(value)`
+                        // and just return the result.
+                        return coerce(result, targetTypeToken);
+                    }
+                    return result;
                 }
             }
         }
@@ -459,7 +471,7 @@ public class TypeCoercions {
         return null;
     }
 
-    public synchronized static <A,B> void registerAdapter(Class<A> sourceType, Class<B> targetType, Function<A,B> fn) {
+    public synchronized static <A,B> void registerAdapter(Class<A> sourceType, Class<B> targetType, Function<? super A,B> fn) {
         registry.put(targetType, sourceType, fn);
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e3154097/core/src/test/java/brooklyn/util/internal/TypeCoercionsTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/util/internal/TypeCoercionsTest.java b/core/src/test/java/brooklyn/util/internal/TypeCoercionsTest.java
index 8ac1e9c..b4d0470 100644
--- a/core/src/test/java/brooklyn/util/internal/TypeCoercionsTest.java
+++ b/core/src/test/java/brooklyn/util/internal/TypeCoercionsTest.java
@@ -222,6 +222,12 @@ public class TypeCoercionsTest {
     }
 
     @Test
+    @SuppressWarnings("serial")
+    public void testCoerceRecursivelyStringToGenericsCollection() {
+        assertEquals(TypeCoercions.coerce("1,2", new TypeToken<List<Integer>>() {}), ImmutableList.of(1, 2));
+    }
+    
+    @Test
     public void testJsonStringToMapCoercion() {
         Map<?,?> s = TypeCoercions.coerce("{ \"a\" : \"1\", b : 2 }", Map.class);
         Assert.assertEquals(s, ImmutableMap.of("a", "1", "b", 2));


[4/4] incubator-brooklyn git commit: This closes #310

Posted by al...@apache.org.
This closes #310


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

Branch: refs/heads/master
Commit: 3cfda7e8ffd573dc1144c47f954f0350973b9e3d
Parents: 3610d8a 7ee06ba
Author: Aled Sage <al...@gmail.com>
Authored: Mon Nov 10 15:59:33 2014 +0000
Committer: Aled Sage <al...@gmail.com>
Committed: Mon Nov 10 15:59:33 2014 +0000

----------------------------------------------------------------------
 .../management/internal/LocalUsageManager.java  |  84 ++++++++-
 .../internal/NonDeploymentUsageManager.java     |  22 +++
 .../management/internal/UsageManager.java       |  40 +++++
 .../java/brooklyn/util/flags/TypeCoercions.java |  16 +-
 .../util/internal/TypeCoercionsTest.java        |   6 +
 .../usage/ApplicationUsageTrackingTest.java     | 179 +++++++++++++++++++
 .../usage/LocationUsageTrackingTest.java        | 108 ++++++++---
 .../usage/RecordingUsageListener.java           |  70 ++++++++
 .../management/usage/UsageListenerTest.java     | 108 +++++++++++
 9 files changed, 598 insertions(+), 35 deletions(-)
----------------------------------------------------------------------



[3/4] incubator-brooklyn git commit: Read UsageListeners from config in brooklyn.properties

Posted by al...@apache.org.
Read UsageListeners from config in brooklyn.properties


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

Branch: refs/heads/master
Commit: 7ee06ba3c67a6d5ea1cb494b75aabb20b00c13f0
Parents: e315409
Author: Aled Sage <al...@gmail.com>
Authored: Fri Nov 7 19:44:34 2014 +0000
Committer: Aled Sage <al...@gmail.com>
Committed: Mon Nov 10 11:39:53 2014 +0000

----------------------------------------------------------------------
 .../management/internal/LocalUsageManager.java  |  29 +++++
 .../management/internal/UsageManager.java       |  11 ++
 .../management/usage/UsageListenerTest.java     | 108 +++++++++++++++++++
 3 files changed, 148 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/7ee06ba3/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 1bb4a8e..476ea3e 100644
--- a/core/src/main/java/brooklyn/management/internal/LocalUsageManager.java
+++ b/core/src/main/java/brooklyn/management/internal/LocalUsageManager.java
@@ -20,6 +20,7 @@ package brooklyn.management.internal;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -41,8 +42,12 @@ import brooklyn.location.basic.LocationInternal;
 import brooklyn.management.usage.ApplicationUsage;
 import brooklyn.management.usage.LocationUsage;
 import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.flags.TypeCoercions;
+import brooklyn.util.javalang.Reflections;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
@@ -59,6 +64,23 @@ public class LocalUsageManager implements UsageManager {
     
     private static final Logger log = LoggerFactory.getLogger(LocalUsageManager.class);
 
+    // Register a coercion from String->UsageListener, so that USAGE_LISTENERS defined in brooklyn.properties
+    // will be instantiated, given their class names.
+    static {
+        TypeCoercions.registerAdapter(String.class, UsageListener.class, new Function<String, UsageListener>() {
+            @Override public UsageListener apply(String input) {
+                // TODO Want to use classLoader = mgmt.getCatalog().getRootClassLoader();
+                ClassLoader classLoader = LocalUsageManager.class.getClassLoader();
+                Optional<Object> result = Reflections.invokeConstructorWithArgs(classLoader, input);
+                if (result.isPresent()) {
+                    return (UsageListener) result.get();
+                } else {
+                    throw new IllegalStateException("Failed to create UsageListener from class name '"+input+"' using no-arg constructor");
+                }
+            }
+        });
+    }
+    
     @VisibleForTesting
     public static final String APPLICATION_USAGE_KEY = "usage-application";
     
@@ -77,6 +99,13 @@ public class LocalUsageManager implements UsageManager {
 
     public LocalUsageManager(LocalManagementContext managementContext) {
         this.managementContext = checkNotNull(managementContext, "managementContext");
+        
+        Collection<UsageListener> listeners = managementContext.getBrooklynProperties().getConfig(UsageManager.USAGE_LISTENERS);
+        if (listeners != null) {
+            for (UsageListener listener : listeners) {
+                addUsageListener(listener);
+            }
+        }
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/7ee06ba3/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 35e602f..56a5ace 100644
--- a/core/src/main/java/brooklyn/management/internal/UsageManager.java
+++ b/core/src/main/java/brooklyn/management/internal/UsageManager.java
@@ -18,10 +18,13 @@
  */
 package brooklyn.management.internal;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import brooklyn.config.ConfigKey;
 import brooklyn.entity.Application;
+import brooklyn.entity.basic.ConfigKeys;
 import brooklyn.entity.basic.Lifecycle;
 import brooklyn.location.Location;
 import brooklyn.management.usage.ApplicationUsage;
@@ -31,10 +34,18 @@ import brooklyn.management.usage.LocationUsage.LocationEvent;
 
 import com.google.common.annotations.Beta;
 import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.reflect.TypeToken;
 
 @Beta
 public interface UsageManager {
 
+    @SuppressWarnings("serial")
+    public static final ConfigKey<List<UsageListener>> USAGE_LISTENERS = ConfigKeys.newConfigKey(
+            new TypeToken<List<UsageListener>>() {},
+            "brooklyn.usageManager.listeners", "Optional usage listeners (i.e. for metering)",
+            ImmutableList.<UsageListener>of());
+
     public interface UsageListener {
         public static final UsageListener NOOP = new UsageListener() {
             @Override public void onApplicationEvent(String applicationId, String applicationName, String entityType, 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/7ee06ba3/software/base/src/test/java/brooklyn/management/usage/UsageListenerTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/brooklyn/management/usage/UsageListenerTest.java b/software/base/src/test/java/brooklyn/management/usage/UsageListenerTest.java
new file mode 100644
index 0000000..0cc27c7
--- /dev/null
+++ b/software/base/src/test/java/brooklyn/management/usage/UsageListenerTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.assertTrue;
+
+import java.util.List;
+
+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.config.BrooklynProperties;
+import brooklyn.entity.basic.Entities;
+import brooklyn.location.Location;
+import brooklyn.management.internal.ManagementContextInternal;
+import brooklyn.management.internal.UsageManager;
+import brooklyn.management.internal.UsageManager.UsageListener;
+import brooklyn.test.Asserts;
+import brooklyn.test.entity.LocalManagementContextForTests;
+import brooklyn.test.entity.TestApplication;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+public class UsageListenerTest {
+
+    // Also see {Application|Location}UsageTrackingTest for listener functionality
+    
+    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 {
+        RecordingStaticUsageListener.clearInstances();
+    }
+
+    @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;
+            RecordingStaticUsageListener.clearInstances();
+        }
+    }
+
+    @Test
+    public void testAddUsageListenerViaProperties() throws Exception {
+        BrooklynProperties brooklynProperties = BrooklynProperties.Factory.newEmpty();
+        brooklynProperties.put(UsageManager.USAGE_LISTENERS, RecordingStaticUsageListener.class.getName());
+        mgmt = LocalManagementContextForTests.newInstance(brooklynProperties);
+        
+        app = TestApplication.Factory.newManagedInstanceForTests(mgmt);
+        app.start(ImmutableList.<Location>of());
+
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = RecordingStaticUsageListener.getInstance().getApplicationEvents();
+                assertTrue(events.size() > 0, "events="+events); // expect some events
+            }});
+    }
+    
+    public static class RecordingStaticUsageListener extends RecordingUsageListener implements UsageListener {
+        private static final List<RecordingStaticUsageListener> STATIC_INSTANCES = Lists.newCopyOnWriteArrayList();
+        
+        public static RecordingStaticUsageListener getInstance() {
+            return Iterables.getOnlyElement(STATIC_INSTANCES);
+        }
+
+        public static void clearInstances() {
+            STATIC_INSTANCES.clear();
+        }
+        
+        public RecordingStaticUsageListener() {
+            // Bad to leak a ref to this before constructor finished, but we'll live with it because
+            // it's just test code!
+            STATIC_INSTANCES.add(this);
+        }
+    }
+}