/* * 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.feed.jmx; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicInteger; import javax.management.MBeanOperationInfo; import javax.management.MBeanParameterInfo; import javax.management.Notification; import javax.management.NotificationListener; import javax.management.ObjectName; import javax.management.StandardEmitterMBean; import javax.management.openmbean.CompositeDataSupport; import javax.management.openmbean.CompositeType; import javax.management.openmbean.OpenType; import javax.management.openmbean.SimpleType; import javax.management.openmbean.TabularDataSupport; import javax.management.openmbean.TabularType; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.entity.EntitySpec; import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.api.sensor.SensorEvent; import org.apache.brooklyn.api.sensor.SensorEventListener; import org.apache.brooklyn.core.entity.AbstractEntity; import org.apache.brooklyn.core.entity.Attributes; import org.apache.brooklyn.core.entity.Entities; import org.apache.brooklyn.core.feed.ConfigToAttributes; import org.apache.brooklyn.core.location.PortRanges; import org.apache.brooklyn.core.location.SimulatedLocation; import org.apache.brooklyn.core.sensor.BasicAttributeSensor; import org.apache.brooklyn.core.sensor.BasicNotificationSensor; import org.apache.brooklyn.core.sensor.Sensors; import org.apache.brooklyn.core.test.entity.TestApplication; import org.apache.brooklyn.core.test.entity.TestApplicationImpl; import org.apache.brooklyn.core.test.entity.TestEntity; import org.apache.brooklyn.core.test.entity.TestEntityImpl; import org.apache.brooklyn.entity.java.JmxSupport; import org.apache.brooklyn.entity.java.UsesJmx; import org.apache.brooklyn.entity.java.UsesJmx.JmxAgentModes; import org.apache.brooklyn.entity.software.base.test.jmx.GeneralisedDynamicMBean; import org.apache.brooklyn.entity.software.base.test.jmx.JmxService; import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation; import org.apache.brooklyn.test.Asserts; import org.apache.brooklyn.test.NetworkingTestUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.testng.collections.Lists; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; /** * Test the operation of the {@link JmxFeed} class. * <p> * Also confirm some of the JMX setup done by {@link JmxSupport} and {@link JmxHelper}, * based on ports in {@link UsesJmx}. * <p> * TODO tests of other JMX_AGENT_MODE are done in ActiveMqIntegrationTest; * would be nice to promote some to live here */ public class JmxFeedTest { // FIXME Move out the JmxHelper tests into the JmxHelperTest class // FIXME Also test that setting poll period takes effect private static final Logger log = LoggerFactory.getLogger(JmxFeedTest.class); private static final int TIMEOUT_MS = 5000; private static final int SHORT_WAIT_MS = 250; private JmxService jmxService; private TestApplication app; private TestEntity entity; private JmxFeed feed; private JmxHelper jmxHelper; private AttributeSensor<Integer> intAttribute = Sensors.newIntegerSensor("brooklyn.test.intAttribute", "Brooklyn testing int attribute"); private AttributeSensor<String> stringAttribute = Sensors.newStringSensor("brooklyn.test.stringAttribute", "Brooklyn testing string attribute"); private BasicAttributeSensor<Map> mapAttribute = new BasicAttributeSensor<Map>(Map.class, "brooklyn.test.mapAttribute", "Brooklyn testing map attribute"); private String objectName = "Brooklyn:type=MyTestMBean,name=myname"; private ObjectName jmxObjectName; private String attributeName = "myattrib"; private String opName = "myop"; public static class TestEntityWithJmx extends TestEntityImpl { @Override public void init() { sensors().set(Attributes.HOSTNAME, "localhost"); sensors().set(UsesJmx.JMX_PORT, LocalhostMachineProvisioningLocation.obtainPort(PortRanges.fromString( // just doing "40123+" was not enough to avoid collisions (on 40125), // observed on jenkins, not sure why but // maybe something else had a UDP connection we weren't detected, // or the static lock our localhost uses was being bypassed; // this should improve things (2016-01) NetworkingTestUtils.randomPortAround(40000)+"+"))); // only supports no-agent, at the moment config().set(UsesJmx.JMX_AGENT_MODE, JmxAgentModes.NONE); sensors().set(UsesJmx.RMI_REGISTRY_PORT, -1); // -1 means to use the JMX_PORT only ConfigToAttributes.apply(this, UsesJmx.JMX_CONTEXT); } } @BeforeMethod(alwaysRun=true) public void setUp() throws Exception { jmxObjectName = new ObjectName(objectName); // Create an entity and configure it with the above JMX service app = TestApplication.Factory.newManagedInstanceForTests(); entity = app.createAndManageChild(EntitySpec.create(TestEntity.class).impl(TestEntityWithJmx.class)); app.start(ImmutableList.of(new SimulatedLocation())); jmxHelper = new JmxHelper(entity); jmxService = new JmxService(entity); } @AfterMethod(alwaysRun=true) public void tearDown() throws Exception { if (feed != null) feed.stop(); if (jmxHelper != null) jmxHelper.disconnect(); if (jmxService != null) jmxService.shutdown(); if (app != null) Entities.destroyAll(app.getManagementContext()); feed = null; } @Test public void testJmxAttributePollerReturnsMBeanAttribute() throws Exception { GeneralisedDynamicMBean mbean = jmxService.registerMBean(ImmutableMap.of(attributeName, 42), objectName); feed = JmxFeed.builder() .entity(entity) .pollAttribute(new JmxAttributePollConfig<Integer>(intAttribute) .objectName(objectName) .period(50) .attributeName(attributeName)) .build(); // Starts with value defined when registering... assertSensorEventually(intAttribute, 42, TIMEOUT_MS); // Change the value and check it updates mbean.updateAttributeValue(attributeName, 64); assertSensorEventually(intAttribute, 64, TIMEOUT_MS); } @Test public void testJmxAttributeOfTypeTabularDataProviderConvertedToMap() throws Exception { // Create the CompositeType and TabularData CompositeType compositeType = new CompositeType( "typeName", "description", new String[] {"myint", "mystring", "mybool"}, // item names new String[] {"myint", "mystring", "mybool"}, // item descriptions, can't be null or empty string new OpenType<?>[] {SimpleType.INTEGER, SimpleType.STRING, SimpleType.BOOLEAN} ); TabularType tt = new TabularType( "typeName", "description", compositeType, new String[] {"myint"} ); TabularDataSupport tds = new TabularDataSupport(tt); tds.put(new CompositeDataSupport( compositeType, new String[] {"mybool", "myint", "mystring"}, new Object[] {true, 1234, "on"} )); // Create MBean GeneralisedDynamicMBean mbean = jmxService.registerMBean(ImmutableMap.of(attributeName, tds), objectName); feed = JmxFeed.builder() .entity(entity) .pollAttribute(new JmxAttributePollConfig<Map>(mapAttribute) .objectName(objectName) .attributeName(attributeName) .onSuccess((Function)JmxValueFunctions.tabularDataToMap())) .build(); // Starts with value defined when registering... assertSensorEventually( mapAttribute, ImmutableMap.of("myint", 1234, "mystring", "on", "mybool", Boolean.TRUE), TIMEOUT_MS); } @Test public void testJmxOperationPolledForSensor() throws Exception { // This is awful syntax... final int opReturnVal = 123; final AtomicInteger invocationCount = new AtomicInteger(); MBeanOperationInfo opInfo = new MBeanOperationInfo(opName, "my descr", new MBeanParameterInfo[0], Integer.class.getName(), MBeanOperationInfo.ACTION); GeneralisedDynamicMBean mbean = jmxService.registerMBean( Collections.emptyMap(), ImmutableMap.of(opInfo, new Function<Object[], Integer>() { public Integer apply(Object[] args) { invocationCount.incrementAndGet(); return opReturnVal; }}), objectName); feed = JmxFeed.builder() .entity(entity) .pollOperation(new JmxOperationPollConfig<Integer>(intAttribute) .objectName(objectName) .operationName(opName)) .build(); Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), new Runnable() { public void run() { assertTrue(invocationCount.get() > 0, "invocationCount="+invocationCount); assertEquals(entity.getAttribute(intAttribute), (Integer)opReturnVal); }}); } @Test public void testJmxOperationWithArgPolledForSensor() throws Exception { // This is awful syntax... MBeanParameterInfo paramInfo = new MBeanParameterInfo("param1", String.class.getName(), "my param1"); MBeanParameterInfo[] paramInfos = new MBeanParameterInfo[] {paramInfo}; MBeanOperationInfo opInfo = new MBeanOperationInfo(opName, "my descr", paramInfos, String.class.getName(), MBeanOperationInfo.ACTION); GeneralisedDynamicMBean mbean = jmxService.registerMBean( Collections.emptyMap(), ImmutableMap.of(opInfo, new Function<Object[], String>() { public String apply(Object[] args) { return args[0]+"suffix"; }}), objectName); feed = JmxFeed.builder() .entity(entity) .pollOperation(new JmxOperationPollConfig<String>(stringAttribute) .objectName(objectName) .operationName(opName) .operationParams(ImmutableList.of("myprefix"))) .build(); assertSensorEventually(stringAttribute, "myprefix"+"suffix", TIMEOUT_MS); } @Test public void testJmxNotificationSubscriptionForSensor() throws Exception { final String one = "notification.one", two = "notification.two"; final StandardEmitterMBean mbean = jmxService.registerMBean(ImmutableList.of(one, two), objectName); final AtomicInteger sequence = new AtomicInteger(0); feed = JmxFeed.builder() .entity(entity) .subscribeToNotification(new JmxNotificationSubscriptionConfig<Integer>(intAttribute) .objectName(objectName) .notificationFilter(JmxNotificationFilters.matchesType(one))) .build(); // Notification updates the sensor // Note that subscription is done async, so can't just send notification immediately during test. Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), new Runnable() { public void run() { sendNotification(mbean, one, sequence.getAndIncrement(), 123); assertEquals(entity.getAttribute(intAttribute), (Integer)123); }}); // But other notification types are ignored sendNotification(mbean, two, sequence.getAndIncrement(), -1); Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), new Runnable() { public void run() { assertEquals(entity.getAttribute(intAttribute), (Integer)123); }}); } @Test public void testJmxNotificationSubscriptionForSensorParsingNotification() throws Exception { final String one = "notification.one", two = "notification.two"; final StandardEmitterMBean mbean = jmxService.registerMBean(ImmutableList.of(one, two), objectName); final AtomicInteger sequence = new AtomicInteger(0); feed = JmxFeed.builder() .entity(entity) .subscribeToNotification(new JmxNotificationSubscriptionConfig<Integer>(intAttribute) .objectName(objectName) .notificationFilter(JmxNotificationFilters.matchesType(one)) .onNotification(new Function<Notification, Integer>() { public Integer apply(Notification notif) { return (Integer) notif.getUserData(); } })) .build(); // Notification updates the sensor // Note that subscription is done async, so can't just send notification immediately during test. Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), new Runnable() { public void run() { sendNotification(mbean, one, sequence.getAndIncrement(), 123); assertEquals(entity.getAttribute(intAttribute), (Integer)123); }}); } @Test public void testJmxNotificationMultipleSubscriptionUsingListener() throws Exception { final String one = "notification.one"; final String two = "notification.two"; final StandardEmitterMBean mbean = jmxService.registerMBean(ImmutableList.of(one, two), objectName); final AtomicInteger sequence = new AtomicInteger(0); feed = JmxFeed.builder() .entity(entity) .subscribeToNotification(new JmxNotificationSubscriptionConfig<Integer>(intAttribute) .objectName(objectName) .notificationFilter(JmxNotificationFilters.matchesTypes(one, two))) .build(); // Notification updates the sensor // Note that subscription is done async, so can't just send notification immediately during test. Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), new Runnable() { public void run() { sendNotification(mbean, one, sequence.getAndIncrement(), 123); assertEquals(entity.getAttribute(intAttribute), (Integer)123); }}); // And wildcard means other notifications also received sendNotification(mbean, two, sequence.getAndIncrement(), 456); assertSensorEventually(intAttribute, 456, TIMEOUT_MS); } // Test reproduces functionality used in Monterey, for Venue entity being told of requestActor @Test public void testSubscribeToJmxNotificationAndEmitCorrespondingNotificationSensor() throws Exception { TestApplication app2 = new TestApplicationImpl(); final EntityWithEmitter entity = new EntityWithEmitter(app2); Entities.startManagement(app2); try { app2.start(ImmutableList.of(new SimulatedLocation())); final List<SensorEvent<String>> received = Lists.newArrayList(); app2.subscriptions().subscribe(null, EntityWithEmitter.MY_NOTIF, new SensorEventListener<String>() { public void onEvent(SensorEvent<String> event) { received.add(event); }}); final StandardEmitterMBean mbean = jmxService.registerMBean(ImmutableList.of("one"), objectName); final AtomicInteger sequence = new AtomicInteger(0); jmxHelper.connect(TIMEOUT_MS); jmxHelper.addNotificationListener(jmxObjectName, new NotificationListener() { public void handleNotification(Notification notif, Object callback) { if (notif.getType().equals("one")) { entity.sensors().emit(EntityWithEmitter.MY_NOTIF, (String) notif.getUserData()); } }}); Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), new Runnable() { public void run() { sendNotification(mbean, "one", sequence.getAndIncrement(), "abc"); assertTrue(received.size() > 0, "received size should be bigger than 0"); assertEquals(received.get(0).getValue(), "abc"); }}); } finally { Entities.destroyAll(app2.getManagementContext()); } } public static class EntityWithEmitter extends AbstractEntity { public static final BasicNotificationSensor<String> MY_NOTIF = new BasicNotificationSensor<String>(String.class, "test.myNotif", "My notif"); public EntityWithEmitter(Entity owner) { super(owner); } public EntityWithEmitter(Map flags) { super(flags); } public EntityWithEmitter(Map flags, Entity owner) { super(flags, owner); } } private Notification sendNotification(StandardEmitterMBean mbean, String type, long seq, Object userData) { Notification notif = new Notification(type, mbean, seq); notif.setUserData(userData); mbean.sendNotification(notif); return notif; } private <T> void assertSensorEventually(final AttributeSensor<T> sensor, final T expectedVal, long timeout) { Asserts.succeedsEventually(ImmutableMap.of("timeout", timeout), new Callable<Void>() { public Void call() { assertEquals(entity.getAttribute(sensor), expectedVal); return null; }}); } }