After Reverting a Feature Module Profile2 Field Values Are Deleted

Error Type: 
Contributed Module
Severity: 
Critical
Origin: 
custom
Description: 

I recently gave an outline of this problem over in the Drupal community here:
https://www.drupal.org/node/1316874#comment-12136170

But I thought it would be interesting to make a more technical post on the subject, so here it is.

Cause: 

If you look in the .info scripts of your feature modules and find something like the following then you are exposed to this problem:

features[profile2_type][] = YOUR_PROFILE_NAME

The reason is that when you revert your feature module there is nothing telling Features how to behave in terms of reverting a Profile2 component, so instead the default hook is used:

entity_features_revert();

Within this function Features looks to see if the Profile2 entity definition defines a "features controller class", but it does not, so instead an instance of EntityDefaultFeaturesController is used.

The revert() method in this class invokes entity_delete_multiple(). At this stage we can see that we have the potential to head into trouble by considering the comments that exist in the node specific hook:

// We don't use node_type_delete() because we do not actually
// want to delete the node type (and invoke hook_node_type()).
// This can lead to bad consequences like CCK deleting field
// storage in the DB.

That fairly accurately sums up what can happen under certain, non-standard conditions.

entity_delete_multiple() retrieves the "controller class" defined in profile2_entity_info() which happens to be "EntityAPIControllerExportable" for the profile2_type entity type.

This class has a delete method, which can, under certain conditions, invoke field_attach_delete_bundle().

Those conditions are very important to understand. First a check is made to detect whether the current operation is attempting to revert an exportable object:
$is_revert = entity_has_status($this->entityType, $entity, ENTITY_IN_CODE);

Crucially the entity_has_status() function has this line of code:
return isset($entity->{$status_key}) && ($entity->{$status_key} & $status) == $status;

Note the single "&", which is a bitwise AND operator.

Under normal operation this is what ought to happen:

  1. You enable the feature module that defines the profile2_type
  2. In the profile_type database table your profile type in question will be assigned these values:
    1. status: 3 (overridden)
    2. module: The name of your feature module, which now effectively "owns" this exportable.
  3. You revert your feature module
  4. Fields are NOT deleted because $is_revert == TRUE meaning this code does not fire:
    if ($hook == 'delete' && !$is_revert) {
            field_attach_delete_bundle($type, $entity->{$this->bundleKey});
    }
  5. Note how the bitwise operator is vital here when assigning to $is_revert. Our current status is Overridden (3)  and not In Code (2) but the bitwise opertaion of 3 & 2 == 2 returns TRUE, correctly assigning TRUE to $is_revert.
  6. In the profile_type database table your profile type in question will be assigned this new value:
    1. status: 2 (in code)

What this tells us is that we will only have our fields deleted when $is_revert === FALSE, which will only be the case when the status property is ENTITY_CUSTOM, meaning the entity is wholly defined in the database, and that will only be the case before the feature module got enabled.

So how can that situation arise?

In the example we have seen there were multiple staging and development instances and a Memcached backend was being used. Unfortunately, after moving from a single instance environment to a multi instance environment no mechanism was in place to ensure that each instance used a unique memcache_key_prefix in settings.php, while each instance did have its own database.

Consequently cached entity config data was bleeding between instances such that during the revert phase the profile2_type entity seemed to have a status property of ENTITY_CUSTOM due to the cached value having been set by another running instance of the site.

As a result field_attach_delete_bundle() would delete all field instances attached to the profile type being reverted and if there were no more field instnances for each field then the field base was also marked for deletion.

If your feature module also defines field bases and field instances your fields will be created again at this point, but the crucial consideration is that your field values are lost and will not re-populate your new field_data tables.

If you were to look in your database at this stage you will pobably see two records in field_config and field_config_instance, one for the originial field that got deleted and one for the new one.

Why is it still there is it has been deleted? Because the actual deletion takes place using a cron task and it could take several cron cycles for the data to be purged and the field_config records to be removed - you will notice that one of the records has a "1" in the "deleted" field.

 

 

 

 

Solution: 

Ensure that multiple instances have unique memcached keys if they also have different databases.

There might also be a case for taking the same approach as is already being taken with nodes - don't rely on the default delete handler, but manually delete the profile2 definition in the database and then insert it again to eliminate the possibility of fields being deleted.

Expedite

Our aim is to add enough detailed information to each of the Drupal error messages that Drupal developers will be able to fix their own problems. This exercise will take some time to complete, at the last count we had almost 700 error messages in the system.

If your site is suffering from this error message please contact us and ask us to expedite the solution to this error and we will do our best to add a solution as soon as possible.