mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

gbellato
04.36.2007 86b33497f6d01df4b1185341d9c468d05a5970ca
opendj-sdk/opends/src/server/org/opends/server/replication/plugin/Historical.java
@@ -26,7 +26,10 @@
 */
package org.opends.server.replication.plugin;
import java.util.ArrayList;
import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.server.messages.MessageHandler.getMessage;
import static org.opends.server.messages.ReplicationMessages.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
@@ -45,6 +48,8 @@
import org.opends.server.types.AttributeType;
import org.opends.server.types.AttributeValue;
import org.opends.server.types.Entry;
import org.opends.server.types.ErrorLogCategory;
import org.opends.server.types.ErrorLogSeverity;
import org.opends.server.types.Modification;
import org.opends.server.types.ModificationType;
@@ -84,11 +89,6 @@
  static final AttributeType entryuuidAttrType =
    DirectoryServer.getSchema().getAttributeType(ENTRYUIDNAME);
  /*
   * The last update seen on this entry, allows fast conflict detection.
   */
  private ChangeNumber moreRecentChangenumber =
                              new ChangeNumber(0,0,(short)0);
  /*
   * contains Historical information for each attribute sorted by attribute type
@@ -108,21 +108,6 @@
  }
  /**
   * Duplicates an Historical Object.
   * attributesInfo are nor duplicated but used as references.
   * @return The duplicate of the Historical Object
   */
  public Historical duplicate()
  {
    Historical dup = new Historical();
    dup.attributesInfo =
                new HashMap<AttributeType,AttrInfoWithOptions>(attributesInfo);
    dup.moreRecentChangenumber = this.moreRecentChangenumber;
    return dup;
  }
  /**
   * Process an operation.
   * This method is responsible for detecting and resolving conflict for
   * modifyOperation. This is done by using the historical information.
@@ -132,200 +117,33 @@
   * @return true if the replayed operation was in conflict
   */
  public boolean replayOperation(ModifyOperation modifyOperation,
                              Entry modifiedEntry)
                                 Entry modifiedEntry)
  {
    boolean bConflict = false;
    List<Modification> mods = modifyOperation.getModifications();
    ChangeNumber changeNumber =
      OperationContext.getChangeNumber(modifyOperation);
    for (Iterator modsIterator = mods.iterator(); modsIterator.hasNext();)
    for (Iterator<Modification> modsIterator = mods.iterator();
         modsIterator.hasNext(); )
    {
      Modification m = (Modification) modsIterator.next();
      Attribute modAttr = m.getAttribute();
      Set<String> options = modAttr.getOptions();
      AttributeType type = modAttr.getAttributeType();
      AttrInfoWithOptions attrInfoWithOptions =  attributesInfo.get(type);
      AttrInfo attrInfo = null;
      if (attrInfoWithOptions != null)
      Modification m = modsIterator.next();
      AttributeInfo attrInfo = getAttrInfo(m);
      if (attrInfo.replayOperation(modsIterator, changeNumber,
                                   modifiedEntry, m))
      {
        attrInfo = attrInfoWithOptions.get(options);
      }
      if (this.hasConflict(attrInfo, changeNumber))
      {
        // We are replaying an operation that was already done
        // on another master server and this operation has a potential
        // conflict
        // with some more recent operations on this same entry
        // we need to take the more complex path to solve them
        switch (m.getModificationType())
        {
          case DELETE:
            if (changeNumber.older(attrInfo.getDeleteTime()))
            {
              /* this delete is already obsoleted by a more recent delete
              * skip this mod
              */
              modsIterator.remove();
              break;
            }
            this.conflictDelete(changeNumber,
                                type, m, modifiedEntry, attrInfo, modAttr);
            break;
          case ADD:
            this.conflictAdd(modsIterator, changeNumber, attrInfo,
                             modAttr.getValues(), modAttr.getOptions());
            break;
          case REPLACE:
            if (changeNumber.older(attrInfo.getDeleteTime()))
            {
              /* this replace is already obsoleted by a more recent delete
              * skip this mod
              */
              modsIterator.remove();
              break;
            }
            /* save the values that are added by the replace operation
            * into addedValues
            * first process the replace as a delete operation -> this generate
            * a list of values that should be kept
            * then process the addedValues as if they were coming from a add
            * -> this generate the list of values that needs to be added
            * concatenate the 2 generated lists into a replace
            */
            LinkedHashSet<AttributeValue> addedValues = modAttr.getValues();
            modAttr.setValues(new LinkedHashSet<AttributeValue>());
            this.conflictDelete(changeNumber, type, m, modifiedEntry,
                                attrInfo, modAttr);
            LinkedHashSet<AttributeValue> keptValues = modAttr.getValues();
            this.conflictAdd(modsIterator, changeNumber, attrInfo, addedValues,
                             modAttr.getOptions());
            keptValues.addAll(addedValues);
            break;
          case INCREMENT:
            // TODO : FILL ME
            break;
        }
        bConflict = true;
      }
      else
      {
        processLocalOrNonConflictModification(changeNumber, m);
      }
    }
    // TODO : now purge old historical information
    if (moreRecentChangenumber == null ||
         moreRecentChangenumber.older(changeNumber))
    {
      moreRecentChangenumber = changeNumber;
    }
    return bConflict;
  }
  /**
   * This method calculate the historical information and update the hist
   * attribute to store the historical information for modify operation that
   * does not conflict with previous operation.
   * This is the usual path and should therefore be optimized.
   *
   * It does not check if the operation to process is conflicting or not with
   * previous operations. The caller is responsible for this.
   *
   * @param changeNumber The changeNumber of the operation to process
   * @param mod The modify operation to process.
   */
  private void processLocalOrNonConflictModification(ChangeNumber changeNumber,
      Modification mod)
  {
    /*
     * The operation is either a non-conflicting operation or a local
     * operation so there is no need to check the historical information
     * for conflicts.
     * If this is a local operation, the this code is run during
     * the pre-operation phase (TODO : should make sure that replication
     * is always run after all other plugins)
     * If this is a non-conflicting replicated operation, this code is run
     * during the handleConflictResolution().
     */
    Attribute modAttr = mod.getAttribute();
    if (modAttr.getAttributeType().equals(historicalAttrType))
    {
      return;
    }
    Set<String> options = modAttr.getOptions();
    AttributeType type = modAttr.getAttributeType();
    AttrInfoWithOptions attrInfoWithOptions =  attributesInfo.get(type);
    AttrInfo attrInfo;
    if (attrInfoWithOptions != null)
    {
      attrInfo = attrInfoWithOptions.get(options);
    }
    else
    {
      attrInfo = null;
    }
    /*
     * The following code only works for multi-valued attributes.
     * TODO : See impact of single valued attributes
     */
    if (attrInfo == null)
    {
      attrInfo = new AttrInfo();
      if (attrInfoWithOptions == null)
      {
        attrInfoWithOptions = new AttrInfoWithOptions();
      }
      attrInfoWithOptions.put(options, attrInfo);
      attributesInfo.put(type, attrInfoWithOptions);
    }
    switch (mod.getModificationType())
    {
    case DELETE:
      if (modAttr.getValues().isEmpty())
      {
        attrInfo.delete(changeNumber);
      }
      else
      {
        attrInfo.delete(modAttr.getValues(), changeNumber);
      }
      break;
    case ADD:
      if (type.isSingleValue())
      {
        attrInfo.delete(changeNumber);
      }
      attrInfo.add(modAttr.getValues(), changeNumber);
      break;
    case REPLACE:
      /* TODO : can we replace specific attribute values ????? */
      attrInfo.delete(changeNumber);
      attrInfo.add(modAttr.getValues(), changeNumber);
      break;
    case INCREMENT:
      /* FIXME : we should update ChangeNumber */
      break;
    }
  }
  /**
   * Append replacement of state information to a given modification.
   *
   * @param modifyOperation the modification.
   */
  public void generateState(ModifyOperation modifyOperation)
