Broken out from #631340: Better notifications, revision support in FeedsNodeProcessor.inc Garret Albright / velosol:

FeedsNodeProcessor, when "update existing" is enabled, updates existing nodes whether they have changed or not.

Node save operations are expensive, unnecessary ones could be avoided by hashing the feed item and checking against the hash before updateing.

Comments

alex_b’s picture

1. Introduce a protected FeedsNodeProcessor::hash($item) and protected FeedsNodeProcessor::getHash($nid)

2. in FeedsNodeProcessor::process():


public function process(FeedsParserResult $parserResult, FeedsSource $source) {
  // ...
   foreach ($parserResult->value['items'] as $item) {
      // ...
      if (!($nid = $this->existingItemId($item, $source)) || $this->config['update_existing']) {
       
        // Hash the item for comparisons. 
        $hash = $this->hash($item);

        if (!empty($nid)) {          
          if ($hash == $this->getHash($nid)) {
            // No change, nothing to do.
            continue;
          }
          $node->nid = $nid;
          
          $updated++;
        }
        else {
          $created++;
        }
        
        // Init node object here, remove this code from map().
        $node = new stdClass();
        $node->type = $this->config['content_type'];
        $node->feeds_node_item = new stdClass();
        $node->feeds_node_item->hash = $hash; // NEW. Will require change to table / storage to accomodate hash.
        node_object_prepare($node);
        $node->uid = 0;

        // Execute mappings from $item to $node.
        $this->map($item, $node);

        // ...
      }
    }
velosol’s picture

Trying to gather some initial ideas for the additions that need to be made:

Added to feeds.install $schema['feeds_node_item']:

//Snip $schema['feeds_node_item']
  'hash' => array(
        'type' => 'varchar',
        'length' => 32, //The length of the output of md5()
        'not null' => TRUE,
        'default' => '',
        'description' => t('The hash of the item.'),
      ),
//End snip
//An update function?
function feeds_update_600X() {
  $ret = array();

  $spec = array(
      'type' => 'varchar',
      'length' => 32, //The length of the output of md5()
      'not null' => TRUE,
      'default' => '',
      'description' => t('The hash of the item.'),
    );
  db_add_field($ret, 'feeds_node_item', 'hash', $spec);
  return $ret;
}

Hash item function:

protected FeedsNodeProcessor::hash($item) {
  $to_hash = '';
  if(is_array($item)) {
    foreach($item as $val) {
      $to_hash .= $val;
    }
  }
  else {
    return FALSE;
  }
  return hash('md5', $to_hash);
}

getHash function:

protected function FeedsNodeProcessor::getHash($nid) {
  $hash = db_result(db_query('SELECT hash FROM {feeds_node_item} WHERE nid = %d', $nid));
  if ($hash) {
    // Return with the hash
    return $hash;
  }
  return 0; //Or FALSE?
}

With regards to the hash function and database definition - it may be better/faster if we use the second argument of md5() to return the raw output (see: http://us2.php.net/manual/en/function.md5.php ) and treat it as an int.

Thoughts, additions, corrections, requests?

Edit: corrected syntax of update function and made protected func syntax match.
Edit2: reading up on that PHP page suggests that hash('md5', $to_hash) is faster than md5(), adjusting.

alex_b’s picture

This is all looking very good. Adjustments can be done when the patch lands.

velosol’s picture

Status: Needs work » Needs review
StatusFileSize
new3.22 KB
new1.09 KB

[edit: most up-to-date below]
Patch, did someone say patch?

I modified FeedsNodeProcessor::map() as you suggested so that it can modify the $node as a parameter - however it still returns - good functionality?

Edit: I also left node_object_prepare in map() as the include statement for the node module is there and I didn't want to break it out of map.

I also included an update function in install as 6007.

I tested by running update.php then first deleting nodes and then importing again; and then trying to import again, I got a happy "There is no new content." I did NOT test importing over nodes that did not yet have a hash set - but I would assume that they would get one final 'fake' revision.

velosol’s picture

[edit: most up-to-date below]
New patch- the last node_processor.patch was overwriting the $node even if it already existed because $node = new stdClass(); came after $node->nid = $nid when a $nid was returned by existingItemId().

This new one moves the creation of the $node up to where before it's really needed, but is cleaner.

alex_b’s picture

Status: Active » Needs work

1. Patch does not apply cleanly to HEAD (1 out of 3 hunks fails).
2. Minor request: Could you create the patch from the actual feeds/ directory? That'd be easier to apply :)
3. Let's move all the node initialization code from map() into process(), also the node_object_prepare() call. More consistent.
4. No need for commenting on closing brackets } //class FeedsNodeProcessor
5. Why is there a test for is_array() in hash() ? Couldn't we just hash('md5', serialize($item)) ?
6. getHash() should return an empty string if no hash was found, hash() must never return an empty string *). Let's not use 0 as return value as it is a different type than the usual return value. If we make the default value in the DB an empty string, we can keep the code simple.
7. Accordingly, the comparison $hash === $this->getHash($nid) must use the triple equals as to exclude false positives between different types (FALSE, 0, NULL and '')
8. There should also be a test for this behavior. Otherwise, future refactoring of this portion of the code would run a high risk of causing regression.

