FELIX-1542 add lastModificationTime and lastUpdateTime fields to track
modifications and updates to prevent the UpdateConfiguration task from
updating properties to ManagedService[Factory] if it has already been
updated upon service registration due to the race condition.

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@809194 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationImpl.java b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationImpl.java
index d7067b5..68f9faa 100644
--- a/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationImpl.java
+++ b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationImpl.java
@@ -40,6 +40,51 @@
 class ConfigurationImpl extends ConfigurationBase
 {
 
+    /*
+     * Concurrency note: There is a slight (but real) chance of a race condition
+     * between a configuration update and a ManagedService[Factory] registration.
+     * Per the specification a ManagedService must be called with configuration
+     * or null when registered and a ManagedService must be called with currently
+     * existing configuration when registered. Also the ManagedService[Factory]
+     * must be updated when the configuration is updated.
+     *
+     * Consider now this situation of two threads T1 and T2:
+     *
+     *    T1. create and update configuration
+     *      ConfigurationImpl.update persists configuration and sets field
+     *      Thread preempted
+     *
+     *    T2. ManagedServiceUpdate constructor reads configuration
+     *      Uses configuration already persisted by T1 for update
+     *      Schedules task to update service with the configuration
+     *
+     *    T1. Runs again creating the UpdateConfiguration task with the
+     *         configuration persisted before being preempted
+     *      Schedules task to update service
+     *
+     *    Update Thread:
+     *      Updates ManagedService with configuration prepared by T2
+     *      Updates ManagedService with configuration prepared by T1
+     *
+     * The correct behaviour would be here, that the second call to update
+     * would not take place.
+     *
+     * This concurrency safety is implemented with the help of the
+     * lastModificationTime field updated by the configure(Dictionary) method
+     * when setting the properties field and the lastUpdatedTime field updated
+     * in the Update Thread after calling the update(Dictionary) method of
+     * the ManagedService[Factory] service.
+     *
+     * The UpdateConfiguration task compares the lastModificationTime to the
+     * lastUpdateTime. If the configuration has been modified after being
+     * updated the last time, it is updated in the ManagedService[Factory]. If
+     * the configuration has already been updated since being modified (as in
+     * the case above), the UpdateConfiguration thread does not call the update
+     * method (but still sends the CM_UPDATED event).
+     *
+     * See also FELIX-1542.
+     */
+
     /**
      * The name of a synthetic property stored in the persisted configuration
      * data to indicate that the configuration data is new, that is created but
@@ -80,6 +125,30 @@
      */
     private volatile boolean isDeleted;
 
+    /**
+     * Time of last configuration properties update.
+     * This is intended to be managed by the {@link #configure(Dictionary)}
+     * method when setting the properties field.
+     * <p>
+     * This field is set to the current time upon construction and every time
+     * the {@link #configure(Dictionary)} method sets the {@link #properties}
+     * field.
+     */
+    private volatile long lastModificationTime;
+
+    /**
+     * Time of last submission of the configuration properties to the (or more)
+     * ManagedService[Factory] services this is managed by the update thread
+     * and must solely be handled on the Update Thread.
+     * <p>
+     * This field is initialized to -1 in the constructors and is set to the
+     * current time when the configuration properties are supplied to the
+     * ManagedService[Factory] in the Update Thread.
+     *
+     * @see #lastModificationTime
+     */
+    private volatile long lastUpdatedTime;
+
 
     ConfigurationImpl( ConfigurationManager configurationManager, PersistenceManager persistenceManager,
         Dictionary properties )
@@ -89,6 +158,7 @@
 
         this.factoryPID = ( String ) properties.remove( ConfigurationAdmin.SERVICE_FACTORYPID );
         this.isDeleted = false;
+        this.lastUpdatedTime = -1;
 
         // set the properties internally
         configureFromPersistence( properties );
@@ -103,6 +173,8 @@
         this.factoryPID = factoryPid;
         this.isDeleted = false;
         this.properties = null;
+        this.lastModificationTime = System.currentTimeMillis();
+        this.lastUpdatedTime = -1;
 
         // this is a new configuration object, store immediately unless
         // the new configuration object is created from a factory, in which
@@ -306,6 +378,42 @@
 
 
     /**
+     * Returns the time of the last update of this configuration object. In
+     * particular this is the time at which the properties field has been
+     * set to new values.
+     * <p>
+     * This value may be compared to the {@link #getLastUpdatedTime()} to
+     * decide whether to update the ManagedService[Factory] or not.
+     */
+    long getLastModificationTime()
+    {
+        return lastModificationTime;
+    }
+
+    /**
+     * Returns the time of the last update of this configuration to the
+     * subscribing ManagedService or ManagedServiceFactory. This time may be
+     * compared to the {@link #getLastModificationTime()} to decide whether
+     * the configuration should be updated or not.
+     */
+    long getLastUpdatedTime()
+    {
+        return lastUpdatedTime;
+    }
+
+    /**
+     * Sets the time of the last update of this configuration to the
+     * ManagedService or ManagedServiceFactory subscribed to this configuration.
+     * <p>
+     * This method should only be called from the Update Thread after supplying
+     * the configuration to the ManagedService[Factory].
+     */
+    void setLastUpdatedTime( )
+    {
+        this.lastUpdatedTime = System.currentTimeMillis();
+    }
+
+    /**
      * Returns <code>false</code> if this configuration contains configuration
      * properties. Otherwise <code>true</code> is returned and this is a
      * newly creted configuration object whose {@link #update(Dictionary)}
@@ -364,7 +472,11 @@
             }
         }
 
-        this.properties = newProperties;
+        synchronized ( this )
+        {
+            this.properties = newProperties;
+            this.lastModificationTime = System.currentTimeMillis();
+        }
     }
 
 
diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationManager.java b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationManager.java
index 7890946..57cbe77 100644
--- a/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationManager.java
+++ b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationManager.java
@@ -1064,7 +1064,7 @@
                 Bundle serviceBundle = sr.getBundle();
                 if ( serviceBundle == null )
                 {
-                    log( LogService.LOG_INFO, "ServiceFactory for PID " + pid
+                    log( LogService.LOG_INFO, "Service for PID " + pid
                         + " seems to already have been unregistered, not updating with configuration", null );
                     return;
                 }
@@ -1115,6 +1115,12 @@
             {
                 handleCallBackError( t, sr, config );
             }
+
+            // update the lastUpdatedTime if there is configuration
+            if ( config != null )
+            {
+                config.setLastUpdatedTime();
+            }
         }
 
         public String toString()
@@ -1276,6 +1282,9 @@
                     {
                         handleCallBackError( t, sr, cfg );
                     }
+
+                    // update the lastUpdatedTime
+                    cfg.setLastUpdatedTime();
                 }
             }
         }
@@ -1292,13 +1301,18 @@
 
         private final ConfigurationImpl config;
         private final Dictionary properties;
+        private final long lastModificationTime;
         private final boolean fireEvent;
 
 
         UpdateConfiguration( final ConfigurationImpl config, boolean fireEvent )
         {
             this.config = config;
-            this.properties = config.getProperties( true );
+            synchronized ( config )
+            {
+                this.properties = config.getProperties( true );
+                this.lastModificationTime = config.getLastModificationTime();
+            }
             this.fireEvent = fireEvent;
         }
 
@@ -1307,6 +1321,18 @@
         {
             try
             {
+                // only update configuration if lastModificationTime is
+                // less than lastUpdateTime
+                // (this must be inside the try-catch-finally to ensure
+                // the event is sent regardless of ManagedService[Factory]
+                // update)
+                if ( lastModificationTime < config.getLastUpdatedTime() )
+                {
+                    log( LogService.LOG_DEBUG, "Configuration " + config.getPid()
+                        + " has already been updated, nothing to be done anymore.", null );
+                    return;
+                }
+
                 if ( config.getFactoryPid() == null )
                 {
                     final ServiceReference[] srList = bundleContext.getServiceReferences( ManagedService.class
@@ -1371,6 +1397,9 @@
                             {
                                 bundleContext.ungetService( userRef );
                             }
+
+                            // update the lastUpdatedTime
+                            config.setLastUpdatedTime();
                         }
                     }
                 }
@@ -1439,6 +1468,9 @@
                             {
                                 bundleContext.ungetService( ref );
                             }
+
+                            // update the lastUpdatedTime
+                            config.setLastUpdatedTime();
                         }
                     }
                 }