@@ -346,12 +164,9 @@
    {
      for (Modification mod : mods)
      {
        processLocalOrNonConflictModification(changeNumber, mod);
      }
      if (moreRecentChangenumber == null ||
           moreRecentChangenumber.older(changeNumber))
      {
        moreRecentChangenumber = changeNumber;
        AttributeInfo attrInfo = getAttrInfo(mod);
        if (attrInfo != null)
          attrInfo.processLocalOrNonConflictModification(changeNumber, mod);
      }
    }
@@ -364,6 +179,48 @@
  }
  /**
   * Get the AttrInfo for a given Modification.
   * The AttrInfo is the object that is used to store the historical
   * information of a given attribute type.
   * If there is no historical information for this attribute yet, a new
   * empty AttrInfo is created and returned.
   *
   * @param mod The Modification that must be used.
   * @return The AttrInfo corresponding to the given Modification.
   */
  private AttributeInfo getAttrInfo(Modification mod)
  {
    Attribute modAttr = mod.getAttribute();
    if (modAttr.getAttributeType().equals(Historical.historicalAttrType))
    {
      // Don´t keep historical information for the attribute that is
      // used to store the historical information.
      return null;
    }
    Set<String> options = modAttr.getOptions();
    AttributeType type = modAttr.getAttributeType();
    AttrInfoWithOptions attrInfoWithOptions =  attributesInfo.get(type);
    AttributeInfo attrInfo;
    if (attrInfoWithOptions != null)
    {
      attrInfo = attrInfoWithOptions.get(options);
    }
    else
    {
      attrInfoWithOptions = new AttrInfoWithOptions();
      attributesInfo.put(type, attrInfoWithOptions);
      attrInfo = null;
    }
    if (attrInfo == null)
    {
      attrInfo = AttributeInfo.createAttributeInfo(type);
      attrInfoWithOptions.put(options, attrInfo);
    }
    return attrInfo;
  }
  /**
   * Encode the historical information in an operational attribute.
   * @return The historical information encoded in an operational attribute.
   */