*)
http://php.net/manual/en/function.md5.php :
md5 on a null string returns null, md5 on an EMPTY string does not return null or an empty string. Rather it returns "d41d8cd98f00b204e9800998ecf8427e"

velosol’s picture

@alex_b:

I have addressed most of the stuff in my working copy of the code - I can post that as a patch if you'd like, but I'd prefer to get some feedback re: my questions in 3 & 5.

1. D'oh! Was patching to alpha7. I can grab stuff from CVS, but it's a huge pain on this Windows box - any chance the -dev/HEAD portion could be tarballed for the front page of Feeds?
2. Will do in future patches, my mistake.
3. Done - all that's left is self::loadMappers() and the parent::map() call - do you want the static $included stuff to live on, or just do the module_load_include() call each time?
4. My bad again, it was in there because I had borked a previous attempt at the patch and didn't want to do it again - I'll take it out before rolling further patches.
5. I'm in the habit of type-checking - and it provided an easy way to return on bad input. If that's not a concern (second half of point 6?), do we even need the overhead of another function call vice:
$hash = hash('md5', serialize($item)); in process()? *
6. Updated getHash() in my working copy to return an empty string when no hash is found. **
7. Done.
8. Definitely! Unfortunately I'm not really great with Drupal's test framework - I'll poke around in other tests and see if I can come up with something, but I expect someone else may be able to do a better job.

* - I can't get hash('md5') nor md5() to ever return NULL (I tried passing, NULL, FALSE, empty strings to each) - only to get the d41...27e hash every time. Beyond that I'm not committed to any particular hash creation method.

** - Forgive my ignorance, but if the query doesn't find anything, the db_result/db_query returns FALSE, not the schema default, right? So we'd still have to catch the FALSE and return ''?

alex_b’s picture

1. Use cygwin http://www.cygwin.com/ - You're a good contributor you should have your tool box in order :-)
3. Leave static in for now. We may want to factor this out in a follow up patch.
5. Drats. I should not have believed what i found in the PHP documentation comments. However, this still works in our favor: hashing an empty item will always return a hash string, while looking up a legacy item will return an empty string or - as you pointed out - looking up a missing item will return FALSE. In these cases, $hash == $this->getHash($nid) will evaluate to false and the item will be updated. Also no need for a triple equals check anymore.
8. :-)

velosol’s picture

Status: Needs review » Needs work
StatusFileSize
new4.74 KB

[edit: most up-to-date below]
Alright, based on the past couple of comments, I've got a new version of the patch (against dev/1.15). The last hunk looks strange (removes and adds the final '}', notes 'No newline at end of file') as I didn't touch that function - but I've left it in for the time being. I've also added to the function header comments to clarify behavior (protected hash always returns a hash, getHash returns hash or empty string).

I'll see what I can do to get a test set up for this functionality.

Is the feeds.install patch from comment #4 OK once this patch is up to snuff?

velosol’s picture

I guess I still need some more coffee!
[edit: see below for bug cause - and fix]
The patch in #9 will update a node, but not create a new revision. I was expecting to get revisions as I had with the patch in #5, but that isn't happening anymore - I think it's because of the $node->vid addition, although I can't pin it down to that.
Sorry I didn't catch this before posting the patch, I had only been checking if the node content was updated, not if the "revisions" link was available.

To get revisions again:

$node->nid = $nid;
$node->vid = db_result(db_result(db_query('SELECT vid FROM {node} WHERE nid = %d', $nid));
$node->revision = TRUE;

Ultimately the question is: What is the "correct" behavior? I think creating a new revision is expected, but that could be just for my use case.

And yes, I realize that this just further highlights the need for tests :) .

alex_b’s picture

I'd say the right behavior is that if "Create revisions" is checked on a content type, FeedsNodeProcessor should create new revisions for nodes of that type.

http://skitch.com/alexbarth/nebw2/feed-item-localhost

alex_b’s picture

.install is looking good except the indentation in the update hook. Could you also roll just one patch for all files?

velosol’s picture

The 'bug' in #10 was my own fault - copy and paste coding fails when the variables change; node_object_prepare() is now called properly and the node's revision status is set based on the setting in the content type (as in #11).

I'm still working towards tests.

The .install indentation is fixed and both .install and FeedsNodeProcessor.inc are in this patch.

alex_b’s picture

Cool. Waiting for the tests for a closer review.

pbuyle’s picture

Some feed format (Atom for instance) provides a field that indicate the last modification time of an item (in Atom, that would be the updated element).

