/** * 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.ambari.server.actionmanager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import java.sql.SQLException; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.Semaphore; import javax.persistence.EntityManager; import org.apache.ambari.server.AmbariException; import org.apache.ambari.server.H2DatabaseCleaner; import org.apache.ambari.server.events.publishers.JPAEventPublisher; import org.apache.ambari.server.orm.GuiceJpaInitializer; import org.apache.ambari.server.orm.InMemoryDefaultTestModule; import org.apache.ambari.server.orm.OrmTestHelper; import org.apache.ambari.server.state.Cluster; import org.apache.ambari.server.state.Clusters; import org.apache.ambari.server.state.Config; import org.apache.ambari.server.state.ConfigFactory; import org.apache.ambari.server.state.ConfigHelper; import org.apache.ambari.server.state.DesiredConfig; import org.apache.ambari.server.state.StackId; import org.junit.After; import org.junit.Before; import org.junit.Test; import com.google.common.collect.Sets; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.persist.UnitOfWork; import junit.framework.Assert; /** * Tests {@link ActionScheduler}, focusing on multi-threaded concerns. */ public class TestActionSchedulerThreading { private static Injector injector; private Clusters clusters; private OrmTestHelper ormTestHelper; /** * Setup test methods. * * @throws Exception */ @Before public void setup() throws Exception { injector = Guice.createInjector(new InMemoryDefaultTestModule()); injector.getInstance(GuiceJpaInitializer.class); clusters = injector.getInstance(Clusters.class); ormTestHelper = injector.getInstance(OrmTestHelper.class); } /** * Cleanup test methods. */ @After public void tearDown() throws AmbariException, SQLException { H2DatabaseCleaner.clearDatabaseAndStopPersistenceService(injector); } /** * Tests that applying configurations for a given stack correctly sets * {@link DesiredConfig}s. This takes into account EclipseLink caching issues * by specifically checking the actionscheduler headless thread. */ @Test public void testDesiredConfigurationsAfterApplyingLatestForStackInOtherThreads() throws Exception { long clusterId = ormTestHelper.createCluster(UUID.randomUUID().toString()); Cluster cluster = clusters.getCluster(clusterId); ormTestHelper.addHost(clusters, cluster, "h1"); StackId stackId = cluster.getCurrentStackVersion(); StackId newStackId = new StackId("HDP-2.2.0"); // make sure the stacks are different Assert.assertFalse(stackId.equals(newStackId)); Map<String, String> properties = new HashMap<>(); Map<String, Map<String, String>> propertiesAttributes = new HashMap<>(); ConfigFactory configFactory = injector.getInstance(ConfigFactory.class); // foo-type for v1 on current stack properties.put("foo-property-1", "foo-value-1"); Config c1 = configFactory.createNew(cluster, "foo-type", "version-1", properties, propertiesAttributes); // make v1 "current" cluster.addDesiredConfig("admin", Sets.newHashSet(c1), "note-1"); // bump the stack cluster.setDesiredStackVersion(newStackId); // save v2 // foo-type for v2 on new stack properties.put("foo-property-2", "foo-value-2"); Config c2 = configFactory.createNew(cluster, "foo-type", "version-2", properties, propertiesAttributes); // make v2 "current" cluster.addDesiredConfig("admin", Sets.newHashSet(c2), "note-2"); // check desired config Map<String, DesiredConfig> desiredConfigs = cluster.getDesiredConfigs(); DesiredConfig desiredConfig = desiredConfigs.get("foo-type"); desiredConfig = desiredConfigs.get("foo-type"); assertNotNull(desiredConfig); assertEquals(Long.valueOf(2), desiredConfig.getVersion()); assertEquals("version-2", desiredConfig.getTag()); final String hostName = cluster.getHosts().iterator().next().getHostName(); // move the stack back to the old stack cluster.setDesiredStackVersion(stackId); // create the semaphores, taking 1 from each to make them blocking from the // start Semaphore applyLatestConfigsSemaphore = new Semaphore(1, true); Semaphore threadInitialCachingSemaphore = new Semaphore(1, true); threadInitialCachingSemaphore.acquire(); applyLatestConfigsSemaphore.acquire(); final InstrumentedActionScheduler runnable = new InstrumentedActionScheduler(clusterId, hostName, threadInitialCachingSemaphore, applyLatestConfigsSemaphore); injector.injectMembers(runnable); final Thread thread = new Thread(runnable); // start the thread to populate the data in it, waiting to ensure that it // finishes the calls it needs to make thread.start(); threadInitialCachingSemaphore.acquire(); // apply the configs for the old stack cluster.applyLatestConfigurations(stackId); // wake the thread up and have it verify that it can see the updated configs applyLatestConfigsSemaphore.release(); // wait for the thread to finish thread.join(); // if the thread failed, then fail this test runnable.validateAssertions(); } /** * Checks the desired configurations of a cluster in a separate thread in * order to test whether the {@link EntityManager} has evicted stale entries. */ private final static class InstrumentedActionScheduler extends ActionScheduler { private final long clusterId; private final Semaphore threadInitialCachingSemaphore; private final Semaphore applyLatestConfigsSemaphore; private final String hostName; private Throwable throwable = null; @Inject private ConfigHelper configHelper; @Inject private Clusters clusters; @Inject private UnitOfWork unitOfWork; /** * Constructor. * * @param clusterId * @param hostName * @param threadInitialCachingSemaphore * @param applyLatestConfigsSemaphore */ private InstrumentedActionScheduler(long clusterId, String hostName, Semaphore threadInitialCachingSemaphore, Semaphore applyLatestConfigsSemaphore) { super(1000, 1000, injector.getInstance(ActionDBAccessor.class), injector.getInstance(JPAEventPublisher.class)); this.clusterId = clusterId; this.threadInitialCachingSemaphore = threadInitialCachingSemaphore; this.applyLatestConfigsSemaphore = applyLatestConfigsSemaphore; this.hostName = hostName; } /** * */ @Override public void run() { unitOfWork.begin(); try { // this is an important line - it replicates what the real scheduler // does during its work routine by grabbing the current thread's // EntityManager so the event can property evict entries threadEntityManager = entityManagerProvider.get(); // first get the configs in order to cache the entities in this thread's // L1 cache Cluster cluster = clusters.getCluster(clusterId); // {foo-type={tag=version-2}} Map<String, Map<String, String>> effectiveDesiredTags = configHelper.getEffectiveDesiredTags( cluster, hostName); assertEquals("version-2", effectiveDesiredTags.get("foo-type").get("tag")); // signal the caller that we're done making our initial call to populate // the EntityManager threadInitialCachingSemaphore.release(); // wait for the method to switch configs applyLatestConfigsSemaphore.acquire(); // {foo-type={tag=version-1}} effectiveDesiredTags = configHelper.getEffectiveDesiredTags(cluster, hostName); assertEquals("version-1", effectiveDesiredTags.get("foo-type").get("tag")); } catch (Throwable throwable) { this.throwable = throwable; } finally { applyLatestConfigsSemaphore.release(); unitOfWork.end(); } } /** * Raise an assertion if the thread failed. */ private void validateAssertions() { if (null != throwable) { throw new AssertionError(throwable); } } } }