brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From aleds...@apache.org
Subject [01/12] incubator-brooklyn git commit: brooklyn-qa: add org.apache package prefix
Date Wed, 15 Jul 2015 23:09:45 GMT
Repository: incubator-brooklyn
Updated Branches:
  refs/heads/master 726bebca3 -> 12625d82c


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/test/java/org/apache/brooklyn/qa/load/LoadTest.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/test/java/org/apache/brooklyn/qa/load/LoadTest.java b/usage/qa/src/test/java/org/apache/brooklyn/qa/load/LoadTest.java
new file mode 100644
index 0000000..d6c479c
--- /dev/null
+++ b/usage/qa/src/test/java/org/apache/brooklyn/qa/load/LoadTest.java
@@ -0,0 +1,244 @@
+/*
+ * 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.qa.load;
+
+import org.apache.brooklyn.qa.load.SimulatedTheeTierApp;
+import static org.testng.Assert.assertEquals;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+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.basic.Entities;
+import brooklyn.entity.basic.StartableApplication;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.entity.rebind.persister.PersistMode;
+import brooklyn.entity.trait.Startable;
+import brooklyn.launcher.BrooklynLauncher;
+import brooklyn.location.Location;
+import brooklyn.management.ManagementContext;
+import brooklyn.management.ha.HighAvailabilityMode;
+import brooklyn.management.internal.LocalManagementContext;
+import brooklyn.test.PerformanceTestUtils;
+import brooklyn.util.os.Os;
+import brooklyn.util.time.Duration;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+/**
+ * Customers ask about the scalability of Brooklyn. These load tests investigate how many

+ * concurrent apps can be deployed and managed by a single Brooklyn management node.
+ * 
+ * The apps are "simulated" in that they don't create the underlying resources 
+ * (we are not checking if the test machine can run 100s of app-servers simultaneously!)

+ * The install/customize/launch will instead execute ssh commands of comparable length,
+ * but that just echo rather than execute the actual commands.
+ * 
+ * "SIMULATE_EXTERNAL_MONITORING" means that we do not poll the entities directly (over ssh,
http or 
+ * whatever). Instead we simulate the metrics being injected directly to be set on the entity
(e.g. 
+ * having been collected from a Graphite server).
+ * 
+ * "SKIP_SSH_ON_START" means don't do the normal install+customize+launch ssh commands. Instead,
just
+ * startup the entities so we can monitor their resource usage.
+ */
+public class LoadTest {
+
+    // TODO Could/should issue provisioning request through REST api, rather than programmatically;

+    // and poll to detect completion.
+    
+    /*
+     * Useful commands when investigating:
+     *     LOG_FILE=usage/qa/brooklyn-camp-tests.log
+     *     grep -E "OutOfMemoryError|[P|p]rovisioning time|sleeping before|CPU fraction|LoadTest
using" $LOG_FILE | less
+     *     grep -E "OutOfMemoryError|[P|p]rovisioning time" $LOG_FILE; grep "CPU fraction"
$LOG_FILE | tail -1; grep "LoadTest using" $LOG_FILE | tail -1
+     *     grep -E "OutOfMemoryError|LoadTest using" $LOG_FILE
+     */
+    private static final Logger LOG = LoggerFactory.getLogger(LoadTest.class);
+
+    private File persistenceDir;
+    private BrooklynLauncher launcher;
+    private String webServerUrl;
+    private ManagementContext managementContext;
+    private ListeningExecutorService executor;
+    private Future<?> cpuFuture;
+    
+    private Location localhost;
+    
+    List<Duration> provisioningTimes;
+
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        // Create management node
+        persistenceDir = Files.createTempDir();
+        launcher = BrooklynLauncher.newInstance()
+                .persistMode(PersistMode.CLEAN)
+                .highAvailabilityMode(HighAvailabilityMode.MASTER)
+                .persistenceDir(persistenceDir)
+                .start();
+        webServerUrl = launcher.getServerDetails().getWebServerUrl();
+        managementContext = launcher.getServerDetails().getManagementContext();
+
+        localhost = managementContext.getLocationRegistry().resolve("localhost");
+        
+        provisioningTimes = Collections.synchronizedList(Lists.<Duration>newArrayList());
+
+        // Create executors
+        executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+
+        // Monitor utilisation (memory/CPU) while tests run
+        executor.submit(new Callable<Void>() {
+            public Void call() {
+                try {
+                    while (true) {
+                        managementContext.getExecutionManager(); // force GC to be instantiated
+                        String usage = ((LocalManagementContext)managementContext).getGarbageCollector().getUsageString();
+                        LOG.info("LoadTest using "+usage);
+                        Thread.sleep(1000);
+                    }
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt(); // exit gracefully
+                } catch (Exception e) {
+                    LOG.error("Error getting usage info", e);
+                }
+                return null;
+            }});
+        
+        cpuFuture = PerformanceTestUtils.sampleProcessCpuTime(Duration.ONE_SECOND, "during
testProvisionAppsConcurrently");
+
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (cpuFuture != null) cpuFuture.cancel(true);
+        if (executor != null) executor.shutdownNow();
+        if (launcher != null) launcher.terminate();
+        if (persistenceDir != null) Os.deleteRecursively(persistenceDir);
+    }
+    
+    /**
+     * Creates multiple apps simultaneously. 
+     * 
+     * Long-term target is 50 concurrent provisioning requests (which may be issued while
there are
+     * many existing applications under management). Until we reach that point, we can partition
the
+     * load across multiple (separate) brooklyn management nodes.
+     *   TODO TBD: is that 50 VMs worth, or 50 apps with 4 VMs in each? 
+     * 
+     * TODO Does not measure the cost of jclouds for creating all the VMs/containers.
+     */
+    @Test(groups="Acceptance")
+    public void testLocalhostProvisioningAppsConcurrently() throws Exception {
+        final int NUM_CONCURRENT_APPS_PROVISIONING = 20; 
+        
+        List<ListenableFuture<StartableApplication>> futures = Lists.newArrayList();
+        for (int i = 0; i < NUM_CONCURRENT_APPS_PROVISIONING; i++) {
+            ListenableFuture<StartableApplication> future = executor.submit(newProvisionAppTask(managementContext,

+                    EntitySpec.create(StartableApplication.class, SimulatedTheeTierApp.class)
+                            .configure(SimulatedTheeTierApp.SIMULATE_EXTERNAL_MONITORING,
true)
+                            .displayName("Simulated app "+i)));
+            futures.add(future);
+        }
+        
+        List<StartableApplication> apps = Futures.allAsList(futures).get();
+        
+        for (StartableApplication app : apps) {
+            assertEquals(app.getAttribute(Startable.SERVICE_UP), (Boolean)true);
+        }
+    }
+    
+    /**
+     * Creates many apps, to monitor resource usage etc.
+     * 
+     * "SIMULATE_EXTERNAL_MONITORING" means that we do not poll the entities directly (over
ssh, http or 
+     * whatever). Instead we simulate the metrics being injected directly to be set on the
entity (e.g. 
+     * having been collected from a Graphite server).
+     * 
+     * Long-term target is 2500 VMs under management.
+     * Until we reach that point, we can partition the load across multiple (separate) brooklyn
management nodes.
+     */
+    @Test(groups="Acceptance")
+    public void testLocalhostManyApps() throws Exception {
+        final int NUM_APPS = 630; // target is 2500 VMs; each blueprint has 4 (rounding up)
+        final int NUM_APPS_PER_BATCH = 10;
+        final int SLEEP_BETWEEN_BATCHES = 10*1000;
+        final boolean SKIP_SSH_ON_START = true; // getting ssh errors otherwise!
+        
+        int counter = 0;
+        
+        for (int i = 0; i < NUM_APPS / NUM_APPS_PER_BATCH; i++) {
+            List<ListenableFuture<StartableApplication>> futures = Lists.newArrayList();
+            for (int j = 0; j < NUM_APPS_PER_BATCH; j++) {
+                ListenableFuture<StartableApplication> future = executor.submit(newProvisionAppTask(
+                        managementContext, 
+                        EntitySpec.create(StartableApplication.class, SimulatedTheeTierApp.class)
+                                .configure(SimulatedTheeTierApp.SIMULATE_EXTERNAL_MONITORING,
true)
+                                .configure(SimulatedTheeTierApp.SKIP_SSH_ON_START, SKIP_SSH_ON_START)
+                                .displayName("Simulated app "+(++counter))));
+                futures.add(future);
+            }
+            
+            List<StartableApplication> apps = Futures.allAsList(futures).get();
+            
+            for (StartableApplication app : apps) {
+                assertEquals(app.getAttribute(Startable.SERVICE_UP), (Boolean)true);
+            }
+
+            synchronized (provisioningTimes) {
+                LOG.info("cycle="+i+"; numApps="+counter+": provisioning times: "+provisioningTimes);
+                provisioningTimes.clear();
+            }
+
+            LOG.info("cycle="+i+"; numApps="+counter+": sleeping before next batch of apps");
+            Thread.sleep(SLEEP_BETWEEN_BATCHES);
+        }
+    }
+    
+    protected <T extends StartableApplication> Callable<T> newProvisionAppTask(final
ManagementContext managementContext, final EntitySpec<T> entitySpec) {
+        return new Callable<T>() {
+            public T call() {
+                Stopwatch stopwatch = Stopwatch.createStarted();
+                T app = managementContext.getEntityManager().createEntity(entitySpec);
+                Entities.startManagement(app, managementContext);
+                app.start(ImmutableList.of(localhost));
+                Duration duration = Duration.of(stopwatch.elapsed(TimeUnit.MILLISECONDS),
TimeUnit.MILLISECONDS);
+                LOG.info("Provisioning time: "+duration);
+                provisioningTimes.add(duration);
+
+                return app;
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/MonitorUtilsTest.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/MonitorUtilsTest.java
b/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/MonitorUtilsTest.java
new file mode 100644
index 0000000..cf377e9
--- /dev/null
+++ b/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/MonitorUtilsTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.qa.longevity;
+
+import org.apache.brooklyn.qa.longevity.MonitorUtils;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+import org.apache.brooklyn.qa.longevity.MonitorUtils.ProcessHasStderr;
+import brooklyn.util.os.Os;
+import brooklyn.util.text.Strings;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Files;
+
+public class MonitorUtilsTest {
+
+    @Test(enabled=false, timeOut=1000) // Demonstrates that process.waitFor() hangs for big
output streams
+    public void testExecuteAndWaitFor() throws Exception {
+        Process process = createDumpingProcess(false);
+        process.waitFor();
+        fail("Should block while waiting to consume process output");
+    }
+
+    @Test(enabled=false, timeOut=1000) // Demonstrates that process.waitFor() hangs for big
err streams
+    public void testExecuteAndWaitForErr() throws Exception {
+        Process process = createDumpingProcess(true);
+        process.waitFor();
+        fail("Should block while waiting to consume process output");
+    }
+
+    @Test(timeOut=1000)
+    public void testExecuteAndWaitForConsumingOutputStream() throws Exception {
+        Process process = createDumpingProcess(false);
+        String out = MonitorUtils.waitFor(process);
+        assertTrue(out.length() > 100000, "out.size="+out.length());
+    }
+
+    @Test(timeOut=1000, expectedExceptions=IllegalStateException.class)
+    public void testExecuteAndWaitForConsumingErrorStream() throws Exception {
+        Process process = createDumpingProcess(true);
+        MonitorUtils.waitFor(process);
+    }
+
+    private Process createDumpingProcess(boolean writeToErr) throws IOException {
+        String errSuffix = writeToErr ? " >&2" : "";
+        //Windows limits the length of the arguments so echo multiple times instead
+        String bigstr = Strings.repeat("a", 8000);
+        String bigcmd = Strings.repeat(getSilentPrefix() + "echo " + bigstr + errSuffix +
Os.LINE_SEPARATOR, 15);
+        File file = Os.newTempFile("test-consume", ".bat");
+        file.setExecutable(true);
+        Files.write(bigcmd, file, Charsets.UTF_8);
+        Process process = MonitorUtils.exec(file.getAbsolutePath());
+        return process;
+    }
+
+    @Test(groups="UNIX")
+    public void testFindOwnPid() throws Exception {
+        int ownpid = MonitorUtils.findOwnPid();
+        assertTrue(ownpid > 0, "ownpid=$ownpid");
+        assertTrue(MonitorUtils.isPidRunning(ownpid, "java"),"java is not running");
+    }
+
+    @Test(groups="UNIX")
+    public void testIsPidRunning() throws Exception {
+        int usedPid = MonitorUtils.findOwnPid();
+
+        //the child process will terminate freeing it PID
+        String[] cmd = new String[]{"bash", "-c", "echo $$"};
+        Process process = Runtime.getRuntime().exec(cmd);
+        String out = MonitorUtils.waitFor(process);
+        int unusedPid = Integer.parseInt(out.trim());
+
+        assertTrue(MonitorUtils.isPidRunning(usedPid));
+        assertFalse(MonitorUtils.isPidRunning(unusedPid));
+        
+        try {
+            assertFalse(MonitorUtils.isPidRunning(1234567)); // too large
+        } catch (ProcessHasStderr e) {
+            // expected on osx
+        }
+    }
+
+    @Test(groups="UNIX")
+    public void testGetRunningPids() throws Exception {
+        int ownpid = MonitorUtils.findOwnPid();
+
+        List<Integer> javapids = MonitorUtils.getRunningPids("java");
+
+        assertTrue(javapids.contains(ownpid), "javapids="+javapids+"; ownpid="+ownpid);
+    }
+
+    @Test
+    public void testIsUrlUp() throws Exception {
+        assertFalse(MonitorUtils.isUrlUp(new URL("http://localhost/thispathdoesnotexist")));
+    }
+
+    @Test(groups="UNIX")
+    public void testSearchLog() throws Exception {
+        String fileContents = "line1\nline2\nline3\n";
+        File file = File.createTempFile("monitorUtilsTest.testSearchLog", ".txt");
+        Files.write(fileContents, file, Charsets.UTF_8);
+
+        try {
+            assertEquals(MonitorUtils.searchLog(file, "line1"), Arrays.asList("line1"));
+            assertEquals(MonitorUtils.searchLog(file, "line1|line2"), Arrays.asList("line1",
"line2"));
+            assertEquals(MonitorUtils.searchLog(file, "textnotthere"), Collections.emptyList());
+            assertEquals(MonitorUtils.searchLog(file, "line"), Arrays.asList("line1", "line2",
"line3"));
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test(groups="Integration")
+    public void testMemoryUsage() throws Exception {
+        int ownpid = MonitorUtils.findOwnPid();
+
+        MonitorUtils.MemoryUsage memUsage = MonitorUtils.getMemoryUsage(ownpid);
+        assertTrue(memUsage.totalInstances > 0, memUsage.toString());
+        assertTrue(memUsage.totalMemoryBytes > 0, memUsage.toString());
+        assertEquals(memUsage.getInstanceCounts(), Collections.emptyMap());
+
+        MonitorUtils.MemoryUsage memUsage2 = MonitorUtils.getMemoryUsage(ownpid, MonitorUtilsTest.class.getCanonicalName(),0);
+        assertEquals(memUsage2.getInstanceCounts(), ImmutableMap.of(MonitorUtilsTest.class.getCanonicalName(),
1));
+
+        MonitorUtils.MemoryUsage memUsage3 = MonitorUtils.getMemoryUsage(ownpid, MonitorUtilsTest.class.getCanonicalName(),
2);
+        assertEquals(memUsage3.getInstanceCounts(), Collections.emptyMap());
+    }
+
+    private String getSilentPrefix() {
+        if (Os.isMicrosoftWindows()) {
+            return "@";
+        } else {
+            return "";
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/webcluster/SinusoidalLoadGenerator.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/webcluster/SinusoidalLoadGenerator.java
b/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/webcluster/SinusoidalLoadGenerator.java
new file mode 100644
index 0000000..17e7a32
--- /dev/null
+++ b/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/webcluster/SinusoidalLoadGenerator.java
@@ -0,0 +1,90 @@
+/*
+ * 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.qa.longevity.webcluster;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.enricher.basic.AbstractEnricher;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.EntityLocal;
+import brooklyn.event.AttributeSensor;
+
+import com.google.common.base.Throwables;
+import com.google.common.reflect.TypeToken;
+
+/**
+ * Periodically publishes values in the range of 0 to #amplitude. 
+ * The value varies sinusoidally over time.
+ */
+public class SinusoidalLoadGenerator extends AbstractEnricher {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SinusoidalLoadGenerator.class);
+
+    public static final ConfigKey<AttributeSensor<Double>> TARGET = ConfigKeys.newConfigKey(new
TypeToken<AttributeSensor<Double>>() {}, "target");
+    
+    public static final ConfigKey<Long> PUBLISH_PERIOD_MS = ConfigKeys.newLongConfigKey("publishPeriodMs");
+
+    public static final ConfigKey<Long> SIN_PERIOD_MS = ConfigKeys.newLongConfigKey("sinPeriodMs");
+
+    public static final ConfigKey<Double> SIN_AMPLITUDE = ConfigKeys.newDoubleConfigKey("sinAmplitude");
+
+    private final ScheduledExecutorService executor;
+    
+    public SinusoidalLoadGenerator() {
+        this.executor = Executors.newSingleThreadScheduledExecutor();
+    }
+    
+    public SinusoidalLoadGenerator(AttributeSensor<Double> target, long publishPeriodMs,
long sinPeriodMs, double sinAmplitude) {
+        config().set(TARGET, target);
+        config().set(PUBLISH_PERIOD_MS, publishPeriodMs);
+        config().set(SIN_PERIOD_MS, sinPeriodMs);
+        config().set(SIN_AMPLITUDE, sinAmplitude);
+        this.executor = Executors.newSingleThreadScheduledExecutor();
+    }
+    
+    @Override
+    public void setEntity(final EntityLocal entity) {
+        super.setEntity(entity);
+        
+        executor.scheduleAtFixedRate(new Runnable() {
+            @Override public void run() {
+                try {
+                    long time = System.currentTimeMillis();
+                    double val = getRequiredConfig(SIN_AMPLITUDE) * (1 + Math.sin( (1.0*time)
/ getRequiredConfig(SIN_PERIOD_MS) * Math.PI * 2  - Math.PI/2 )) / 2;
+                    entity.setAttribute(getRequiredConfig(TARGET), val);
+                } catch (Throwable t) {
+                    LOG.warn("Error generating sinusoidal-load metric", t);
+                    throw Throwables.propagate(t);
+                }
+            }
+
+        }, 0, getRequiredConfig(PUBLISH_PERIOD_MS), TimeUnit.MILLISECONDS);
+    }
+    
+    @Override
+    public void destroy() {
+        executor.shutdownNow();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/webcluster/WebClusterApp.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/webcluster/WebClusterApp.java
b/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/webcluster/WebClusterApp.java
new file mode 100644
index 0000000..0d3d694
--- /dev/null
+++ b/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/webcluster/WebClusterApp.java
@@ -0,0 +1,101 @@
+/*
+ * 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.qa.longevity.webcluster;
+
+import java.util.List;
+
+import brooklyn.config.BrooklynProperties;
+import brooklyn.enricher.Enrichers;
+import brooklyn.entity.basic.AbstractApplication;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.StartableApplication;
+import brooklyn.entity.proxy.nginx.NginxController;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.entity.webapp.ControlledDynamicWebAppCluster;
+import brooklyn.entity.webapp.jboss.JBoss7Server;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.Sensors;
+import brooklyn.launcher.BrooklynLauncher;
+import brooklyn.policy.EnricherSpec;
+import brooklyn.policy.autoscaling.AutoScalerPolicy;
+import brooklyn.util.CommandLineUtil;
+
+import com.google.common.collect.Lists;
+
+public class WebClusterApp extends AbstractApplication {
+
+    static BrooklynProperties config = BrooklynProperties.Factory.newDefault();
+
+    public static final String WAR_PATH = "classpath://hello-world.war";
+
+    private static final long loadCyclePeriodMs = 2 * 60 * 1000L;
+
+    @Override
+    public void initApp() {
+        final AttributeSensor<Double> sinusoidalLoad =
+                Sensors.newDoubleSensor("brooklyn.qa.sinusoidalLoad", "Sinusoidal server
load");
+        AttributeSensor<Double> averageLoad =
+                Sensors.newDoubleSensor("brooklyn.qa.averageLoad", "Average load in cluster");
+
+        NginxController nginxController = addChild(EntitySpec.create(NginxController.class)
+                // .configure("domain", "webclusterexample.brooklyn.local")
+                .configure("port", "8000+"));
+
+        EntitySpec<JBoss7Server> jbossSpec = EntitySpec.create(JBoss7Server.class)
+                .configure("httpPort", "8080+")
+                .configure("war", WAR_PATH)
+                .enricher(EnricherSpec.create(SinusoidalLoadGenerator.class)
+                        .configure(SinusoidalLoadGenerator.TARGET, sinusoidalLoad)
+                        .configure(SinusoidalLoadGenerator.PUBLISH_PERIOD_MS, 500L)
+                        .configure(SinusoidalLoadGenerator.SIN_PERIOD_MS, loadCyclePeriodMs)
+                        .configure(SinusoidalLoadGenerator.SIN_AMPLITUDE, 1d));
+
+        ControlledDynamicWebAppCluster web = addChild(EntitySpec.create(ControlledDynamicWebAppCluster.class)
+                .displayName("WebApp cluster")
+                .configure("controller", nginxController)
+                .configure("initialSize", 1)
+                .configure("memberSpec", jbossSpec));
+
+        web.getCluster().addEnricher(Enrichers.builder()
+                .aggregating(sinusoidalLoad)
+                .publishing(averageLoad)
+                .fromMembers()
+                .computingAverage()
+                .build());
+        web.getCluster().addPolicy(AutoScalerPolicy.builder()
+                .metric(averageLoad)
+                .sizeRange(1, 3)
+                .metricRange(0.3, 0.7)
+                .build());
+    }
+    
+    public static void main(String[] argv) {
+        List<String> args = Lists.newArrayList(argv);
+        String port =  CommandLineUtil.getCommandLineOption(args, "--port", "8081+");
+        String location = CommandLineUtil.getCommandLineOption(args, "--location", "localhost");
+
+        BrooklynLauncher launcher = BrooklynLauncher.newInstance()
+                .application(EntitySpec.create(StartableApplication.class, WebClusterApp.class).displayName("Brooklyn
WebApp Cluster example"))
+                .webconsolePort(port)
+                .location(location)
+                .start();
+         
+        Entities.dumpInfo(launcher.getApplications());
+    }
+}


Mime
View raw message