Problem/Motivation

I've been experiencing several issues with the static cache of fields/properties of the Entity metadata wrappers lately in contrib and several projects.

Basically, I found out that the following sequence would fail:
1) Set a field/property value using the wrapper
2) Access the value with the wrapper (i.e calling $wrapper->foo->value() will cause $wrapper->cache['foo'] to be populated).
3) Call a function that accepts the unwrapped entity and sets the value of the same property/field that was set in 1 ($wrapper->foo again).
4) $wrapper->foo->value() is now stale unless you explicitly call $wrapper->foo->clear().

See below a concrete failing example with a Drupal Commerce order entity:

The order is loaded from DB and has already an order total set, I then wrap the order, set its total, access it, call commerce_order_calculate_total() that accepts the unwrapped order and wrap it (the ->commerce_order_total field is recalculated there), then I access $order_wrapper->commerce_order_total->amount->value() right after, which is now stale.

Proposed resolution

Haven't thought about a resolution yet.

Remaining tasks

  1. Write a test to demonstrate the failure
  2. Fix it
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

jsacksick created an issue. See original summary.

jsacksick’s picture

Issue summary: View changes
jsacksick’s picture

Attaching a failing test.

Status: Needs review » Needs work

The last submitted patch, 3: entity-emw-static-cache-issue-2927615-3.fail_.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

jsacksick’s picture

Assigned: jsacksick » Unassigned
mglaman’s picture

This test definitely helps identify the broken references whenever and entity is rewrapped.

However it definitely brings to question: do we fix, or is this a developer bug where they need to ensure they do not rewrap an object.

jsacksick’s picture

Since each call to entity_metadata_wrapper() will return a different object, updating a property|field of the second wrapper won't clear the static cache of the first wrapper.

We could probably fix this by adding a static cache to entity_metadata_wrapper() itself (although that would technically work only for saved entities that have IDS).

mglaman’s picture

This is actually hard to fix. Or, at least elegantly.

* First the data can be null, $data = NULL.
* Second, the $type can be "entity" or a type of entity (which populates the entity info. That means the entity info can't be reliably used
* The data can be an entity ID or object.

This snippet makes the test pass.

    $entity_wrapper_cache =& drupal_static(__FUNCTION__, array());
    if (!is_object($data)) {
      $wrapper = new EntityDrupalWrapper($type, $data, $info);
    }
    else {
      if (isset($entity_wrapper_cache[$type][spl_object_hash($data)])) {
        $wrapper = $entity_wrapper_cache[$type][spl_object_hash($data)];
      }
      else {
        $wrapper = new EntityDrupalWrapper($type, $data, $info);
        $entity_wrapper_cache[$type][spl_object_hash($data)] = $wrapper;
      }
    }

    return $wrapper;

We have to check if the data is an object. If it's not, there is no chance to re-use a metadata wrapper. Or I guess there is. "If type is a valid entity type, assume this is an identifier and look up from wrapper caches."

Since $entity_info may not be defined (due to type == 'entity') we cannot use `entity keys['id']`. Or. Again, I guess we could with the assumption that "well you passed type === entity, so no benefits for you"

mglaman’s picture

Status: Needs work » Needs review
FileSize
2.97 KB

Here is a patch which intends to improve the `entity_metadata_wrapper` function determination of what wrapper to return.

If the type is "entity" it is much more explicit.

  if ($type == 'entity') {
    return new EntityDrupalWrapper($type, $data, $info);
  }

If a valid entity type is passed we attempt to re-use wrappers.

Status: Needs review » Needs work

The last submitted patch, 9: 2927615-9.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

jsacksick’s picture

Status: Needs work » Needs review
FileSize
3.52 KB

Here's a new patch, I use the hash from spl_object_hash() as the key for the static cache in case the entity passed to entity_metadata_wrapper() is new (i.e no ID), I did several tests and it seems to work as expected, and tests are passing.

I'm not 100% sure spl_object_hash() is reliable though but in case the hash returned for the second call to entity_metadata_wrapper() with the same entity is different, the end result would be similar to with the current codebase (i.e wrapper object returned would be different than the previous call).

mglaman’s picture

+++ b/entity.module
@@ -1407,16 +1407,49 @@ function entity_get_extra_fields_controller($type = NULL) {
+        // Fallback to the object hash in case we couldn't get the entity ID.
+        $cache_key = spl_object_hash($data);

I think this works. It's better to catch than not catch.

I'd also like to do some profiling on this before and after.

We should check object count and memory usage.

mglaman’s picture

Assigned: Unassigned » mglaman
Status: Needs review » Needs work

Discussed with jsacksick offline.

We need to increase the test coverage to map all of our assumptions and outcomes of using this static cache. For example a generic "entity" type is not statically cached. Also that the `spl_object_hash` works with unsaved entities, which could be crucial in the order refresh process of Commerce (possibly.)

mglaman’s picture

Status: Needs work » Needs review
FileSize
4.75 KB

This tests an unsaved node using the same pattern - modifying the title and rewrapping.

mglaman’s picture

Updated to test that creating a wrapper from an entity's ID will return any previous entity object's wrapper.

jsacksick’s picture

Status: Needs review » Needs work

I've been experiencing issues with the patch while working on Commerce Discount.

Some logic in commerce_cart_order_refresh() is relying on the current behavior (i.e returning two different object instances of the wrapper) which probably means it's too risky to commit it as it is right now.

There's a comparison performed on the line item's unit price (we compare the current line item's unit price with the unit price after the line item was manipulated in the product pricing rules).

The product line item is re-wrapped after being passed to the product pricing rules. In that specific edge case, we actually want a different wrapper object instance returned, otherwise it's causing the unit price comparison to fail causing the line item not to be saved after its price has changed...

czigor’s picture

Another idea to solve this without breaking BC: expose a cache clearing API on entities and use that wherever needed (e.g. rules). (Currently $wrapped_entity->clear() also clears the data property, not just the cache.)

czigor’s picture

mglaman’s picture

Assigned: mglaman » Unassigned
Alex Bukach’s picture