[CAMEL-8948] Precise synchronization with BP container of the test bundle (cherry picked from commit 692e479b940b1198df3cec386c16aa4fd071e16c)
Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/d0586a23 Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/d0586a23 Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/d0586a23 Branch: refs/heads/camel-2.15.x Commit: d0586a23e54af00611aba3d72f29d945ebdb9f31 Parents: ec5a7aa Author: Grzegorz Grzybek <gr.grzy...@gmail.com> Authored: Fri Jul 17 21:40:16 2015 +0200 Committer: Grzegorz Grzybek <gr.grzy...@gmail.com> Committed: Tue Jul 21 09:45:02 2015 +0200 ---------------------------------------------------------------------- .../test/blueprint/CamelBlueprintHelper.java | 52 ++++++++++- .../blueprint/CamelBlueprintTestSupport.java | 91 +++++++------------- .../org/apache/camel/test/blueprint/Main.java | 3 +- 3 files changed, 82 insertions(+), 64 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/d0586a23/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/CamelBlueprintHelper.java ---------------------------------------------------------------------- diff --git a/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/CamelBlueprintHelper.java b/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/CamelBlueprintHelper.java index 126f68a..b920f9f 100644 --- a/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/CamelBlueprintHelper.java +++ b/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/CamelBlueprintHelper.java @@ -35,6 +35,9 @@ import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.jar.JarInputStream; import de.kalpatec.pojosr.framework.PojoServiceRegistryFactoryImpl; @@ -58,6 +61,9 @@ import org.osgi.framework.Filter; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.blueprint.container.BlueprintEvent; +import org.osgi.service.blueprint.container.BlueprintListener; import org.osgi.service.cm.Configuration; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.util.tracker.ServiceTracker; @@ -107,6 +113,9 @@ public final class CamelBlueprintHelper { String uid = "" + System.currentTimeMillis(); String tempDir = "target/bundles/" + uid; System.setProperty("org.osgi.framework.storage", tempDir); + // explicitly set this to "false" - we will not depend on the order of starting bundles, + // (and running their BP containers) but we will have to do more synchornization + System.setProperty("org.apache.aries.blueprint.synchronous", "false"); createDirectory(tempDir); // use another directory for the jar of the bundle as it cannot be in the same directory @@ -169,7 +178,8 @@ public final class CamelBlueprintHelper { // pick up persistent file configuration @SuppressWarnings({"unchecked", "rawtypes"}) public static void setPersistentFileForConfigAdmin(BundleContext bundleContext, String pid, - String fileName, Dictionary props) throws IOException { + String fileName, final Dictionary props, + String symbolicName, Set<Long> bpEvents) throws IOException, InterruptedException { if (pid != null) { if (fileName == null) { throw new IllegalArgumentException("The persistent file should not be null"); @@ -186,9 +196,20 @@ public final class CamelBlueprintHelper { if (configAdmin != null) { // ensure we update // we *have to* use "null" as 2nd arg to have correct bundle location for Configuration object - Configuration config = configAdmin.getConfiguration(pid, null); + final Configuration config = configAdmin.getConfiguration(pid, null); LOG.info("Updating ConfigAdmin {} by overriding properties {}", config, props); - config.update(props); + // we will have update and in consequence, BP container reload, let's wait for it to + // be CREATED again + CamelBlueprintHelper.waitForBlueprintContainer(bpEvents, bundleContext, symbolicName, BlueprintEvent.CREATED, new Runnable() { + @Override + public void run() { + try { + config.update(props); + } catch (IOException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + }); } } @@ -226,6 +247,7 @@ public final class CamelBlueprintHelper { // Note that the tracker is not closed to keep the reference // This is buggy, as the service reference may change i think Object svc = tracker.waitForService(timeout); + if (svc == null) { Dictionary<?, ?> dic = bundleContext.getBundle().getHeaders(); LOG.warn("Test bundle headers: " + explode(dic)); @@ -248,6 +270,30 @@ public final class CamelBlueprintHelper { } } + /** + * Synchronization method to wait for particular state of BlueprintContainer under test. + */ + public static void waitForBlueprintContainer(final Set<Long> eventHistory, BundleContext context, final String symbolicName, final int bpEvent, final Runnable runAndWait) throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + ServiceRegistration<BlueprintListener> registration = context.registerService(BlueprintListener.class, new BlueprintListener() { + @Override + public void blueprintEvent(BlueprintEvent event) { + if (event.getType() == bpEvent && event.getBundle().getSymbolicName().equals(symbolicName)) { + // we skip events that we've already seen + // it works with BP container reloads if next CREATE state is at least 1ms after previous one + if (eventHistory == null || eventHistory.add(event.getTimestamp())) { + latch.countDown(); + } + } + } + }, null); + if (runAndWait != null) { + runAndWait.run(); + } + latch.await(CamelBlueprintHelper.DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS); + registration.unregister(); + } + protected static TinyBundle createTestBundle(String name, String version, String descriptors) throws FileNotFoundException, MalformedURLException { TinyBundle bundle = TinyBundles.newBundle(); for (URL url : getBlueprintDescriptors(descriptors)) { http://git-wip-us.apache.org/repos/asf/camel/blob/d0586a23/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/CamelBlueprintTestSupport.java ---------------------------------------------------------------------- diff --git a/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/CamelBlueprintTestSupport.java b/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/CamelBlueprintTestSupport.java index 71e5864..030f99e 100644 --- a/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/CamelBlueprintTestSupport.java +++ b/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/CamelBlueprintTestSupport.java @@ -17,14 +17,14 @@ package org.apache.camel.test.blueprint; import java.io.File; +import java.io.IOException; import java.util.Dictionary; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Properties; import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import org.apache.camel.CamelContext; import org.apache.camel.component.properties.PropertiesComponent; @@ -36,13 +36,9 @@ import org.junit.AfterClass; import org.junit.Before; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceRegistration; -import org.osgi.service.blueprint.container.BlueprintContainer; import org.osgi.service.blueprint.container.BlueprintEvent; -import org.osgi.service.blueprint.container.BlueprintListener; import org.osgi.service.cm.Configuration; import org.osgi.service.cm.ConfigurationAdmin; -import org.osgi.service.cm.ConfigurationEvent; -import org.osgi.service.cm.ConfigurationListener; /** * Base class for OSGi Blueprint unit tests with Camel. @@ -93,7 +89,7 @@ public abstract class CamelBlueprintTestSupport extends CamelTestSupport { } // must reuse props as we can do both load from .cfg file and override afterwards - Dictionary props = new Properties(); + final Dictionary props = new Properties(); // load configuration file String[] file = loadConfigAdminConfigurationFile(); @@ -101,79 +97,54 @@ public abstract class CamelBlueprintTestSupport extends CamelTestSupport { throw new IllegalArgumentException("The returned String[] from loadConfigAdminConfigurationFile must be of length 2, was " + file.length); } + // if blueprint XML uses <cm:property-placeholder> (any update-strategy and any default properties) + // - org.apache.aries.blueprint.compendium.cm.ManagedObjectManager.register() is called + // - ManagedServiceUpdate is scheduled in felix.cm + // - org.apache.felix.cm.impl.ConfigurationImpl.setDynamicBundleLocation() is called + // - CM_LOCATION_CHANGED event is fired + // - if BP was alredy created, it's <cm:property-placeholder> receives the event and + // - org.apache.aries.blueprint.compendium.cm.CmPropertyPlaceholder.updated() is called, + // but no BP reload occurs + // we will however wait for BP container of the test bundle to become CREATED for the first time + // each configadmin update *may* lead to reload of BP container, if it uses <cm:property-placeholder> + // with update-strategy="reload" + + // we will gather timestamps of BP events. We don't want to be fooled but repeated events related + // to the same state of BP container + Set<Long> bpEvents = new HashSet<>(); + + CamelBlueprintHelper.waitForBlueprintContainer(bpEvents, answer, symbolicName, BlueprintEvent.CREATED, null); + if (file != null) { if (!new File(file[0]).exists()) { throw new IllegalArgumentException("The provided file \"" + file[0] + "\" from loadConfigAdminConfigurationFile doesn't exist"); } - CamelBlueprintHelper.setPersistentFileForConfigAdmin(answer, file[1], file[0], props); + CamelBlueprintHelper.setPersistentFileForConfigAdmin(answer, file[1], file[0], props, symbolicName, bpEvents); } // allow end user to override properties String pid = useOverridePropertiesWithConfigAdmin(props); if (pid != null) { - // we will update the configuration now. As OSGi is highly asynchronous, we need to make the tests as repeatable as possible - // the problem is when blueprint container defines cm:property-placeholder with update-strategy="reload" - // updating the configuration leads to (felix framework + aries blueprint): - // 1. schedule org.apache.felix.cm.impl.ConfigurationManager.UpdateConfiguration object to run in config admin thread - // 2. this thread calls org.apache.felix.cm.impl.ConfigurationImpl#tryBindLocation() - // 3. org.osgi.service.cm.ConfigurationEvent#CM_LOCATION_CHANGED is send - // 4. org.apache.aries.blueprint.compendium.cm.ManagedObjectManager.ConfigurationWatcher#updated() is invoked - // 5. new Thread().start() is called - // 6. org.apache.aries.blueprint.compendium.cm.ManagedObject#updated() is called - // 7. org.apache.aries.blueprint.compendium.cm.CmPropertyPlaceholder#updated() is called - // 8. new Thread().start() is called - // 9. org.apache.aries.blueprint.services.ExtendedBlueprintContainer#reload() is called which destroys everything in BP container - // 10. finally reload of BP container is scheduled (in yet another thread) - // - // if we start/use camel context between point 9 and 10 we may get many different errors described in https://issues.apache.org/jira/browse/ARIES-961 - - // to synchronize this (main) thread of execution with the asynchronous series of events, we can register the following listener. - // this way be sure that we got to point 3 - final CountDownLatch latch = new CountDownLatch(2); - answer.registerService(ConfigurationListener.class, new ConfigurationListener() { - @Override - public void configurationEvent(ConfigurationEvent event) { - if (event.getType() == ConfigurationEvent.CM_LOCATION_CHANGED) { - latch.countDown(); - } - // when we update the configuration, BP container will be reloaded as well - // hoping that we get the event after *second* restart, let's register the listener - answer.registerService(BlueprintListener.class, new BlueprintListener() { - @Override - public void blueprintEvent(BlueprintEvent event) { - if (event.getType() == BlueprintEvent.CREATED && event.getBundle().getSymbolicName().equals(symbolicName)) { - latch.countDown(); - } - } - }, null); - } - }, null); - + // we will update the configuration again ConfigurationAdmin configAdmin = CamelBlueprintHelper.getOsgiService(answer, ConfigurationAdmin.class); // passing null as second argument ties the configuration to correct bundle. // using single-arg method causes: // *ERROR* Cannot use configuration xxx.properties for [org.osgi.service.cm.ManagedService, id=N, bundle=N/jar:file:xyz.jar!/]: No visibility to configuration bound to file:pojosr - Configuration config = configAdmin.getConfiguration(pid, null); + final Configuration config = configAdmin.getConfiguration(pid, null); if (config == null) { throw new IllegalArgumentException("Cannot find configuration with pid " + pid + " in OSGi ConfigurationAdmin service."); } log.info("Updating ConfigAdmin {} by overriding properties {}", config, props); - config.update(props); - - latch.await(CamelBlueprintHelper.DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS); - } else { - // let's wait for BP container to start - final CountDownLatch latch = new CountDownLatch(1); - answer.registerService(BlueprintListener.class, new BlueprintListener() { + CamelBlueprintHelper.waitForBlueprintContainer(bpEvents, answer, symbolicName, BlueprintEvent.CREATED, new Runnable() { @Override - public void blueprintEvent(BlueprintEvent event) { - if (event.getType() == BlueprintEvent.CREATED && event.getBundle().getSymbolicName().equals(symbolicName)) { - latch.countDown(); + public void run() { + try { + config.update(props); + } catch (IOException e) { + throw new RuntimeException(e.getMessage(), e); } } - }, null); - - latch.await(CamelBlueprintHelper.DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS); + }); } return answer; http://git-wip-us.apache.org/repos/asf/camel/blob/d0586a23/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/Main.java ---------------------------------------------------------------------- diff --git a/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/Main.java b/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/Main.java index 3094d5e..cadc063 100644 --- a/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/Main.java +++ b/components/camel-test-blueprint/src/main/java/org/apache/camel/test/blueprint/Main.java @@ -100,7 +100,8 @@ public class Main extends MainSupport { } LOG.debug("Starting Blueprint XML file: " + descriptors); bundleContext = createBundleContext(bundleName); - CamelBlueprintHelper.setPersistentFileForConfigAdmin(bundleContext, configAdminPid, configAdminFileName, new Properties()); + CamelBlueprintHelper.setPersistentFileForConfigAdmin(bundleContext, configAdminPid, configAdminFileName, new Properties(), + bundleName, null); camelContext = CamelBlueprintHelper.getOsgiService(bundleContext, CamelContext.class); if (camelContext == null) { throw new IllegalArgumentException("Cannot find CamelContext in blueprint XML file: " + descriptors);