Browse Source

AMBARI-10639 - Configuration Versions Should Be Calculated By the Database (jonathanhurley)

Jonathan Hurley 10 years ago
parent
commit
511b744161

+ 21 - 1
ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ClusterDAO.java

@@ -27,13 +27,13 @@ import javax.persistence.criteria.CriteriaBuilder;
 import javax.persistence.criteria.CriteriaQuery;
 import javax.persistence.criteria.Root;
 
-import com.google.inject.Singleton;
 import org.apache.ambari.server.orm.RequiresSession;
 import org.apache.ambari.server.orm.entities.ClusterConfigEntity;
 import org.apache.ambari.server.orm.entities.ClusterEntity;
 
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import com.google.inject.persist.Transactional;
 
 @Singleton
@@ -121,6 +121,26 @@ public class ClusterDAO {
     return daoUtils.selectOne(query);
   }
 
+  /**
+   * Gets the next version that will be created for a given
+   * {@link ClusterConfigEntity}.
+   *
+   * @param clusterId
+   *          the cluster that the service is a part of.
+   * @param configType
+   *          the name of the configuration type (not {@code null}).
+   * @return the highest existing value of the version column + 1
+   */
+  public Long findNextConfigVersion(long clusterId, String configType) {
+    TypedQuery<Long> query = entityManagerProvider.get().createNamedQuery(
+        "ClusterConfigEntity.findNextConfigVersion", Long.class);
+
+    query.setParameter("clusterId", clusterId);
+    query.setParameter("configType", configType);
+
+    return daoUtils.selectSingle(query);
+  }
+
   /**
    * Create Cluster entity in Database
    * @param clusterEntity entity to create

+ 36 - 32
ambari-server/src/main/java/org/apache/ambari/server/orm/dao/ServiceConfigDAO.java

@@ -18,12 +18,10 @@
 
 package org.apache.ambari.server.orm.dao;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-import com.google.inject.persist.Transactional;
-import org.apache.ambari.server.orm.RequiresSession;
-import org.apache.ambari.server.orm.entities.ServiceConfigEntity;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 
 import javax.persistence.EntityManager;
 import javax.persistence.Tuple;
@@ -31,21 +29,22 @@ import javax.persistence.TypedQuery;
 import javax.persistence.criteria.CriteriaBuilder;
 import javax.persistence.criteria.CriteriaQuery;
 import javax.persistence.criteria.Root;
-import javax.persistence.criteria.Subquery;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+
+import org.apache.ambari.server.orm.RequiresSession;
+import org.apache.ambari.server.orm.entities.ServiceConfigEntity;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.persist.Transactional;
 
 @Singleton
 public class ServiceConfigDAO {
   @Inject
-  Provider<EntityManager> entityManagerProvider;
-  @Inject
-  DaoUtils daoUtils;
+  private Provider<EntityManager> entityManagerProvider;
 
+  @Inject
+  private DaoUtils daoUtils;
 
   @RequiresSession
   public ServiceConfigEntity find(Long serviceConfigId) {
@@ -60,20 +59,6 @@ public class ServiceConfigDAO {
     return daoUtils.selectOne(query, serviceName, version);
   }
 
-  @RequiresSession
-  public Map<String, Long> findMaxVersions(Long clusterId) {
-    Map<String, Long> maxVersions = new HashMap<String, Long>();
-
-    TypedQuery<String> query = entityManagerProvider.get().createQuery("SELECT DISTINCT scv.serviceName FROM ServiceConfigEntity scv WHERE scv.clusterId = ?1", String.class);
-    List<String> serviceNames = daoUtils.selectList(query, clusterId);
-
-    for (String serviceName : serviceNames) {
-      maxVersions.put(serviceName, findMaxVersion(clusterId, serviceName).getVersion());
-    }
-
-    return maxVersions;
-  }
-
   @RequiresSession
   public List<ServiceConfigEntity> getLastServiceConfigVersionsForGroups(Collection<Long> configGroupIds) {
     if (configGroupIds == null || configGroupIds.isEmpty()) {
@@ -156,6 +141,27 @@ public class ServiceConfigDAO {
     return daoUtils.selectList(query, clusterId);
   }
 
+  /**
+   * Gets the next version that will be created when persisting a new
+   * {@link ServiceConfigEntity}.
+   *
+   * @param clusterId
+   *          the cluster that the service is a part of.
+   * @param serviceName
+   *          the name of the service (not {@code null}).
+   * @return the maximum version value + 1
+   */
+  @RequiresSession
+  public Long findNextServiceConfigVersion(long clusterId, String serviceName) {
+    TypedQuery<Long> query = entityManagerProvider.get().createNamedQuery(
+        "ServiceConfigEntity.findNextServiceConfigVersion", Long.class);
+
+    query.setParameter("clusterId", clusterId);
+    query.setParameter("serviceName", serviceName);
+
+    return daoUtils.selectSingle(query);
+  }
+
   @Transactional
   public void create(ServiceConfigEntity serviceConfigEntity) {
     entityManagerProvider.get().persist(serviceConfigEntity);
@@ -170,6 +176,4 @@ public class ServiceConfigDAO {
   public void remove(ServiceConfigEntity serviceConfigEntity) {
     entityManagerProvider.get().remove(merge(serviceConfigEntity));
   }
-
-
 }

+ 3 - 0
ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ClusterConfigEntity.java

@@ -31,6 +31,8 @@ import javax.persistence.JoinColumn;
 import javax.persistence.Lob;
 import javax.persistence.ManyToMany;
 import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
 import javax.persistence.OneToMany;
 import javax.persistence.OneToOne;
 import javax.persistence.Table;
@@ -47,6 +49,7 @@ import javax.persistence.UniqueConstraint;
   , initialValue = 1
   , allocationSize = 1
 )