In addition to hashing, wouldn't it be nice to have the option to only update a node only if $node->changed < $item->udpated ?

velosol’s picture

Status: Needs work » Needs review
StatusFileSize
new32.19 KB
new12.3 KB

After a bit of a fight with testing and the core patch, I've got tests written and passing to verify the update-if-changed behavior. The other RSS file I've added (/tests/feeds/developmentseed.rss3) I had to attach separately as my cvs/diff would not incorporate it into the patch.

Please review and let me know if more needs to be done.

@monoglito404 : I believe this patch will take care of the $node->changed < $item->updated case by virtue of hashing. If there's a specific case where hashing wouldn't take care of that use case (or another similar situation) please post back.

alex_b’s picture

StatusFileSize
new48.66 KB

Great. I did a close review and adjusted the following:

- rearranged innards of process() for better legibility (please review to make sure I'm not trampling over assumptions)
- in tests, use setSettings() instead of updateExisting()
- renamed developmentseed.rss3 to developmentseed_changes.rss2
- fixed broken tests in feeds_defaults.test
- added test to feeds_defaults.test node import testing the new import behavior, this required a new test feed nodes_changes.csv
- some other minor cleanups
- I got queasy about editFeedNode() because it has many assumptions, but these are just the same as in createFeedNode(). Let's leave it this way for now, at some point we'll need to abstract this out.

All tests passing. Please give this patch another close look (especially process()); see whether all tests are passing for you. When I get an RTBC I will commit.

FYI - check out cvsdo for adding files to cvs repositories you don't have access to (I know, CVS sucks).

velosol’s picture

I'm having some trouble getting (any) tests to work in my environment right now so I can't really put your patch (17) through its paces. I did however just do some import tests by hand and it seems as though $node->log is cleared by node_object_prepare() - moving the $node->log = "Created/..." below the n_o_p call fixed it.

I will post back once I've got my test environment up and running again and an updated patch.

Edit: I forgot to mention: everything else you did looks great and other than the above, I think process() is good - it had been arranged the same as before with the map() code in place of the map call for consistency, so no 'real' reason. Both edit and createFeedNode could use some cleanup/unification, but I didn't know where to start so I just mimicked the old code. Also, I can't believe I missed setSettings!

alex_b’s picture

Status: Needs review » Needs work

Re: tests: use simpletest 2.9, read its install file before you install your drupal site and follow it religiously. If you still can't get your tests running, please open another issue for this. I'd be interested in helping you fix it. I know tests can be iffy to run and I think it's important to smooth out the creases.

NW because of the $node->log issue.

velosol’s picture

Thank you for the offer of help - it appears as though my issue is #523878: Fatal error: Maximum execution time of 180 seconds exceeded which hasn't yet been backported as it hit HEAD after SimpleTest 2.9 was made. I'll get the patch into my SimpleTest and hope that clears things up. Posting here so others with a similar issue may find the fix - if it still doesn't fix the problem, I will open a new issue.

This is the referenced patch:

+++ modules/simpletest/drupal_web_test_case.php	16 Aug 2009 20:26:48 -0000
@@ -31,7 +31,7 @@
   /**
    * Time limit for the test.
    */
-  protected $timeLimit = 180;
+  protected $timeLimit = 500;
velosol’s picture

StatusFileSize
new49.14 KB

My testing platform is working again, yay! After moving the $node->log as in 18 the RSS import to nodes test was working fine (as before) and no more log error on manual import.

I found errors in Defaults: Node import test and ran the import manually - it was creating 9 Story nodes (incorrect) and also complaining that 'url' didn't have a default value. This happened regardless of location of $node->log. If I added a mapping for (CSV) guid => (DB) url, it imported correctly. I assume the correct behavior is to not have to map anything to 'url', but then Drupal complains about the INSERT queries. I'm leaving NW because of this, but it may not be related.

I also made minor doc updates in feeds_default.test. The updated patch has new files included (thanks for the cvsdo tip!). While the RSS to node test passes, I had been seeing errors on others*.

*:I was getting #587304: Call to undefined function drupal_realpath() on the CVS import to users, Defaults: Node import, and other(?) tests. I made the adjustment as noted in that issue and now everything not already mentioned that I can test is passing (i.e. those tests requiring SimplePie or Data were not run).

alex_b’s picture

Status: Needs work » Fixed

Committed. This is an *awesome* contribution. This is fixing the update option to an extent that makes it a viable option for large feeds.

I could not reproduce the URL error you reported on the node import form. Please open a new issue if it persists.

Thank you.

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

Equinger’s picture

Maybe this is an edge case, but this doesn't work if your feed items don't have timestamps. The reason is that "Feeds" applies the feed timestamp to all the items if there is no timestamp. Reasonable enough, but the hash in this case is guaranteed to be different from the hash value in the node regardless of whether the content is different.

I'm just adding a timestamp to my feed items now, but this is just an FYI.