FELIX-1545 integration testcases exhibiting the concurrency issues of
duplicate and lost configuration update
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@809592 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigUpdateStressTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigUpdateStressTest.java
new file mode 100644
index 0000000..298932f
--- /dev/null
+++ b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigUpdateStressTest.java
@@ -0,0 +1,227 @@
+/*
+ * 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.felix.cm.integration;
+
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Dictionary;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.cm.integration.helper.ConfigureThread;
+import org.apache.felix.cm.integration.helper.ManagedServiceFactoryThread;
+import org.apache.felix.cm.integration.helper.ManagedServiceThread;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.JUnit4TestRunner;
+
+
+/**
+ * The <code>ConfigUpdateStressTest</code> class tests the issues related to
+ * concurrency between configuration update (Configuration.update(Dictionary))
+ * and ManagedService[Factory] registration.
+ * <p>
+ * @see <a href="https://issues.apache.org/jira/browse/FELIX-1545">FELIX-1545</a>
+ */
+@RunWith(JUnit4TestRunner.class)
+public class ConfigUpdateStressTest extends ConfigurationTestBase
+{
+
+ @Test
+ public void test_ManagedService_race_condition_test()
+ {
+ int counterMax = 30;
+ int failures = 0;
+
+ for ( int counter = 0; counter < counterMax; counter++ )
+ {
+ try
+ {
+ single_test_ManagedService_race_condition_test( counter );
+ }
+ catch ( Throwable ae )
+ {
+ System.out.println( "single_test_ManagedService_race_condition_test#" + counter + " failed: " + ae );
+ ae.printStackTrace( System.out );
+ failures++;
+ }
+ }
+
+ // fail the test if there is at least one failure
+ if ( failures != 0 )
+ {
+ TestCase.fail( failures + "/" + counterMax + " iterations failed" );
+ }
+ }
+
+
+ @Test
+ public void test_ManagedServiceFactory_race_condition_test()
+ {
+ int counterMax = 30;
+ int failures = 0;
+
+ for ( int counter = 0; counter < counterMax; counter++ )
+ {
+ try
+ {
+ single_test_ManagedServiceFactory_race_condition_test( counter );
+ }
+ catch ( Throwable ae )
+ {
+ System.out.println( "single_test_ManagedServiceFactory_race_condition_test#" + counter + " failed: "
+ + ae );
+ ae.printStackTrace( System.out );
+ failures++;
+ }
+ }
+
+ // fail the test if there is at least one failure
+ if ( failures != 0 )
+ {
+ TestCase.fail( failures + "/" + counterMax + " iterations failed" );
+ }
+ }
+
+
+ // runs a single test to encounter the race condition between ManagedService
+ // registration and Configuration.update(Dictionary)
+ // This test creates/updates configuration and registers a ManagedService
+ // almost at the same time. The ManagedService must receive the
+ // configuration
+ // properties exactly once.
+ private void single_test_ManagedService_race_condition_test( final int counter ) throws IOException,
+ InterruptedException
+ {
+
+ final String pid = "single_test_ManagedService_race_condition_test." + counter;
+
+ final ConfigureThread ct = new ConfigureThread( getConfigurationAdmin(), pid, false );
+ final ManagedServiceThread mt = new ManagedServiceThread( bundleContext, pid );
+
+ try
+ {
+ // start threads -- both are waiting to be triggered
+ ct.start();
+ mt.start();
+
+ // trigger for action
+ ct.trigger();
+ mt.trigger();
+
+ // wait for threads to terminate
+ ct.join();
+ mt.join();
+
+ // wait for all tasks to terminate
+ delay();
+
+ final ArrayList<Dictionary> configs = mt.getConfigs();
+
+ // terminate mt to ensure no further config updates
+ mt.cleanup();
+
+ if ( configs.size() == 0 )
+ {
+ TestCase.fail( "No configuration provided to ManagedService at all" );
+ }
+ else if ( configs.size() == 2 )
+ {
+ final Dictionary props0 = configs.get( 0 );
+ final Dictionary props1 = configs.get( 1 );
+
+ TestCase.assertNull( "Expected first (of two) updates without configuration", props0 );
+ TestCase.assertNotNull( "Expected second (of two) updates with configuration", props1 );
+ }
+ else if ( configs.size() == 1 )
+ {
+ final Dictionary props = configs.get( 0 );
+ TestCase.assertNotNull( "Expected non-null configuration: " + props, props );
+ }
+ else
+ {
+ TestCase.fail( "Unexpectedly got " + configs.size() + " updated" );
+ }
+ }
+ finally
+ {
+ mt.cleanup();
+ ct.cleanup();
+ }
+ }
+
+
+ // runs a single test to encounter the race condition between
+ // ManagedServiceFactory registration and Configuration.update(Dictionary)
+ // This test creates/updates configuration and registers a
+ // ManagedServiceFactory almost at the same time. The ManagedServiceFactory
+ // must receive the configuration properties exactly once.
+ private void single_test_ManagedServiceFactory_race_condition_test( final int counter ) throws IOException,
+ InterruptedException
+ {
+
+ final String factoryPid = "single_test_ManagedServiceFactory_race_condition_test." + counter;
+
+ final ConfigureThread ct = new ConfigureThread( getConfigurationAdmin(), factoryPid, true );
+ final ManagedServiceFactoryThread mt = new ManagedServiceFactoryThread( bundleContext, factoryPid );
+
+ try
+ {
+ // start threads -- both are waiting to be triggered
+ ct.start();
+ mt.start();
+
+ // trigger for action
+ ct.trigger();
+ mt.trigger();
+
+ // wait for threads to terminate
+ ct.join();
+ mt.join();
+
+ // wait for all tasks to terminate
+ delay();
+
+ final ArrayList<Dictionary> configs = mt.getConfigs();
+
+ // terminate mt to ensure no further config updates
+ mt.cleanup();
+
+ if ( configs.size() == 0 )
+ {
+ TestCase.fail( "No configuration provided to ManagedServiceFactory at all" );
+ }
+ else if ( configs.size() == 1 )
+ {
+ final Dictionary props = configs.get( 0 );
+ TestCase.assertNotNull( "Expected non-null configuration: " + props, props );
+ }
+ else
+ {
+ TestCase.fail( "Unexpectedly got " + configs.size() + " updated" );
+ }
+ }
+ finally
+ {
+ mt.cleanup();
+ ct.cleanup();
+ }
+ }
+}
diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ConfigureThread.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ConfigureThread.java
new file mode 100644
index 0000000..7320867
--- /dev/null
+++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ConfigureThread.java
@@ -0,0 +1,92 @@
+/*
+ * 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.felix.cm.integration.helper;
+
+
+import java.io.IOException;
+import java.util.Hashtable;
+
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+
+
+/**
+ * The <code>ConfigureThread</code> class is extends the {@link TestThread} for
+ * use as the configuration creator and updater in the
+ * {@link org.apache.felix.cm.integration.ConfigUpdateStressTest}.
+ */
+public class ConfigureThread extends TestThread
+{
+ private final Configuration config;
+
+ private final Hashtable<String, Object> props;
+
+
+ public ConfigureThread( final ConfigurationAdmin configAdmin, final String pid, final boolean isFactory )
+ throws IOException
+ {
+ // ensure configuration and disown it
+ final Configuration config;
+ if ( isFactory )
+ {
+ config = configAdmin.createFactoryConfiguration( pid );
+ }
+ else
+ {
+ config = configAdmin.getConfiguration( pid );
+ }
+ config.setBundleLocation( null );
+
+ Hashtable<String, Object> props = new Hashtable<String, Object>();
+ props.put( "prop1", "aValue" );
+ props.put( "prop2", 4711 );
+
+ this.config = config;
+ this.props = props;
+ }
+
+
+ @Override
+ public void doRun()
+ {
+ try
+ {
+ config.update( props );
+ System.out.println( " " + config.getPid() + " - Configuration updated" );
+ }
+ catch ( IOException ioe )
+ {
+ // TODO: log !!
+ }
+ }
+
+
+ @Override
+ public void cleanup()
+ {
+ try
+ {
+ config.delete();
+ }
+ catch ( IOException ioe )
+ {
+ // TODO: log !!
+ }
+ }
+}
\ No newline at end of file
diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryThread.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryThread.java
new file mode 100644
index 0000000..46e8dd1
--- /dev/null
+++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryThread.java
@@ -0,0 +1,113 @@
+/*
+ * 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.felix.cm.integration.helper;
+
+
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.cm.ManagedServiceFactory;
+
+
+/**
+ * The <code>ManagedServiceFactoryThread</code> class is a ManagedServiceFactory
+ * and extends the {@link TestThread} for use in the
+ * {@link org.apache.felix.cm.integration.ConfigUpdateStressTest}.
+ */
+public class ManagedServiceFactoryThread extends TestThread implements ManagedServiceFactory
+{
+
+ private final BundleContext bundleContext;
+
+ private final Hashtable<String, Object> serviceProps;
+
+ private ServiceRegistration service;
+
+ private final ArrayList<Dictionary> configs;
+
+ private boolean configured;
+
+
+ public ManagedServiceFactoryThread( final BundleContext bundleContext, final String pid )
+ {
+ Hashtable<String, Object> serviceProps = new Hashtable<String, Object>();
+ serviceProps.put( Constants.SERVICE_PID, pid );
+
+ this.bundleContext = bundleContext;
+ this.serviceProps = serviceProps;
+ this.configs = new ArrayList<Dictionary>();
+ }
+
+
+ public ArrayList<Dictionary> getConfigs()
+ {
+ synchronized ( configs )
+ {
+ return new ArrayList<Dictionary>( configs );
+ }
+ }
+
+
+ public boolean isConfigured()
+ {
+ return configured;
+ }
+
+
+ @Override
+ public void doRun()
+ {
+ service = bundleContext.registerService( ManagedServiceFactory.class.getName(), this, serviceProps );
+ System.out.println( " " + serviceProps.get( Constants.SERVICE_PID ) + " - ManagedServiceFactory registered" );
+ }
+
+
+ @Override
+ public void cleanup()
+ {
+ if ( service != null )
+ {
+ service.unregister();
+ service = null;
+ }
+ }
+
+
+ public void deleted( String pid )
+ {
+ synchronized ( configs )
+ {
+ configs.add( null );
+ }
+ }
+
+
+ public void updated( String pid, Dictionary properties )
+ {
+ synchronized ( configs )
+ {
+ configs.add( properties );
+ configured = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceThread.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceThread.java
new file mode 100644
index 0000000..ba48a74
--- /dev/null
+++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceThread.java
@@ -0,0 +1,104 @@
+/*
+ * 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.felix.cm.integration.helper;
+
+
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.cm.ManagedService;
+
+
+/**
+ * The <code>ManagedServiceThread</code> class is a ManagedService and extends
+ * the {@link TestThread} for use in the
+ * {@link org.apache.felix.cm.integration.ConfigUpdateStressTest}.
+ */
+public class ManagedServiceThread extends TestThread implements ManagedService
+{
+
+ private final BundleContext bundleContext;
+
+ private final Hashtable<String, Object> serviceProps;
+
+ private ServiceRegistration service;
+
+ private final ArrayList<Dictionary> configs;
+
+ private boolean configured;
+
+
+ public ManagedServiceThread( final BundleContext bundleContext, final String pid )
+ {
+ Hashtable<String, Object> serviceProps = new Hashtable<String, Object>();
+ serviceProps.put( Constants.SERVICE_PID, pid );
+
+ this.bundleContext = bundleContext;
+ this.serviceProps = serviceProps;
+ this.configs = new ArrayList<Dictionary>();
+ }
+
+
+ public ArrayList<Dictionary> getConfigs()
+ {
+ synchronized ( configs )
+ {
+ return new ArrayList<Dictionary>( configs );
+ }
+ }
+
+
+ public boolean isConfigured()
+ {
+ return configured;
+ }
+
+
+ @Override
+ public void doRun()
+ {
+ service = bundleContext.registerService( ManagedService.class.getName(), this, serviceProps );
+ System.out.println( " " + serviceProps.get( Constants.SERVICE_PID ) + " - ManagedService registered" );
+ }
+
+
+ @Override
+ public void cleanup()
+ {
+ if ( service != null )
+ {
+ service.unregister();
+ service = null;
+ }
+ }
+
+
+ public void updated( Dictionary properties )
+ {
+ synchronized ( configs )
+ {
+ configs.add( properties );
+ configured = properties != null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/TestThread.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/TestThread.java
new file mode 100644
index 0000000..8c258fd
--- /dev/null
+++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/TestThread.java
@@ -0,0 +1,74 @@
+/*
+ * 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.felix.cm.integration.helper;
+
+
+/**
+ * The <code>TestThread</code> class is a base helper class for the
+ * {@link org.apache.felix.cm.integration.ConfigUpdateStressTest}. It implements
+ * basic mechanics to be able to run two task at quasi the same time.
+ * <p>
+ * It is not important to have exact timings because running the tests multiple
+ * times and based on low-level Java VM timings thread execution will in the end
+ * be more or less random.
+ */
+abstract class TestThread extends Thread
+{
+ private final Object flag = new Object();
+
+ private volatile boolean notified;
+
+
+ @Override
+ public void run()
+ {
+ synchronized ( flag )
+ {
+ if ( !notified )
+ {
+ try
+ {
+ flag.wait();
+ }
+ catch ( InterruptedException ie )
+ {
+ // TODO: log
+ }
+ }
+ }
+
+ doRun();
+ }
+
+
+ protected abstract void doRun();
+
+
+ public abstract void cleanup();
+
+
+ public void trigger()
+ {
+ synchronized ( flag )
+ {
+ notified = true;
+ flag.notifyAll();
+ }
+ }
+}
\ No newline at end of file
diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/UpdateThreadSignalTask.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/UpdateThreadSignalTask.java
new file mode 100644
index 0000000..dd55aed
--- /dev/null
+++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/UpdateThreadSignalTask.java
@@ -0,0 +1,80 @@
+/*
+ * 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.felix.cm.integration.helper;
+
+
+import junit.framework.TestCase;
+
+
+/**
+ * The <code>UpdateThreadSignalTask</code> class is a special task used by the
+ * {@link org.apache.felix.cm.integration.ConfigurationTestBase#delay} method.
+ * <p>
+ * This task is intended to be added to the update thread schedule and signals
+ * to the tests that all current tasks on the queue have terminated and tests
+ * may continue checking results.
+ */
+public class UpdateThreadSignalTask implements Runnable
+{
+
+ private final Object trigger = new Object();
+
+ private volatile boolean signal;
+
+
+ public void run()
+ {
+ synchronized ( trigger )
+ {
+ signal = true;
+ trigger.notifyAll();
+ }
+ }
+
+
+ public void waitSignal()
+ {
+ synchronized ( trigger )
+ {
+ if ( !signal )
+ {
+ try
+ {
+ trigger.wait( 10 * 1000L ); // seconds
+ }
+ catch ( InterruptedException ie )
+ {
+ // sowhat ??
+ }
+ }
+
+ if ( !signal )
+ {
+ TestCase.fail( "Timed out waiting for the queue to keep up" );
+ }
+ }
+ }
+
+
+ @Override
+ public String toString()
+ {
+ return "Update Thread Signal Task";
+ }
+}