@@ -376,16 +233,17 @@
    {
      AttributeType type = entryWithOptions.getKey();
      HashMap<Set<String> , AttrInfo> attrwithoptions =
      HashMap<Set<String> , AttributeInfo> attrwithoptions =
                                entryWithOptions.getValue().getAttributesInfo();
      for (Map.Entry<Set<String>, AttrInfo> entry : attrwithoptions.entrySet())
      for (Map.Entry<Set<String>, AttributeInfo> entry :
           attrwithoptions.entrySet())
      {
        boolean delAttr = false;
        Set<String> options = entry.getKey();
        String optionsString = "";
        AttrInfo info = entry.getValue();
        ChangeNumber deleteTime = info.getDeleteTime();
        AttributeInfo info = entry.getValue();
        if (options != null)
        {
@@ -398,6 +256,7 @@
          optionsString = optionsBuilder.toString();
        }
        ChangeNumber deleteTime = info.getDeleteTime();
        /* generate the historical information for deleted attributes */
        if (deleteTime != null)
        {
@@ -428,9 +287,18 @@
            }
            else
            {
              strValue = type.getNormalizedPrimaryName() + optionsString + ":" +
              valInfo.getValueUpdateTime().toString() +
              ":add:" + valInfo.getValue().toString();
              if (valInfo.getValue() == null)
              {
                strValue = type.getNormalizedPrimaryName() + optionsString
                           + ":" + valInfo.getValueUpdateTime().toString() +
                           ":add";
              }
              else
              {
                strValue = type.getNormalizedPrimaryName() + optionsString
                           + ":" + valInfo.getValueUpdateTime().toString() +
                           ":add:" + valInfo.getValue().toString();
              }
            }
            AttributeValue val = new AttributeValue(historicalAttrType,
@@ -463,286 +331,6 @@
    return attr;
  }
  /**
   * Detect if a new change has a potential conflict with previous changes.
   * Must be blinding fast
   * @param info the historical information for the attribute that
   * was modified
   * @param newChange the ChangeNumber of the change for which we need to check
   *        for potential conflict
   * @return true if there is a potential conflict, false otherwise
   */
  private boolean hasConflict(AttrInfo info, ChangeNumber newChange)
  {
    // if I've already seen a change that is more recent than the one
    // that is currently being processed, then there is
    // a potential conflict
    if (ChangeNumber.compare(newChange, moreRecentChangenumber) <= 0)
    {
      if (info == null)
      {
        return false;   // the attribute was never modified -> no conflict
      }
      else if (ChangeNumber.compare(newChange, info.getLastUpdateTime()) <= 0)
      {
        return true; // the attribute was modified after this change -> conflict
      }
      else
      {
        return false;// the attribute was not modified more recently
      }
    }
    else
    {
      return false;
    }
  }
  /**
   * Process a delete attribute values that is conflicting with a previous
   * modification.
   *
   * @param changeNumber The changeNumber of the currently processed change
   * @param type attribute type
   * @param m the modification that is being processed
   * @param modifiedEntry the entry that is modified (before current mod)
   * @param attrInfo the historical info associated to the entry
   * @param modAttr the attribute modification
   * @return false if there is nothing to do
   */
  private boolean conflictDelete(ChangeNumber changeNumber,
                                AttributeType type, Modification m,
                                Entry modifiedEntry,
                                AttrInfo attrInfo, Attribute modAttr )
  {
    LinkedHashSet<AttributeValue> delValues;
    LinkedHashSet<AttributeValue> replValues;
    /*
     * We are processing a conflicting DELETE modification
     *
     * This code is written on the assumption that conflict are
     * rare. We therefore don't care much about the performance
     * However since it is rarely executed this code needs to be
     * as simple as possible to make sure that all paths are tested.
     * In this case the most simple seem to change the DELETE
     * in a REPLACE modification that keeps all values
     * more recent that the DELETE.
     * we are therefore going to change m into a REPLACE that will keep
     * all the values that have been updated after the DELETE time
     * If a value is present in the entry without any state information
     * it must be removed so we simply ignore them
     */
    delValues = modAttr.getValues();
    if ((delValues == null) || (delValues.isEmpty()))
    {
      /*
       * We are processing a DELETE attribute modification
       */
      m.setModificationType(ModificationType.REPLACE);
      replValues = new LinkedHashSet<AttributeValue>();
      for (Iterator it = attrInfo.getValuesInfo().iterator();
           it.hasNext();)
      {
        ValueInfo valInfo; valInfo = (ValueInfo) it.next();
        if (changeNumber.older(valInfo.getValueUpdateTime()))
        {
          /*
           * this value has been updated after this delete, therefore
           * this value must be kept
           */
          replValues.add(valInfo.getValue());
        }
        else
        {
          /*
           * this value is going to be deleted, remove it from historical
           * information unless it is a Deleted attribute value that is
           * more recent than this DELETE
           */
          if (changeNumber.newerOrEquals(valInfo.getValueDeleteTime()))
          {
            it.remove();
          }
        }
      }
      modAttr.setValues(replValues);
      if (changeNumber.newer(attrInfo.getDeleteTime()))
      {
        attrInfo.setDeleteTime(changeNumber);
      }
      if (changeNumber.newer(attrInfo.getLastUpdateTime()))
      {
        attrInfo.setLastUpdateTime(changeNumber);
      }
    }
    else
    {
      /*
       * we are processing DELETE of some attribute values
       */
      ArrayList<ValueInfo> valuesInfo = attrInfo.getValuesInfo();
      for (Iterator<AttributeValue> delValIterator = delValues.iterator();
           delValIterator.hasNext();)
      {
        AttributeValue val = delValIterator.next();
        Boolean deleteIt = true;  // true if the delete must be done
        /* update historical information */
        ValueInfo valInfo = new ValueInfo(val, null, changeNumber);
        int index = valuesInfo.indexOf(valInfo);
        if (index != -1)
        {
          /* this value already exist in the historical information */
          ValueInfo oldValInfo  = valuesInfo.get(index);
          if (changeNumber.newer(oldValInfo.getValueDeleteTime()) &&
              changeNumber.newer(oldValInfo.getValueUpdateTime()))
          {
            valuesInfo.remove(index);
            valuesInfo.add(valInfo);
          }
          else if (oldValInfo.isUpdate())
          {
            deleteIt = false;
          }
        }
        else
        {
          valuesInfo.add(valInfo);
        }
        /* if the attribute value is not to be deleted
         * or if attribute value is not present suppress it from the
         * mod to make sure the delete is going to process again
         */
        modifiedEntry.getAttribute(type);
        if (!deleteIt
            || !modifiedEntry.hasValue(type, modAttr.getOptions(), val))
        {
          delValIterator.remove();
        }
      }
      if (changeNumber.newer(attrInfo.getLastUpdateTime()))
      {
        attrInfo.setLastUpdateTime(changeNumber);
      }
    }
    return true;
  }
  /**
   * Process a add attribute values that is conflicting with a previous
   * modification.
   *
   * @param modsIterator iterator on the list of modification
   * @param changeNumber  the historical info associated to the entry
   * @param attrInfo the historical info associated to the entry
   * @param addValues the values that are added
   * @param options the options that are added
   * @return false if operation becomes empty and must not be processed
   */
  private boolean conflictAdd(Iterator modsIterator, ChangeNumber changeNumber,
                          AttrInfo attrInfo,
                          LinkedHashSet<AttributeValue> addValues,
                          Set<String> options)
  {
    /* if historicalattributedelete is newer forget this mod
     *   else find attr value
     *     if does not exist
     *           add historicalvalueadded timestamp
     *           add real value in entry
     *     else if timestamp older and already was historicalvalueadded
     *        update historicalvalueadded
     *     else if timestamp older and was historicalvaluedeleted
     *        change historicalvaluedeleted into historicalvalueadded
     *        add value in real entry
     */
    if (changeNumber.older(attrInfo.getDeleteTime()))
    {
      /* A delete has been done more recently than this add
       * forget this MOD ADD
       */
      modsIterator.remove();
      return false;
    }
    for (Iterator<AttributeValue> valIterator = addValues.iterator();
         valIterator.hasNext();)
    {
      AttributeValue addVal= valIterator.next();
      ArrayList<ValueInfo> valuesInfo = attrInfo.getValuesInfo();
      ValueInfo valInfo = new ValueInfo(addVal, changeNumber, null);
      int index = valuesInfo.indexOf(valInfo);
      if (index == -1)
      {
        /* this values does not exist yet
         * add it in the historical information
         * let the operation process normally
         */
        valuesInfo.add(valInfo);
      }
      else
      {
        ValueInfo oldValueInfo = valuesInfo.get(index);
        if  (oldValueInfo.isUpdate())
        {
          /* if the value is already present
           * check if the updateTime must be updated
           * in all cases suppress this value from the value list
           * as it is already present in the entry
           */
          if (changeNumber.newer(oldValueInfo.getValueUpdateTime()))
          {
            valuesInfo.remove(index);
            valuesInfo.add(valInfo);
          }
          valIterator.remove();
        }
        else
        {
          /* this value is marked as a deleted value
           * check if this mod is more recent the this delete
           */
          if (changeNumber.newer(oldValueInfo.getValueDeleteTime()))
          {
            /* this add is more recent,
             * remove the old delete historical information
             * and add our more recent one
             * let the operation process
             */
            valuesInfo.remove(index);
            valuesInfo.add(valInfo);
          }
          else
          {
            /* the delete that is present in the historical information
             * is more recent so it must win,
             * remove this value from the list of values to add
             * don't update the historical information
             */
            valIterator.remove();
          }
        }
      }
    }
    if (addValues.isEmpty())
    {
      modsIterator.remove();
    }
    if (changeNumber.newer(attrInfo.getLastUpdateTime()))
    {
      attrInfo.setLastUpdateTime(changeNumber);
    }
    return true;
  }
  /**
   * read the historical information from the entry attribute and
@@ -756,7 +344,7 @@
    Historical histObj = new Historical();
    AttributeType lastAttrType = null;
    Set<String> lastOptions = new HashSet<String>();
    AttrInfo attrInfo = null;
    AttributeInfo attrInfo = null;
    AttrInfoWithOptions attrInfoWithOptions = null;
    if (hist == null)
@@ -764,89 +352,70 @@
      return histObj;
    }
    for (Attribute attr : hist)
    try
    {
      for (AttributeValue val : attr.getValues())
      for (Attribute attr : hist)
      {
        HistVal histVal = new HistVal(val.getStringValue());
        AttributeType attrType = histVal.getAttrType();
        Set<String> options = histVal.getOptions();
        ChangeNumber cn = histVal.getCn();
        AttributeValue value = histVal.getAttributeValue();
        HistKey histKey = histVal.getHistKey();
        if (attrType == null)
        for (AttributeValue val : attr.getValues())
        {
          /*
           * This attribute is unknown from the schema
           * Just skip it, the modification will be processed but no
           * historical information is going to be kept.
           * TODO : recovery tool should deal with this, add some logging.
           */
          continue;
        }
          HistVal histVal = new HistVal(val.getStringValue());
          AttributeType attrType = histVal.getAttrType();
          Set<String> options = histVal.getOptions();
          ChangeNumber cn = histVal.getCn();
          AttributeValue value = histVal.getAttributeValue();
          HistKey histKey = histVal.getHistKey();
        /* if attribute type does not match we create new
         *   AttrInfoWithOptions and AttrInfo
         *   we also add old AttrInfoWithOptions into histObj.attributesInfo
         * if attribute type match but options does not match we create new
         *   AttrInfo that we add to AttrInfoWithOptions
         * if both match we keep everything
         */
        if (attrType != lastAttrType)
        {
          attrInfo = new AttrInfo();
          attrInfoWithOptions = new AttrInfoWithOptions();
          attrInfoWithOptions.put(options, attrInfo);
          histObj.attributesInfo.put(attrType, attrInfoWithOptions);
          lastAttrType = attrType;
          lastOptions = options;
        }
        else
        {
          if (!options.equals(lastOptions))
          if (attrType == null)
          {
            attrInfo = new AttrInfo();
            /*
             * This attribute is unknown from the schema
             * Just skip it, the modification will be processed but no
             * historical information is going to be kept.
             * TODO : REPAIR tool should deal with this, add some logging.
             */
            continue;
          }
          /* if attribute type does not match we create new
           *   AttrInfoWithOptions and AttrInfo
           *   we also add old AttrInfoWithOptions into histObj.attributesInfo
           * if attribute type match but options does not match we create new
           *   AttrInfo that we add to AttrInfoWithOptions
           * if both match we keep everything
           */
          if (attrType != lastAttrType)
          {
            attrInfo = AttributeInfo.createAttributeInfo(attrType);
            attrInfoWithOptions = new AttrInfoWithOptions();
            attrInfoWithOptions.put(options, attrInfo);
            histObj.attributesInfo.put(attrType, attrInfoWithOptions);
            lastAttrType = attrType;
            lastOptions = options;
          }
        }
        if (histObj.moreRecentChangenumber.older(cn))
        {
          histObj.moreRecentChangenumber = cn;
        }
        switch (histKey)
        {
        case ADD:
          if (value != null)
          else
          {
            attrInfo.add(value, cn);
            if (!options.equals(lastOptions))
            {
              attrInfo = AttributeInfo.createAttributeInfo(attrType);
              attrInfoWithOptions.put(options, attrInfo);
              lastOptions = options;
            }
          }
        break;
        case DEL:
          if (value != null)
          {
            attrInfo.delete(value, cn);
          }
        break;
        case REPL:
          attrInfo.delete(cn);
          if (value != null)
          {
            attrInfo.add(value, cn);
          }
        break;
        case DELATTR:
          attrInfo.delete(cn);
        break;
          attrInfo.load(histKey, value, cn);
        }
      }
    } catch (Exception e)
    {
      // Any exception happening here means that the coding of the hsitorical
      // information was wrong.
      // Log an error and continue with an empty historical.
      int    msgID   = MSGID_BAD_HISTORICAL;
      String message = getMessage(msgID, entry.getDN().toString());
      logError(ErrorLogCategory.SYNCHRONIZATION,
               ErrorLogSeverity.SEVERE_ERROR,
               message, msgID);
    }
    /* set the reference to the historical information in the entry */