{"action":"created","comment":{"url":"https://api.github.com/repos/codice/ddf/pulls/comments/219878955","pull_request_review_id":158132911,"id":219878955,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIxOTg3ODk1NQ==","diff_hunk":"@@ -0,0 +1,566 @@\n+/**\n+ * Copyright (c) Codice Foundation\n+ *\n+ * <p>This is free software: you can redistribute it and/or modify it under the terms of the GNU\n+ * Lesser General Public License as published by the Free Software Foundation, either version 3 of\n+ * the License, or any later version.\n+ *\n+ * <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n+ * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public\n+ * License is distributed along with this program and can be found at\n+ * <http://www.gnu.org/licenses/lgpl.html>.\n+ */\n+package org.codice.ddf.sync.installer.impl;\n+\n+import static org.osgi.framework.Bundle.ACTIVE;\n+\n+import com.google.common.annotations.VisibleForTesting;\n+import com.google.common.collect.ImmutableMap;\n+import java.io.IOException;\n+import java.security.AccessController;\n+import java.security.PrivilegedExceptionAction;\n+import java.util.Arrays;\n+import java.util.Collection;\n+import java.util.Dictionary;\n+import java.util.EnumSet;\n+import java.util.Enumeration;\n+import java.util.HashSet;\n+import java.util.Hashtable;\n+import java.util.List;\n+import java.util.Map;\n+import java.util.Set;\n+import java.util.concurrent.Callable;\n+import java.util.concurrent.TimeUnit;\n+import java.util.function.BiConsumer;\n+import java.util.stream.Collectors;\n+import java.util.stream.Stream;\n+import org.apache.karaf.bundle.core.BundleInfo;\n+import org.apache.karaf.bundle.core.BundleService;\n+import org.apache.karaf.bundle.core.BundleState;\n+import org.apache.karaf.features.BootFinished;\n+import org.apache.karaf.features.Feature;\n+import org.apache.karaf.features.FeaturesService;\n+import org.codice.ddf.sync.installer.api.SynchronisedInstaller;\n+import org.codice.ddf.sync.installer.api.SynchronisedInstallerException;\n+import org.osgi.framework.Bundle;\n+import org.osgi.framework.BundleContext;\n+import org.osgi.framework.BundleException;\n+import org.osgi.framework.Constants;\n+import org.osgi.framework.Filter;\n+import org.osgi.framework.ServiceRegistration;\n+import org.osgi.service.cm.Configuration;\n+import org.osgi.service.cm.ConfigurationAdmin;\n+import org.osgi.service.cm.ConfigurationEvent;\n+import org.osgi.service.cm.ConfigurationListener;\n+import org.osgi.util.tracker.ServiceTracker;\n+import org.slf4j.Logger;\n+import org.slf4j.LoggerFactory;\n+\n+public class SynchronisedInstallerImpl implements SynchronisedInstaller {\n+\n+  private static final Map<Integer, String> BUNDLE_STATES =\n+      new ImmutableMap.Builder<Integer, String>()\n+          .put(Bundle.UNINSTALLED, \"UNINSTALLED\")\n+          .put(Bundle.INSTALLED, \"INSTALLED\")\n+          .put(Bundle.RESOLVED, \"RESOLVED\")\n+          .put(Bundle.STARTING, \"STARTING\")\n+          .put(Bundle.STOPPING, \"STOPPING\")\n+          .put(ACTIVE, \"ACTIVE\")\n+          .build();\n+\n+  private static final String SERVICE_PID_FILTER = \"(\" + Constants.SERVICE_PID + \"=%s)\";\n+\n+  private static final long DEFAULT_MAX_FEATURE_WAIT = TimeUnit.MINUTES.toMillis(10);\n+\n+  private static final long DEFAULT_MAX_SERVICE_WAIT = TimeUnit.MINUTES.toMillis(10);\n+\n+  private static final long DEFAULT_MAX_BUNDLE_WAIT = TimeUnit.MINUTES.toMillis(10);\n+\n+  private static final long DEFAULT_POLLING_INTERVAL = TimeUnit.SECONDS.toMillis(3);\n+\n+  private static final Logger LOGGER = LoggerFactory.getLogger(SynchronisedInstallerImpl.class);\n+\n+  private BundleContext bundleContext;\n+\n+  private ConfigurationAdmin configAdmin;\n+\n+  private FeaturesService featuresService;\n+\n+  private BundleService bundleService;\n+\n+  public SynchronisedInstallerImpl(\n+      BundleContext bundleContext,\n+      ConfigurationAdmin configAdmin,\n+      FeaturesService featuresService,\n+      BundleService bundleService) {\n+    this.bundleContext = bundleContext;\n+    this.configAdmin = configAdmin;\n+    this.featuresService = featuresService;\n+    this.bundleService = bundleService;\n+  }\n+\n+  @Override\n+  public void waitForBootFinish() throws SynchronisedInstallerException {\n+    waitForBootFinish(DEFAULT_MAX_FEATURE_WAIT);\n+  }\n+\n+  @Override\n+  public void waitForBootFinish(long maxWaitTime) throws SynchronisedInstallerException {\n+    Callable<Boolean> isBootFinishedAvailable =\n+        () -> {\n+          LOGGER.debug(\"Waiting for BootFinished service to appear.\");\n+          return bundleContext.getServiceReference(BootFinished.class) != null;\n+        };\n+\n+    wait(\n+        isBootFinishedAvailable,\n+        maxWaitTime,\n+        DEFAULT_POLLING_INTERVAL,\n+        String.format(\"BootFinished service did not appear within [%s] ms\", maxWaitTime));\n+    waitForBundles();\n+  }\n+\n+  @Override\n+  public Configuration createManagedFactoryService(\n+      String factoryPid, Map<String, Object> properties, String bundleLocation)\n+      throws SynchronisedInstallerException {\n+    return createManagedFactoryService(\n+        DEFAULT_MAX_SERVICE_WAIT, factoryPid, properties, bundleLocation);\n+  }\n+\n+  @Override\n+  public Configuration createManagedFactoryService(\n+      long maxWaitTime, String factoryPid, Map<String, Object> properties, String bundleLocation)\n+      throws SynchronisedInstallerException {\n+    LOGGER.info(\"Creating managed service of factorypid [{}]\", factoryPid);\n+\n+    Configuration createdConfig;\n+    try {\n+      createdConfig = configAdmin.createFactoryConfiguration(factoryPid, null);\n+      createdConfig.setBundleLocation(bundleLocation);\n+    } catch (IOException e) {\n+      throw new SynchronisedInstallerException(\n+          \"Failed to initialize managed service factory configuration with fpid of [\" + factoryPid + \"]\",\n+          e);\n+    }\n+\n+    try {\n+      if (properties == null || properties.isEmpty()) {\n+        createdConfig.update();\n+      } else {\n+        createdConfig.update(toDic(properties));\n+      }\n+    } catch (IOException e) {\n+      throw new SynchronisedInstallerException(\n+          \"Failed to update created managed service factory configuration with pid of [\"\n+              + createdConfig.getPid()\n+              + \"]\",\n+          e);\n+    }\n+\n+    waitForServiceAvailability(maxWaitTime, createdConfig.getPid());\n+    return createdConfig;\n+  }\n+\n+  @Override\n+  public void updateManagedService(\n+      String servicePid, Map<String, Object> properties, String bundleLocation)\n+      throws SynchronisedInstallerException {\n+    updateManagedService(DEFAULT_MAX_SERVICE_WAIT, servicePid, properties, bundleLocation);\n+  }\n+\n+  @Override\n+  public void updateManagedService(\n+      long maxWaitTime, String servicePid, Map<String, Object> properties, String bundleLocation)\n+      throws SynchronisedInstallerException {\n+    waitForServiceAvailability(maxWaitTime, servicePid);\n+\n+    LOGGER.debug(\n+        \"Registering service configuration listener to listen to service with pid of {}.\",\n+        servicePid);\n+    ServiceConfigurationListener listener = new ServiceConfigurationListener(servicePid);\n+    ServiceRegistration<?> registration =\n+        bundleContext.registerService(ConfigurationListener.class.getName(), listener, null);\n+\n+    Callable<Boolean> isServiceUpdated =\n+        () -> {\n+          LOGGER.debug(\n+              \"Waiting for service with pid [{}] to reflect configuration updates...\", servicePid);\n+          return listener.isUpdated();\n+        };\n+\n+    boolean available;\n+\n+    LOGGER.info(\"Updating configuration of service with pid [{}].\", servicePid);\n+\n+    try {\n+      Configuration config = configAdmin.getConfiguration(servicePid, bundleLocation);\n+      config.setBundleLocation(bundleLocation);\n+      config.update(toDic(properties));\n+\n+      LOGGER.debug(\"Updated configuration of service with pid [{}].\", servicePid);\n+      available = wait(isServiceUpdated, maxWaitTime, DEFAULT_POLLING_INTERVAL);\n+    } catch (IOException e) {\n+      throw new SynchronisedInstallerException(\n+          \"Failed to update managed service configuration with pid of [\" + servicePid + \"]\", e);\n+    } catch (Exception e) {\n+      throw new SynchronisedInstallerException(\n+          \"Interrupted while waiting for service with pid of [\" + servicePid + \"] to be updated.\",\n+          e);\n+    } finally {\n+      registration.unregister();\n+    }\n+\n+    if (!available) {\n+      throw new SynchronisedInstallerException(\n+          \"Managed service failed to update within [\" + maxWaitTime + \"] ms.\");\n+    }\n+  }\n+\n+  @Override\n+  public void waitForServiceAvailability(String servicePid) throws SynchronisedInstallerException {\n+    waitForServiceAvailability(DEFAULT_MAX_SERVICE_WAIT, servicePid);\n+  }\n+\n+  @Override\n+  public void waitForServiceAvailability(long maxWaitTime, String servicePid)\n+      throws SynchronisedInstallerException {\n+    ServiceTracker st = getServiceTracker(bundleContext, getServiceIdFilter(servicePid));\n+    st.open(true);\n+\n+    Callable<Boolean> isServiceAvailable =\n+        () -> {\n+          LOGGER.debug(\"Waiting for service with pid [{}] to be registered\", servicePid);\n+          return st.getService() != null;\n+        };\n+\n+    try {\n+      wait(\n+          isServiceAvailable,\n+          maxWaitTime,\n+          DEFAULT_POLLING_INTERVAL,\n+          \"Managed service failed to appear after [\" + maxWaitTime + \"] ms.\");\n+    } finally {\n+      st.close();\n+    }\n+  }\n+\n+  @Override\n+  public void installFeatures(String feature, String... additionalFeatures)\n+      throws SynchronisedInstallerException {\n+    installFeatures(DEFAULT_MAX_FEATURE_WAIT, feature, additionalFeatures);\n+  }\n+\n+  @Override\n+  public void installFeatures(long maxWaitTime, String feature, String... additionalFeatures)\n+      throws SynchronisedInstallerException {\n+\n+    Set<String> featuresToInstall =\n+        getFeaturesFromNames(feature, additionalFeatures)\n+            .filter(f -> !featuresService.isInstalled(f))\n+            .map(Feature::getName)\n+            .collect(Collectors.toSet());\n+\n+    LOGGER.info(\"Installing the following features: [{}]\", String.join(\", \", featuresToInstall));\n+    long startTime = System.currentTimeMillis();\n+    try {\n+      AccessController.doPrivileged(\n+          (PrivilegedExceptionAction<Void>)\n+              () -> {\n+                featuresService.installFeatures(\n+                    featuresToInstall, EnumSet.of(FeaturesService.Option.NoAutoRefreshBundles));\n+                return null;\n+              });\n+    } catch (Exception e) {\n+      throw new SynchronisedInstallerException(\n+          \"Failed to install features [\" + String.join(\",\", featuresToInstall) + \"]\", e);\n+    }\n+\n+    waitForBundles(maxWaitTime);\n+    LOGGER.info(\"Finished installing features in {} ms\", (System.currentTimeMillis() - startTime));\n+  }\n+\n+  @Override\n+  public void uninstallFeatures(String feature, String... additionalFeatures)\n+      throws SynchronisedInstallerException {\n+    uninstallFeatures(DEFAULT_MAX_FEATURE_WAIT, feature, additionalFeatures);\n+  }\n+\n+  @Override\n+  public void uninstallFeatures(long maxWaitTime, String feature, String... additionalFeatures)\n+      throws SynchronisedInstallerException {\n+    Set<String> featuresToUninstall =\n+        getFeaturesFromNames(feature, additionalFeatures)\n+            .filter(f -> featuresService.isInstalled(f))\n+            .map(Feature::getName)\n+            .collect(Collectors.toSet());\n+\n+    LOGGER.info(\n+        \"Uninstalling the following features: [{}]\", String.join(\", \", featuresToUninstall));\n+    long startTime = System.currentTimeMillis();\n+    try {\n+      AccessController.doPrivileged(\n+          (PrivilegedExceptionAction<Void>)\n+              () -> {\n+                featuresService.uninstallFeatures(\n+                    featuresToUninstall, EnumSet.of(FeaturesService.Option.NoAutoRefreshBundles));\n+                return null;\n+              });\n+\n+    } catch (Exception e) {\n+      throw new SynchronisedInstallerException(\n+          \"Failed to uninstall features [\" + String.join(\",\", featuresToUninstall) + \"]\", e);\n+    }\n+\n+    waitForBundles(maxWaitTime);\n+    LOGGER.info(\n+        \"Finished uninstalling features in {} ms\", (System.currentTimeMillis() - startTime));\n+  }\n+\n+  @Override\n+  public void stopBundles(String symbolicName, String... additionalSymbolicNames)\n+      throws SynchronisedInstallerException {\n+    Set<String> toStop = toSet(symbolicName, additionalSymbolicNames);\n+    LOGGER.info(\"Stopping the following bundles:[{}]\", String.join(\", \", toStop));\n+    long startTime = System.currentTimeMillis();\n+\n+    for (Bundle bundle : bundleContext.getBundles()) {\n+      if (toStop.contains(bundle.getSymbolicName())) {\n+        try {\n+          bundle.stop();\n+        } catch (BundleException e) {\n+          throw new SynchronisedInstallerException(\n+              \"Failed to stop bundle [\" + bundle.getSymbolicName() + \"]\");\n+        }\n+      }\n+    }\n+\n+    LOGGER.info(\"Finished stopping bundles in [{}] ms\", (System.currentTimeMillis() - startTime));\n+  }\n+\n+  @Override\n+  public void startBundles(String symbolicName, String... additionalSymbolicNames)\n+      throws SynchronisedInstallerException {\n+    Set<String> toStart = toSet(symbolicName, additionalSymbolicNames);\n+    LOGGER.info(\"Starting the following bundles:[{}]\", String.join(\", \", toStart));\n+    long startTime = System.currentTimeMillis();\n+\n+    for (Bundle bundle : bundleContext.getBundles()) {\n+      if (toStart.contains(bundle.getSymbolicName())) {\n+        try {\n+          bundle.start();\n+        } catch (BundleException e) {\n+          throw new SynchronisedInstallerException(\n+              \"Failed to start bundle [\" + bundle.getSymbolicName() + \"]\");\n+        }\n+      }\n+    }\n+\n+    LOGGER.info(\"Finished starting bundles in [{}] ms\", (System.currentTimeMillis() - startTime));\n+  }\n+\n+  @Override\n+  public void waitForBundles(String... symbolicNames) throws SynchronisedInstallerException {\n+    waitForBundles(DEFAULT_MAX_BUNDLE_WAIT, symbolicNames);\n+  }\n+\n+  @Override\n+  public void waitForBundles(long maxWaitTime, String... symbolicNames)\n+      throws SynchronisedInstallerException {\n+    Set<String> toWaitFor =\n+        symbolicNames.length == 0\n+            ? Arrays.stream(bundleContext.getBundles())\n+                .map(Bundle::getSymbolicName)\n+                .collect(Collectors.toSet())\n+            : Arrays.stream(symbolicNames).collect(Collectors.toSet());\n+\n+    Callable<Boolean> areBundlesReady = () -> getUnavailableBundles(toWaitFor).isEmpty();\n+\n+    boolean available = wait(areBundlesReady, maxWaitTime, DEFAULT_POLLING_INTERVAL);\n+    if (!available) {\n+      printBundleInformation(LOGGER::error, getUnavailableBundles(toWaitFor));\n+      throw new SynchronisedInstallerException(\n+          \"Bundles failed to reach Active within: \" + maxWaitTime + \" ms.\");\n+    }\n+  }\n+\n+  @VisibleForTesting\n+  protected boolean wait(\n+      Callable<Boolean> conditionIsMet, long maxWait, long pollInterval, String onFailureMessage)\n+      throws SynchronisedInstallerException {\n+    if (pollInterval > maxWait) {\n+      throw new SynchronisedInstallerException(\"Poll interval cannot be larger than maxWait time.\");\n+    }\n+\n+    final long startTime = System.currentTimeMillis();\n+    LOGGER.trace(\"Waiting for condition to be met. Max wait time: [{}] ms\", maxWait);\n+\n+    do {\n+      try {\n+        if (conditionIsMet.call()) {\n+          return true;\n+        } else {\n+          Thread.sleep(pollInterval);\n+        }\n+      } catch (InterruptedException e) {\n+        throw new SynchronisedInstallerException(\n+            \"Interrupted while waiting for condition to be met.\", e);\n+      } catch (Exception e) {\n+        throw new SynchronisedInstallerException(\n+            \"Exception thrown while waiting for condition to be met.\", e);\n+      }\n+\n+    } while ((System.currentTimeMillis() - startTime) <= maxWait);\n+\n+    LOGGER.trace(\"Condition not met within [{}]\", maxWait);\n+\n+    if (onFailureMessage != null) {\n+      throw new SynchronisedInstallerException(onFailureMessage);\n+    }\n+    return false;\n+  }\n+\n+  protected boolean wait(Callable<Boolean> conditionIsMet, long maxWait, long pollInterval)\n+      throws SynchronisedInstallerException {\n+    return wait(conditionIsMet, maxWait, pollInterval, null);\n+  }\n+\n+  private Set<Bundle> getUnavailableBundles(Set<String> toCheck)\n+      throws SynchronisedInstallerException {\n+    Set<Bundle> unavailableBundles = new HashSet<>();\n+    Set<Bundle> failedBundles = new HashSet<>();\n+\n+    for (Bundle bundle : bundleContext.getBundles()) {\n+      if (!toCheck.contains(bundle.getSymbolicName())) {\n+        continue;\n+      }\n+\n+      String bundleName = bundle.getHeaders().get(Constants.BUNDLE_NAME);\n+      BundleInfo bundleInfo = bundleService.getInfo(bundle);\n+      BundleState bundleState = bundleInfo.getState();\n+\n+      if (BundleState.Failure.equals(bundleState)) {\n+        failedBundles.add(bundle);\n+      }\n+\n+      if (bundleInfo.isFragment()) {\n+        if (!BundleState.Resolved.equals(bundleState)) {\n+          LOGGER.debug(\"Fragment [{}] not ready with state [{}]\", bundleName, bundleState);\n+          unavailableBundles.add(bundle);\n+        }\n+      } else if (!BundleState.Active.equals(bundleState)) {\n+        LOGGER.debug(\"Bundle [{}] not ready with state [{}]\", bundleName, bundleState);\n+        unavailableBundles.add(bundle);\n+      }\n+    }\n+\n+    if (!failedBundles.isEmpty()) {\n+      printBundleInformation(LOGGER::error, failedBundles);\n+      throw new SynchronisedInstallerException(\n+          \"Bundles failed to start [\"\n+              + failedBundles.stream().map(Bundle::getSymbolicName).collect(Collectors.joining(\",\"))\n+              + \"].\");\n+    }\n+\n+    if (!unavailableBundles.isEmpty()) {\n+      LOGGER.info(\n+          \"Waiting on the following bundles to reach Active state [{}]\",\n+          unavailableBundles\n+              .stream()\n+              .map(Bundle::getSymbolicName)\n+              .collect(Collectors.joining(\",\")));\n+    }\n+\n+    return unavailableBundles;\n+  }\n+\n+  @VisibleForTesting\n+  protected ServiceTracker getServiceTracker(BundleContext bundleContext, Filter filter) {\n+    return new ServiceTracker<>(bundleContext, filter, null);\n+  }\n+\n+  private void printBundleInformation(\n+      BiConsumer<String, Object[]> logConsumer, Collection<Bundle> bundles) {\n+    for (Bundle bundle : bundles) {\n+      StringBuilder headerString = new StringBuilder(\"[ \");\n+      Enumeration<String> keys = bundle.getHeaders().keys();\n+\n+      while (keys.hasMoreElements()) {\n+        String key = keys.nextElement();\n+        headerString.append(key).append(\"=\").append(bundle.getHeaders().get(key)).append(\", \");\n+      }\n+\n+      headerString.append(\" ]\");\n+      logConsumer.accept(\n+          \"\\n\\tBundle: {}_v{} | {}\\n\\tHeaders: {}\",\n+          new Object[] {\n+            bundle.getSymbolicName(),\n+            bundle.getVersion(),\n+            BUNDLE_STATES.getOrDefault(bundle.getState(), \"UNKNOWN\"),\n+            headerString\n+          });\n+    }\n+  }\n+\n+  private Stream<Feature> getFeaturesFromNames(String feature, String... additionalFeatures)\n+      throws SynchronisedInstallerException {\n+    Stream.Builder<Feature> transformedFeatures = Stream.builder();\n+\n+    for (String f : toList(feature, additionalFeatures)) {\n+      try {\n+        transformedFeatures.add(featuresService.getFeature(f));\n+      } catch (Exception e) {\n+        throw new SynchronisedInstallerException(\"Failed to retrieve feature [\" + f + \"]\", e);\n+      }\n+    }\n+\n+    return transformedFeatures.build();\n+  }\n+\n+  private Filter getServiceIdFilter(String servicePid) throws SynchronisedInstallerException {\n+    try {\n+      return bundleContext.createFilter(String.format(SERVICE_PID_FILTER, servicePid));\n+    } catch (Exception e) {\n+      throw new SynchronisedInstallerException(\"Failed to create service filter.\", e);\n+    }\n+  }\n+\n+  private Dictionary<String, Object> toDic(Map<String, Object> props) {\n+    Dictionary<String, Object> dic = new Hashtable<>();\n+    props.forEach(dic::put);\n+    return dic;\n+  }\n+\n+  private <T> List<T> toList(T ele, T... eles) {\n+    return Stream.concat(Stream.of(ele), Stream.of(eles)).collect(Collectors.toList());\n+  }\n+\n+  private <T> Set<T> toSet(T ele, T... eles) {\n+    return Stream.concat(Stream.of(ele), Stream.of(eles)).collect(Collectors.toSet());\n+  }\n+\n+  private class ServiceConfigurationListener implements ConfigurationListener {","path":"platform/sync-installer/sync-installer-impl/src/main/java/org/codice/ddf/sync/installer/impl/SynchronisedInstallerImpl.java","position":543,"original_position":543,"commit_id":"6a3c8c5328f9cbd9dcc98a8f5ebaa8fa839548db","original_commit_id":"6a3c8c5328f9cbd9dcc98a8f5ebaa8fa839548db","user":{"login":"paouelle","id":10549555,"node_id":"MDQ6VXNlcjEwNTQ5NTU1","avatar_url":"https://avatars0.githubusercontent.com/u/10549555?v=4","gravatar_id":"","url":"https://api.github.com/users/paouelle","html_url":"https://github.com/paouelle","followers_url":"https://api.github.com/users/paouelle/followers","following_url":"https://api.github.com/users/paouelle/following{/other_user}","gists_url":"https://api.github.com/users/paouelle/gists{/gist_id}","starred_url":"https://api.github.com/users/paouelle/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/paouelle/subscriptions","organizations_url":"https://api.github.com/users/paouelle/orgs","repos_url":"https://api.github.com/users/paouelle/repos","events_url":"https://api.github.com/users/paouelle/events{/privacy}","received_events_url":"https://api.github.com/users/paouelle/received_events","type":"User","site_admin":false},"body":"✏️ javadoc. I would add a note to make sure this is never a `SynchronizedConfigurationListener` as we want to make sure we are notified from the same pool of threads that are used to dispatch asynchronous configuration events.","created_at":"2018-09-24T15:23:28Z","updated_at":"2018-09-24T15:46:45Z","html_url":"https://github.com/codice/ddf/pull/3778#discussion_r219878955","pull_request_url":"https://api.github.com/repos/codice/ddf/pulls/3778","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/codice/ddf/pulls/comments/219878955"},"html":{"href":"https://github.com/codice/ddf/pull/3778#discussion_r219878955"},"pull_request":{"href":"https://api.github.com/repos/codice/ddf/pulls/3778"}}},"pull_request":{"url":"https://api.github.com/repos/codice/ddf/pulls/3778","id":217110552,"node_id":"MDExOlB1bGxSZXF1ZXN0MjE3MTEwNTUy","html_url":"https://github.com/codice/ddf/pull/3778","diff_url":"https://github.com/codice/ddf/pull/3778.diff","patch_url":"https://github.com/codice/ddf/pull/3778.patch","issue_url":"https://api.github.com/repos/codice/ddf/issues/3778","number":3778,"state":"open","locked":false,"title":"DDF-4163: Make WaitForReady a karaf command ","user":{"login":"tbatie","id":9013620,"node_id":"MDQ6VXNlcjkwMTM2MjA=","avatar_url":"https://avatars3.githubusercontent.com/u/9013620?v=4","gravatar_id":"","url":"https://api.github.com/users/tbatie","html_url":"https://github.com/tbatie","followers_url":"https://api.github.com/users/tbatie/followers","following_url":"https://api.github.com/users/tbatie/following{/other_user}","gists_url":"https://api.github.com/users/tbatie/gists{/gist_id}","starred_url":"https://api.github.com/users/tbatie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tbatie/subscriptions","organizations_url":"https://api.github.com/users/tbatie/orgs","repos_url":"https://api.github.com/users/tbatie/repos","events_url":"https://api.github.com/users/tbatie/events{/privacy}","received_events_url":"https://api.github.com/users/tbatie/received_events","type":"User","site_admin":false},"body":"#### What does this PR do?\r\n- Created a SynchronisedInstaller to wait on different portions of the system to be ready\r\n- Updated the installer to use the SynchronisedInstaller, resolves a race condition where the installer would continue through setup when not all the bundles were in an Active state\r\n- Makes WaitForReady a karaf command instead of being in the start script\r\n- Updates a portion of the itests to use the SynchronisedInstaller\r\n- Disabled solr for itests that do not need it\r\n- Made the solr port dynamic for a portion of the itests\r\n\r\n#### Who is reviewing it? \r\n@figliold\r\n@ethantmanns \r\n@jhunzik \r\n@rzwiefel \r\n\r\n#### Select relevant component teams: \r\nhttps://github.com/orgs/codice/teams\r\n\r\n#### Ask 2 committers to review/merge the PR and tag them here.\r\n@clockard\r\n@coyotesqrl\r\n\r\n#### How should this be tested?\r\n- Start up the distribution several times and ensure the wfr is waiting for all the bundles to reach an Active state.\r\n- Go through the installer several times and ensure all the bundles reach an active state before the installer continues\r\n- Review the logs of the SynchronisedInstaller and ensure they are accurately reflecting the state of the system\r\n\r\n#### Any background context you want to provide?\r\n\r\n#### What are the relevant tickets?\r\nhttps://codice.atlassian.net/browse/DDF-2182\r\nhttps://codice.atlassian.net/browse/DDF-3483\r\n\r\n#### Checklist:\r\n- [In progress ] Documentation Updated\r\n- [In progress] Update / Add Unit Tests\r\n- [x ] Update / Add Integration Tests\r\n\r\n#### Notes on Review Process\r\nPlease see [Notes on Review Process](https://codice.atlassian.net/wiki/spaces/DDF/pages/71946981/Pull+Request+Guidelines) for further guidance on requirements for merging and abbreviated reviews. \r\n\r\n#### Review Comment Legend:\r\n- ✏️ (Pencil) This comment is a nitpick or style suggestion, no action required for approval. This comment should provide a suggestion either as an in line code snippet or a gist. \r\n- ❓ (Question Mark) This comment is to gain a clearer understanding of design or code choices, clarification is required but action may not be necessary for approval.\r\n- ❗ (Exclamation Mark) This comment is critical and requires clarification or action before approval.\r\n","created_at":"2018-09-20T21:20:55Z","updated_at":"2018-09-24T15:46:43Z","closed_at":null,"merged_at":null,"merge_commit_sha":"296c242282e61fed12b5fd38b10c7ce3a9adfa2f","assignee":{"login":"coyotesqrl","id":1469860,"node_id":"MDQ6VXNlcjE0Njk4NjA=","avatar_url":"https://avatars2.githubusercontent.com/u/1469860?v=4","gravatar_id":"","url":"https://api.github.com/users/coyotesqrl","html_url":"https://github.com/coyotesqrl","followers_url":"https://api.github.com/users/coyotesqrl/followers","following_url":"https://api.github.com/users/coyotesqrl/following{/other_user}","gists_url":"https://api.github.com/users/coyotesqrl/gists{/gist_id}","starred_url":"https://api.github.com/users/coyotesqrl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/coyotesqrl/subscriptions","organizations_url":"https://api.github.com/users/coyotesqrl/orgs","repos_url":"https://api.github.com/users/coyotesqrl/repos","events_url":"https://api.github.com/users/coyotesqrl/events{/privacy}","received_events_url":"https://api.github.com/users/coyotesqrl/received_events","type":"User","site_admin":false},"assignees":[{"login":"coyotesqrl","id":1469860,"node_id":"MDQ6VXNlcjE0Njk4NjA=","avatar_url":"https://avatars2.githubusercontent.com/u/1469860?v=4","gravatar_id":"","url":"https://api.github.com/users/coyotesqrl","html_url":"https://github.com/coyotesqrl","followers_url":"https://api.github.com/users/coyotesqrl/followers","following_url":"https://api.github.com/users/coyotesqrl/following{/other_user}","gists_url":"https://api.github.com/users/coyotesqrl/gists{/gist_id}","starred_url":"https://api.github.com/users/coyotesqrl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/coyotesqrl/subscriptions","organizations_url":"https://api.github.com/users/coyotesqrl/orgs","repos_url":"https://api.github.com/users/coyotesqrl/repos","events_url":"https://api.github.com/users/coyotesqrl/events{/privacy}","received_events_url":"https://api.github.com/users/coyotesqrl/received_events","type":"User","site_admin":false},{"login":"clockard","id":5248090,"node_id":"MDQ6VXNlcjUyNDgwOTA=","avatar_url":"https://avatars1.githubusercontent.com/u/5248090?v=4","gravatar_id":"","url":"https://api.github.com/users/clockard","html_url":"https://github.com/clockard","followers_url":"https://api.github.com/users/clockard/followers","following_url":"https://api.github.com/users/clockard/following{/other_user}","gists_url":"https://api.github.com/users/clockard/gists{/gist_id}","starred_url":"https://api.github.com/users/clockard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/clockard/subscriptions","organizations_url":"https://api.github.com/users/clockard/orgs","repos_url":"https://api.github.com/users/clockard/repos","events_url":"https://api.github.com/users/clockard/events{/privacy}","received_events_url":"https://api.github.com/users/clockard/received_events","type":"User","site_admin":false}],"requested_reviewers":[{"login":"stustison","id":4391679,"node_id":"MDQ6VXNlcjQzOTE2Nzk=","avatar_url":"https://avatars0.githubusercontent.com/u/4391679?v=4","gravatar_id":"","url":"https://api.github.com/users/stustison","html_url":"https://github.com/stustison","followers_url":"https://api.github.com/users/stustison/followers","following_url":"https://api.github.com/users/stustison/following{/other_user}","gists_url":"https://api.github.com/users/stustison/gists{/gist_id}","starred_url":"https://api.github.com/users/stustison/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stustison/subscriptions","organizations_url":"https://api.github.com/users/stustison/orgs","repos_url":"https://api.github.com/users/stustison/repos","events_url":"https://api.github.com/users/stustison/events{/privacy}","received_events_url":"https://api.github.com/users/stustison/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/codice/ddf/pulls/3778/commits","review_comments_url":"https://api.github.com/repos/codice/ddf/pulls/3778/comments","review_comment_url":"https://api.github.com/repos/codice/ddf/pulls/comments{/number}","comments_url":"https://api.github.com/repos/codice/ddf/issues/3778/comments","statuses_url":"https://api.github.com/repos/codice/ddf/statuses/6a3c8c5328f9cbd9dcc98a8f5ebaa8fa839548db","head":{"label":"tbatie:wait-for-ready","ref":"wait-for-ready","sha":"6a3c8c5328f9cbd9dcc98a8f5ebaa8fa839548db","user":{"login":"tbatie","id":9013620,"node_id":"MDQ6VXNlcjkwMTM2MjA=","avatar_url":"https://avatars3.githubusercontent.com/u/9013620?v=4","gravatar_id":"","url":"https://api.github.com/users/tbatie","html_url":"https://github.com/tbatie","followers_url":"https://api.github.com/users/tbatie/followers","following_url":"https://api.github.com/users/tbatie/following{/other_user}","gists_url":"https://api.github.com/users/tbatie/gists{/gist_id}","starred_url":"https://api.github.com/users/tbatie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tbatie/subscriptions","organizations_url":"https://api.github.com/users/tbatie/orgs","repos_url":"https://api.github.com/users/tbatie/repos","events_url":"https://api.github.com/users/tbatie/events{/privacy}","received_events_url":"https://api.github.com/users/tbatie/received_events","type":"User","site_admin":false},"repo":{"id":38900987,"node_id":"MDEwOlJlcG9zaXRvcnkzODkwMDk4Nw==","name":"ddf","full_name":"tbatie/ddf","private":false,"owner":{"login":"tbatie","id":9013620,"node_id":"MDQ6VXNlcjkwMTM2MjA=","avatar_url":"https://avatars3.githubusercontent.com/u/9013620?v=4","gravatar_id":"","url":"https://api.github.com/users/tbatie","html_url":"https://github.com/tbatie","followers_url":"https://api.github.com/users/tbatie/followers","following_url":"https://api.github.com/users/tbatie/following{/other_user}","gists_url":"https://api.github.com/users/tbatie/gists{/gist_id}","starred_url":"https://api.github.com/users/tbatie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tbatie/subscriptions","organizations_url":"https://api.github.com/users/tbatie/orgs","repos_url":"https://api.github.com/users/tbatie/repos","events_url":"https://api.github.com/users/tbatie/events{/privacy}","received_events_url":"https://api.github.com/users/tbatie/received_events","type":"User","site_admin":false},"html_url":"https://github.com/tbatie/ddf","description":"The Distributed Data Framework - Distribution, SDK, and Test","fork":true,"url":"https://api.github.com/repos/tbatie/ddf","forks_url":"https://api.github.com/repos/tbatie/ddf/forks","keys_url":"https://api.github.com/repos/tbatie/ddf/keys{/key_id}","collaborators_url":"https://api.github.com/repos/tbatie/ddf/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/tbatie/ddf/teams","hooks_url":"https://api.github.com/repos/tbatie/ddf/hooks","issue_events_url":"https://api.github.com/repos/tbatie/ddf/issues/events{/number}","events_url":"https://api.github.com/repos/tbatie/ddf/events","assignees_url":"https://api.github.com/repos/tbatie/ddf/assignees{/user}","branches_url":"https://api.github.com/repos/tbatie/ddf/branches{/branch}","tags_url":"https://api.github.com/repos/tbatie/ddf/tags","blobs_url":"https://api.github.com/repos/tbatie/ddf/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/tbatie/ddf/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/tbatie/ddf/git/refs{/sha}","trees_url":"https://api.github.com/repos/tbatie/ddf/git/trees{/sha}","statuses_url":"https://api.github.com/repos/tbatie/ddf/statuses/{sha}","languages_url":"https://api.github.com/repos/tbatie/ddf/languages","stargazers_url":"https://api.github.com/repos/tbatie/ddf/stargazers","contributors_url":"https://api.github.com/repos/tbatie/ddf/contributors","subscribers_url":"https://api.github.com/repos/tbatie/ddf/subscribers","subscription_url":"https://api.github.com/repos/tbatie/ddf/subscription","commits_url":"https://api.github.com/repos/tbatie/ddf/commits{/sha}","git_commits_url":"https://api.github.com/repos/tbatie/ddf/git/commits{/sha}","comments_url":"https://api.github.com/repos/tbatie/ddf/comments{/number}","issue_comment_url":"https://api.github.com/repos/tbatie/ddf/issues/comments{/number}","contents_url":"https://api.github.com/repos/tbatie/ddf/contents/{+path}","compare_url":"https://api.github.com/repos/tbatie/ddf/compare/{base}...{head}","merges_url":"https://api.github.com/repos/tbatie/ddf/merges","archive_url":"https://api.github.com/repos/tbatie/ddf/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/tbatie/ddf/downloads","issues_url":"https://api.github.com/repos/tbatie/ddf/issues{/number}","pulls_url":"https://api.github.com/repos/tbatie/ddf/pulls{/number}","milestones_url":"https://api.github.com/repos/tbatie/ddf/milestones{/number}","notifications_url":"https://api.github.com/repos/tbatie/ddf/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/tbatie/ddf/labels{/name}","releases_url":"https://api.github.com/repos/tbatie/ddf/releases{/id}","deployments_url":"https://api.github.com/repos/tbatie/ddf/deployments","created_at":"2015-07-10T20:36:35Z","updated_at":"2016-04-06T16:24:17Z","pushed_at":"2018-09-20T21:07:37Z","git_url":"git://github.com/tbatie/ddf.git","ssh_url":"git@github.com:tbatie/ddf.git","clone_url":"https://github.com/tbatie/ddf.git","svn_url":"https://github.com/tbatie/ddf","homepage":"","size":209080,"stargazers_count":0,"watchers_count":0,"language":"Java","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":{"key":"other","name":"Other","spdx_id":null,"url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"codice:master","ref":"master","sha":"6627fa3d6e2a80f9321ff952859ab271cb8a8c60","user":{"login":"codice","id":2386734,"node_id":"MDEyOk9yZ2FuaXphdGlvbjIzODY3MzQ=","avatar_url":"https://avatars2.githubusercontent.com/u/2386734?v=4","gravatar_id":"","url":"https://api.github.com/users/codice","html_url":"https://github.com/codice","followers_url":"https://api.github.com/users/codice/followers","following_url":"https://api.github.com/users/codice/following{/other_user}","gists_url":"https://api.github.com/users/codice/gists{/gist_id}","starred_url":"https://api.github.com/users/codice/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codice/subscriptions","organizations_url":"https://api.github.com/users/codice/orgs","repos_url":"https://api.github.com/users/codice/repos","events_url":"https://api.github.com/users/codice/events{/privacy}","received_events_url":"https://api.github.com/users/codice/received_events","type":"Organization","site_admin":false},"repo":{"id":15173563,"node_id":"MDEwOlJlcG9zaXRvcnkxNTE3MzU2Mw==","name":"ddf","full_name":"codice/ddf","private":false,"owner":{"login":"codice","id":2386734,"node_id":"MDEyOk9yZ2FuaXphdGlvbjIzODY3MzQ=","avatar_url":"https://avatars2.githubusercontent.com/u/2386734?v=4","gravatar_id":"","url":"https://api.github.com/users/codice","html_url":"https://github.com/codice","followers_url":"https://api.github.com/users/codice/followers","following_url":"https://api.github.com/users/codice/following{/other_user}","gists_url":"https://api.github.com/users/codice/gists{/gist_id}","starred_url":"https://api.github.com/users/codice/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codice/subscriptions","organizations_url":"https://api.github.com/users/codice/orgs","repos_url":"https://api.github.com/users/codice/repos","events_url":"https://api.github.com/users/codice/events{/privacy}","received_events_url":"https://api.github.com/users/codice/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codice/ddf","description":"DDF Distributed Data Framework - an open source, modular integration framework.","fork":false,"url":"https://api.github.com/repos/codice/ddf","forks_url":"https://api.github.com/repos/codice/ddf/forks","keys_url":"https://api.github.com/repos/codice/ddf/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codice/ddf/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codice/ddf/teams","hooks_url":"https://api.github.com/repos/codice/ddf/hooks","issue_events_url":"https://api.github.com/repos/codice/ddf/issues/events{/number}","events_url":"https://api.github.com/repos/codice/ddf/events","assignees_url":"https://api.github.com/repos/codice/ddf/assignees{/user}","branches_url":"https://api.github.com/repos/codice/ddf/branches{/branch}","tags_url":"https://api.github.com/repos/codice/ddf/tags","blobs_url":"https://api.github.com/repos/codice/ddf/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codice/ddf/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codice/ddf/git/refs{/sha}","trees_url":"https://api.github.com/repos/codice/ddf/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codice/ddf/statuses/{sha}","languages_url":"https://api.github.com/repos/codice/ddf/languages","stargazers_url":"https://api.github.com/repos/codice/ddf/stargazers","contributors_url":"https://api.github.com/repos/codice/ddf/contributors","subscribers_url":"https://api.github.com/repos/codice/ddf/subscribers","subscription_url":"https://api.github.com/repos/codice/ddf/subscription","commits_url":"https://api.github.com/repos/codice/ddf/commits{/sha}","git_commits_url":"https://api.github.com/repos/codice/ddf/git/commits{/sha}","comments_url":"https://api.github.com/repos/codice/ddf/comments{/number}","issue_comment_url":"https://api.github.com/repos/codice/ddf/issues/comments{/number}","contents_url":"https://api.github.com/repos/codice/ddf/contents/{+path}","compare_url":"https://api.github.com/repos/codice/ddf/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codice/ddf/merges","archive_url":"https://api.github.com/repos/codice/ddf/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codice/ddf/downloads","issues_url":"https://api.github.com/repos/codice/ddf/issues{/number}","pulls_url":"https://api.github.com/repos/codice/ddf/pulls{/number}","milestones_url":"https://api.github.com/repos/codice/ddf/milestones{/number}","notifications_url":"https://api.github.com/repos/codice/ddf/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codice/ddf/labels{/name}","releases_url":"https://api.github.com/repos/codice/ddf/releases{/id}","deployments_url":"https://api.github.com/repos/codice/ddf/deployments","created_at":"2013-12-13T20:38:28Z","updated_at":"2018-09-20T21:37:34Z","pushed_at":"2018-09-24T14:27:50Z","git_url":"git://github.com/codice/ddf.git","ssh_url":"git@github.com:codice/ddf.git","clone_url":"https://github.com/codice/ddf.git","svn_url":"https://github.com/codice/ddf","homepage":"http://ddf.codice.org","size":404593,"stargazers_count":89,"watchers_count":89,"language":"Java","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":153,"mirror_url":null,"archived":false,"open_issues_count":42,"license":{"key":"other","name":"Other","spdx_id":null,"url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":153,"open_issues":42,"watchers":89,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/codice/ddf/pulls/3778"},"html":{"href":"https://github.com/codice/ddf/pull/3778"},"issue":{"href":"https://api.github.com/repos/codice/ddf/issues/3778"},"comments":{"href":"https://api.github.com/repos/codice/ddf/issues/3778/comments"},"review_comments":{"href":"https://api.github.com/repos/codice/ddf/pulls/3778/comments"},"review_comment":{"href":"https://api.github.com/repos/codice/ddf/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/codice/ddf/pulls/3778/commits"},"statuses":{"href":"https://api.github.com/repos/codice/ddf/statuses/6a3c8c5328f9cbd9dcc98a8f5ebaa8fa839548db"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"paouelle"}}