+@NamedQueries({ @NamedQuery(name = "ClusterConfigEntity.findNextConfigVersion", query = "SELECT COALESCE(MAX(clusterConfig.version),0) + 1 as nextVersion FROM ClusterConfigEntity clusterConfig WHERE clusterConfig.type=:configType AND clusterConfig.clusterId=:clusterId") })
 public class ClusterConfigEntity {
 
   @Id

+ 3 - 0
ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ServiceConfigEntity.java

@@ -32,6 +32,8 @@ import javax.persistence.JoinColumn;
 import javax.persistence.JoinTable;
 import javax.persistence.ManyToMany;
 import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
 import javax.persistence.OneToOne;
 import javax.persistence.Table;
 import javax.persistence.TableGenerator;
@@ -44,6 +46,7 @@ import javax.persistence.TableGenerator;
   , initialValue = 1
   , allocationSize = 1
 )
+@NamedQueries({ @NamedQuery(name = "ServiceConfigEntity.findNextServiceConfigVersion", query = "SELECT COALESCE(MAX(serviceConfig.version), 0) + 1 AS nextVersion FROM ServiceConfigEntity serviceConfig WHERE serviceConfig.serviceName=:serviceName AND serviceConfig.clusterId=:clusterId") })
 public class ServiceConfigEntity {
   @Id
   @Column(name = "service_config_id")

+ 3 - 1
ambari-server/src/main/java/org/apache/ambari/server/serveraction/upgrades/ConfigureAction.java

@@ -40,7 +40,9 @@ import org.apache.ambari.server.state.stack.upgrade.ConfigureTask;
 import com.google.inject.Inject;
 
 /**
- * Action that represents a manual stage.
+ * The {@link ConfigureAction} is used to alter a configuration property during
+ * an upgrade. It will only produce a new configuration if the value being
+ * changed is different than the existing value.
  */
 public class ConfigureAction extends AbstractServerAction {
 

+ 0 - 52
ambari-server/src/main/java/org/apache/ambari/server/state/ConfigVersionHelper.java

@@ -1,52 +0,0 @@
-/*
- * 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.state;
-
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.atomic.AtomicLong;
-
-/**
- * Utility class to manage config versions for cluster
- */
-public class ConfigVersionHelper {
-
-  ConcurrentMap<String, AtomicLong> versionCounters = new ConcurrentHashMap<String, AtomicLong>();
-
-  public ConfigVersionHelper(Map<String, Long> configTypeLastVersions) {
-    for (Map.Entry<String, Long> entry : configTypeLastVersions.entrySet()) {
-      String type = entry.getKey();
-      Long version = entry.getValue();
-      versionCounters.put(type, new AtomicLong(version));
-    }
-  }
-
-  public long getNextVersion(String key) {
-    AtomicLong version = versionCounters.get(key);
-    if (version == null) {
-      version = new AtomicLong();
-      AtomicLong tmp = versionCounters.putIfAbsent(key, version);
-      if (tmp != null) {
-        version = tmp;
-      }
-    }
-    return version.incrementAndGet();
-  }
-}

+ 44 - 39
ambari-server/src/main/java/org/apache/ambari/server/state/cluster/ClusterImpl.java

@@ -91,7 +91,6 @@ import org.apache.ambari.server.state.ComponentInfo;
 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.ConfigVersionHelper;
 import org.apache.ambari.server.state.DesiredConfig;
 import org.apache.ambari.server.state.Host;
 import org.apache.ambari.server.state.HostHealthStatus;
@@ -180,36 +179,48 @@ public class ClusterImpl implements Cluster {
 
   private ClusterEntity clusterEntity;
 
-  private final ConfigVersionHelper configVersionHelper;
-
   @Inject
   private ClusterDAO clusterDAO;
+
   @Inject
   private ClusterStateDAO clusterStateDAO;
+
   @Inject
   private ClusterVersionDAO clusterVersionDAO;
+
   @Inject
   private HostDAO hostDAO;
+
   @Inject
   private HostVersionDAO hostVersionDAO;
+
   @Inject
   private ServiceFactory serviceFactory;
+
   @Inject
   private ConfigFactory configFactory;
+
   @Inject
   private HostConfigMappingDAO hostConfigMappingDAO;
+
   @Inject
   private ConfigGroupFactory configGroupFactory;
+
   @Inject
   private ConfigGroupHostMappingDAO configGroupHostMappingDAO;
+
   @Inject
   private RequestExecutionFactory requestExecutionFactory;
+
   @Inject
   private ConfigHelper configHelper;
+
   @Inject
   private MaintenanceStateHelper maintenanceStateHelper;
+
   @Inject
   private AmbariMetaInfo ambariMetaInfo;
+
   @Inject
   private ServiceConfigDAO serviceConfigDAO;
 
@@ -272,8 +283,6 @@ public class ClusterImpl implements Cluster {
       StringUtils.isEmpty(desiredStackVersion.getStackVersion())) {
       loadServiceConfigTypes();
     }
-
-    configVersionHelper = new ConfigVersionHelper(getConfigLastVersions());
   }
 
 
@@ -1926,18 +1935,13 @@ public class ClusterImpl implements Cluster {
 
 
   @Override
-  public ServiceConfigVersionResponse createServiceConfigVersion(String serviceName, String user, String note,
-                                                                 ConfigGroup configGroup) {
+  public ServiceConfigVersionResponse createServiceConfigVersion(
+      String serviceName, String user, String note, ConfigGroup configGroup) {
 
-    //create next service config version
+    // create next service config version
     ServiceConfigEntity serviceConfigEntity = new ServiceConfigEntity();
-    serviceConfigEntity.setServiceName(serviceName);
-    serviceConfigEntity.setClusterEntity(clusterEntity);
-    serviceConfigEntity.setVersion(configVersionHelper.getNextVersion(serviceName));
-    serviceConfigEntity.setUser(user);
-    serviceConfigEntity.setNote(note);
-    serviceConfigEntity.setStack(clusterEntity.getDesiredStack());
 
+    // set config group
     if (configGroup != null) {
       serviceConfigEntity.setGroupId(configGroup.getId());
       Collection<Config> configs = configGroup.getConfigurations().values();
@@ -1945,8 +1949,8 @@ public class ClusterImpl implements Cluster {
       for (Config config : configs) {
         configEntities.add(clusterDAO.findConfig(getClusterId(), config.getType(), config.getTag()));
       }
-      serviceConfigEntity.setClusterConfigEntities(configEntities);
 
+      serviceConfigEntity.setClusterConfigEntities(configEntities);
       serviceConfigEntity.setHostNames(new ArrayList<String>(configGroup.getHosts().keySet()));
 
     } else {
@@ -1954,7 +1958,23 @@ public class ClusterImpl implements Cluster {
       serviceConfigEntity.setClusterConfigEntities(configEntities);
     }
 
-    serviceConfigDAO.create(serviceConfigEntity);
+    clusterGlobalLock.writeLock().lock();
+
+    try {
+      long nextServiceConfigVersion = serviceConfigDAO.findNextServiceConfigVersion(
+          clusterEntity.getClusterId(), serviceName);
+
+      serviceConfigEntity.setServiceName(serviceName);
+      serviceConfigEntity.setClusterEntity(clusterEntity);
+      serviceConfigEntity.setVersion(nextServiceConfigVersion);
+      serviceConfigEntity.setUser(user);
+      serviceConfigEntity.setNote(note);
+      serviceConfigEntity.setStack(clusterEntity.getDesiredStack());
+
+      serviceConfigDAO.create(serviceConfigEntity);
+    } finally {
+      clusterGlobalLock.writeLock().unlock();
+    }
 
     configChangeLog.info("Cluster '{}' changed by: '{}'; service_name='{}' config_group='{}' config_group_id='{}' " +
       "version='{}'", getClusterName(), user, serviceName,
@@ -2193,6 +2213,9 @@ public class ClusterImpl implements Cluster {
       }
     }
 
+    long nextServiceConfigVersion = serviceConfigDAO.findNextServiceConfigVersion(
+        clusterEntity.getClusterId(), serviceName);
+
     ServiceConfigEntity serviceConfigEntityClone = new ServiceConfigEntity();
     serviceConfigEntityClone.setCreateTimestamp(System.currentTimeMillis());
     serviceConfigEntityClone.setUser(user);
@@ -2204,7 +2227,7 @@ public class ClusterImpl implements Cluster {
     serviceConfigEntityClone.setHostNames(serviceConfigEntity.getHostNames());
     serviceConfigEntityClone.setGroupId(serviceConfigEntity.getGroupId());
     serviceConfigEntityClone.setNote(serviceConfigVersionNote);
-    serviceConfigEntityClone.setVersion(configVersionHelper.getNextVersion(serviceName));
+    serviceConfigEntityClone.setVersion(nextServiceConfigVersion);
 
     serviceConfigDAO.create(serviceConfigEntityClone);
 
@@ -2356,30 +2379,12 @@ public class ClusterImpl implements Cluster {
     return getHostsDesiredConfigs(hostnames);
   }
 
+  /**
+   * {@inheritDoc}
+   */
   @Override
   public Long getNextConfigVersion(String type) {
-    return configVersionHelper.getNextVersion(type);
-  }
-
-  private Map<String, Long> getConfigLastVersions() {
-    Map<String, Long> maxVersions = new HashMap<String, Long>();
-    //config versions
-    for (Entry<String, Map<String, Config>> mapEntry : allConfigs.entrySet()) {
-      String type = mapEntry.getKey();
-      Long lastVersion = 0L;
-      for (Entry<String, Config> configEntry : mapEntry.getValue().entrySet()) {
-        Long version = configEntry.getValue().getVersion();
-        if (version > lastVersion) {
-          lastVersion = version;
-        }
-      }
-      maxVersions.put(type, lastVersion);
-    }
-
-    //service config versions
-    maxVersions.putAll(serviceConfigDAO.findMaxVersions(getClusterId()));
-
-    return maxVersions;
+    return clusterDAO.findNextConfigVersion(clusterEntity.getClusterId(), type);
   }
 
   @Transactional

+ 6 - 9
ambari-server/src/test/java/org/apache/ambari/server/orm/dao/ServiceConfigDAOTest.java

@@ -18,7 +18,6 @@
 package org.apache.ambari.server.orm.dao;
 
 import java.util.List;
-import java.util.Map;
 
 import junit.framework.Assert;
 
@@ -174,16 +173,14 @@ public class ServiceConfigDAOTest {
     createServiceConfig("HDFS", "admin", 2L, 2L, 2222L, null);
     createServiceConfig("YARN", "admin", 1L, 3L, 3333L, null);
 
+    long hdfsVersion = serviceConfigDAO.findNextServiceConfigVersion(
+        clusterDAO.findByName("c1").getClusterId(), "HDFS");
 
-    Map<String,Long> maxVersions = serviceConfigDAO.findMaxVersions(
-      clusterDAO.findByName("c1").getClusterId());
+    long yarnVersion = serviceConfigDAO.findNextServiceConfigVersion(
+        clusterDAO.findByName("c1").getClusterId(), "YARN");
 
-    Assert.assertNotNull(maxVersions);
-
-    Assert.assertEquals(2, maxVersions.size());
-
-    Assert.assertEquals(Long.valueOf(2), maxVersions.get("HDFS"));
-    Assert.assertEquals(Long.valueOf(1), maxVersions.get("YARN"));
+    Assert.assertEquals(3, hdfsVersion);
+    Assert.assertEquals(2, yarnVersion);
   }
 
   @Test

+ 265 - 0
ambari-server/src/test/java/org/apache/ambari/server/state/cluster/ConcurrentServiceConfigVersionTest.java

@@ -0,0 +1,265 @@
+/**
+* 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.state.cluster;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.ServiceComponentNotFoundException;
+import org.apache.ambari.server.ServiceNotFoundException;
+import org.apache.ambari.server.controller.ServiceConfigVersionResponse;
+import org.apache.ambari.server.events.listeners.upgrade.HostVersionOutOfSyncListener;
+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.orm.dao.ServiceConfigDAO;
+import org.apache.ambari.server.state.Cluster;
+import org.apache.ambari.server.state.Clusters;
+import org.apache.ambari.server.state.Host;
+import org.apache.ambari.server.state.RepositoryVersionState;
+import org.apache.ambari.server.state.Service;
+import org.apache.ambari.server.state.ServiceComponent;
+import org.apache.ambari.server.state.ServiceComponentFactory;
+import org.apache.ambari.server.state.ServiceComponentHost;
+import org.apache.ambari.server.state.ServiceComponentHostFactory;
+import org.apache.ambari.server.state.ServiceFactory;
+import org.apache.ambari.server.state.StackId;
+import org.apache.ambari.server.state.State;
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.inject.Binder;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.persist.PersistService;
+import com.google.inject.util.Modules;
+
+/**
+ * Tests that concurrent threads attempting to create configurations don't cause
+ * unique violations with the configuration version.
+ */
+public class ConcurrentServiceConfigVersionTest {
+  private static final int NUMBER_OF_SERVICE_CONFIG_VERSIONS = 10;
+  private static final int NUMBER_OF_THREADS = 2;
+
+  @Inject
+  private Injector injector;
+
+  @Inject
+  private Clusters clusters;
+
+  @Inject
+  private ServiceFactory serviceFactory;
+
+  @Inject
+  private ServiceComponentFactory serviceComponentFactory;
+
+  @Inject
+  private ServiceComponentHostFactory serviceComponentHostFactory;
+
+  @Inject
+  private OrmTestHelper helper;
+
+  @Inject
+  private ServiceConfigDAO serviceConfigDAO;
+
+  private StackId stackId = new StackId("HDP-0.1");
+
+  /**
+   * The cluster.
+   */
+  private Cluster cluster;
+
+  /**
+   * Creates a cluster and installs HDFS with NN and DN.
+   *
+   * @throws Exception
+   */
+  @Before
+  public void setup() throws Exception {
+    injector = Guice.createInjector(Modules.override(
+        new InMemoryDefaultTestModule()).with(new MockModule()));
+
+    injector.getInstance(GuiceJpaInitializer.class);
+    injector.injectMembers(this);
+    clusters.addCluster("c1", stackId);
+    cluster = clusters.getCluster("c1");
+    helper.getOrCreateRepositoryVersion(stackId, stackId.getStackVersion());
+    cluster.createClusterVersion(stackId,
+        stackId.getStackVersion(), "admin", RepositoryVersionState.UPGRADING);
+
+    String hostName = "c6401.ambari.apache.org";
+    clusters.addHost(hostName);
+    setOsFamily(clusters.getHost(hostName), "redhat", "6.4");
+    clusters.getHost(hostName).persist();
+    clusters.mapHostToCluster(hostName, "c1");
+
+    Service service = installService("HDFS");
+    addServiceComponent(service, "NAMENODE");
+    addServiceComponent(service, "DATANODE");
+
+    createNewServiceComponentHost("HDFS", "NAMENODE", hostName);
+    createNewServiceComponentHost("HDFS", "DATANODE", hostName);
+  }
+
+  @After
+  public void teardown() {
+    injector.getInstance(PersistService.class).stop();
+  }
+
+  /**
+   * Tests that creating service config versions from multiple threads doesn't
+   * violate unique constraints.
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testConcurrentServiceConfigVersions() throws Exception {
+    long nextVersion = serviceConfigDAO.findNextServiceConfigVersion(
+        cluster.getClusterId(), "HDFS");
+
+    Assert.assertEquals(nextVersion, 1);
+
+    List<Thread> threads = new ArrayList<Thread>();
+    for (int i = 0; i < NUMBER_OF_THREADS; i++) {
+      Thread thread = new ConcurrentServiceConfigThread(cluster);
+      threads.add(thread);
+
+      thread.start();
+    }
+
+    for (Thread thread : threads) {
+      thread.join();
+    }
+
+    long maxVersion = NUMBER_OF_THREADS * NUMBER_OF_SERVICE_CONFIG_VERSIONS;
+    nextVersion = serviceConfigDAO.findNextServiceConfigVersion(
+        cluster.getClusterId(), "HDFS");
+
+    Assert.assertEquals(maxVersion + 1, nextVersion);
+  }
+
+  private final static class ConcurrentServiceConfigThread extends Thread {
+
+    private Cluster cluster = null;
+
+    private ConcurrentServiceConfigThread(Cluster cluster) {
+      this.cluster = cluster;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void run() {
+      try {
+        for (int i = 0; i < NUMBER_OF_SERVICE_CONFIG_VERSIONS; i++) {
+          ServiceConfigVersionResponse response = cluster.createServiceConfigVersion(
+              "HDFS", null, getName() + "-serviceConfig" + i, null);
+
+          System.out.println("**** " + response.getVersion());
+
+          Thread.sleep(100);
+        }
+      } catch (Exception exception) {
+        throw new RuntimeException(exception);
+      }
+    }
+  }
+
+  private void setOsFamily(Host host, String osFamily, String osVersion) {
+    Map<String, String> hostAttributes = new HashMap<String, String>(2);
+    hostAttributes.put("os_family", osFamily);
+    hostAttributes.put("os_release_version", osVersion);
+    host.setHostAttributes(hostAttributes);
+  }
+
+  private ServiceComponentHost createNewServiceComponentHost(String svc,
+      String svcComponent, String hostName) throws AmbariException {
+    Assert.assertNotNull(cluster.getConfigGroups());
+    Service s = installService(svc);
+    ServiceComponent sc = addServiceComponent(s, svcComponent);
+
+    ServiceComponentHost sch = serviceComponentHostFactory.createNew(sc,
+        hostName);
+
+    sc.addServiceComponentHost(sch);
+    sch.setDesiredState(State.INSTALLED);
+    sch.setState(State.INSTALLED);
+    sch.setDesiredStackVersion(stackId);
+    sch.setStackVersion(stackId);
+
+    sch.persist();
+    return sch;
+  }
+
+  private Service installService(String serviceName) throws AmbariException {
+    Service service = null;
+
+    try {
+      service = cluster.getService(serviceName);
+    } catch (ServiceNotFoundException e) {
+      service = serviceFactory.createNew(cluster, serviceName);
+      cluster.addService(service);
+      service.persist();
+    }
+
+    return service;
+  }
+
+  private ServiceComponent addServiceComponent(Service service,
+      String componentName) throws AmbariException {
+    ServiceComponent serviceComponent = null;
+    try {
+      serviceComponent = service.getServiceComponent(componentName);
+    } catch (ServiceComponentNotFoundException e) {
+      serviceComponent = serviceComponentFactory.createNew(service,
+          componentName);
+      service.addServiceComponent(serviceComponent);
+      serviceComponent.setDesiredState(State.INSTALLED);
+      serviceComponent.persist();
+    }
+
+    return serviceComponent;
+  }
+
+  /**
+  *
+  */
+  private class MockModule implements Module {
+    /**
+    *
+    */
+    @Override
+    public void configure(Binder binder) {
+      // this listener gets in the way of actually testing the concurrency
+      // between the threads; it slows them down too much, so mock it out
+      binder.bind(HostVersionOutOfSyncListener.class).toInstance(
+          EasyMock.createNiceMock(HostVersionOutOfSyncListener.class));
+    }
+  }
+}