Problem/Motivation

As a result of working on #2484105: Optional d6_user_picture_file dependency in d6_user migration is not, in fact, optional, I discovered that, if it cannot resolve a destination ID, the Migration process plugin will throw a SkipRowException as a last resort, thus causing the entire row to be skipped. @mikeryan and I agreed that this is overzealous behavior.

In general, process plugins are allowed to cause entire row skips. In doing this, they're overstepping their boundaries -- process plugins are intended to deal with individual properties, not entire rows. They need be able to signal when a property should be skipped, but not reject an entire row. Except for the skip_row_on_empty and skip_row_if_not_set plugins, deciding whether or not to skip a row should be the sole responsibility of the destination plugins.

Proposed Resolution

Replace MigrateSkipProcessException with two new exception types:

  • MigrateFailProcessException can be thrown when a process plugin encounters an error and needs to abort the processing pipeline. Since this exception represents an error condition, MigrateExecutable should log it, and the row should be given information about the property which caused the failure, so that the destination plugin can act appropriately. This requires handling the list of failed properties in the Row class.
  • MigrateFinishProcessException should be thrown when a process plugin wishes to prevent further processing of the value. This also aborts the process pipeline but it is a success condition, not an error, and is not logged anywhere.

Remaining Tasks

  1. Add two new exceptions and remove MigrateSkipProcessException
  2. Add getters and setters in Row for fallen properties
  3. Alter process plugins and their tests to use these new exceptions

API Changes

MigrateSkipProcessException will be replaced by the two new exceptions described above and Row will get new methods.

UI Changes

Not applicable.

Comments

phenaproxima’s picture

Status: Active » Needs review
StatusFileSize
new512 bytes

A patch so simple and tiny, it's downright adorable.

phenaproxima’s picture

StatusFileSize
new631 bytes

Added explicit return statement.

phenaproxima’s picture

StatusFileSize
new634 bytes

At @mikeryan's request, changed Migration::transform()'s default return value to an empty array.

The last submitted patch, 1: 2487568-1.patch, failed testing.

The last submitted patch, 2: 2487568-2.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 3: 2487568-3.patch, failed testing.

chx’s picture

To sum up today's discussions: it was suggested to return empty if the lookup fails.

But if a required subthing in a thing (like a field in an entity) has a default then empty will get overridden by default. This very well might not be what we want -- we just changed the value of a subthing which might not meet user expectations at all. The decision at the time was to better not a thing at all than have subtly altered things.

We do not have in band signaling for saying "hey destination, we don't have an idea of what this thing is, you probably want to skip it or tell the user it is missing or something".

So we went with out of band signaling ie throwing an exception.

To sum up: the problem here is of user expectations. What stands out more? Missing things or changed things? What is easier to fix? I do not know. I am just asking the questions.

mikeryan’s picture

Let's look closely at the scenario where this happens:

  1. A field in a migration is a reference to an item being imported by another migration.
  2. The incoming reference cannot be resolved to an already-migrated item. This may be because there is a) a dangling reference in the source data, or b) we have chicken-and-egg issues where the target item exists in the source but has not yet been migrated.
  3. Some (content) migrations support stubbing in these cases - we create a stub item, and if b) applies it will eventually be overwritten by the real target item and all is well. If a) applies, we're left with stub items in the database, which hopefully are easily identifiable (on the contrib side, we should think about tools for identifying and removing unresolved stubs).
  4. If the incoming reference could not be resolved, and the destination does not support stubbing, we fall through.

In Migrate 2, when we fell through we simply returned an empty value - and that was generally fine. The main problem in practice was being slow to notice missing references, and being somewhat tedious to track them down. That can be helped by logging something like "Failure to resolve source ID 123 in field_related_article" to the migration message table.

In D8 the scenario is a little more serious, though. Now we're migrating configuration as well as content, which is both non-stubbable and more likely to give problems if omitted. Although, how often does one deal with references to configuration? I'm having trouble thinking of use cases other than text formats (which is where this trouble began) and roles, and in those cases I think we can fall through to a sensible default.

Regardless, the scope of the processing pipeline is the migration of a single field, and that is not the place to decide to scrap the entire entity being migrated. The place to do that would be the destination plugin - more particularly, I would hope that when we ultimately call entity_save() (or whatever core API is appropriate), it would fully validate the object we're giving it and throw an exception if anything is missing or invalid - ideally migration wouldn't have to do its own validation. Whether the core APIs do that consistently is an open question that needs investigation.

Back to the issue at hand - given the scope of the processing pipeline, I believe the necessary changes to the migration plugin itself are simply:

  1. Log the situation to the message table
  2. Return an empty value instead of throwing SkipRow (leaving it to the migration itself whether to apply a default value).

However, at the very least it seems we have tests that were depending on that SkipRow, and perhaps there are other components of migrate/migrate_drupal that need to account for the empty values, so there is still some work to go here.

chx’s picture

Continuing on #7 based on the problems raised by #8 -- if what we need is detailed signalling from process to destination why don't we beef up Row to maintain a list of destination properties where the processing has failed and let destination work from that? Destination can add a default or throw away the thing as it fits. There's more than just Migration, StaticMap can also fail for example.

phenaproxima’s picture

Priority: Normal » Major

Upgrading this issue to major.

+1 @chx's idea in #9. I love the fact that it gives the destination plugin the latitude to use complex logic to fill in the gaps left by the processing pipeline. It also gives the destination plugin the opportunity to do its own validation, beyond whatever core does (or fails to do).

chx’s picture

The next step is retitleing the issue to remove MigrateSkipRowException ; to change every instance to a new MigrateFailedProcessException ; to catch this and add the info to Row via a new method and to change the affected destinations to act sensibly. I would also rename MigrateSkipProcessException to MigrateFinishedProcessException and carefully change the current calls to either MigrateFinishedProcessException or MigrateFailedProcessException .

phenaproxima’s picture

Priority: Major » Normal

to change every instance to a new MigrateFailedProcessException

There is already a MigrateSkipProcessException for precisely this purpose -- can't we just use that?

phenaproxima’s picture

Title: The Migration process plugin should not skip the row on failure » Remove MigrateSkipRowException

Re-titling.

phenaproxima’s picture

Does this deprecate the skip_row_if_not_set and skip_row_on_empty plugins? I'm not sure it does, or should.

chx’s picture

> There is already a MigrateSkipProcessException for precisely this purpose -- can't we just use that?
> skip_row_if_not_set and skip_row_on_empty plugins

Skipping the rest of process might happen because it's just not relevant (success) or because of failure. The current skip process exception doesn't make this distinction. The plugins probably should stay but they should get a fail or somesuch configuration option so that if a skip occurs then we throw the fail exception and consequently record a failure .

phenaproxima’s picture

Title: Remove MigrateSkipRowException » Process plugins should not be allowed to skip rows
Issue summary: View changes
chx’s picture

Issue summary: View changes
phenaproxima’s picture

Issue summary: View changes
phenaproxima’s picture

Issue summary: View changes
benjy’s picture

Issue summary sounds great, +1 on that approach.

Just to clarify, "abort the processing pipeline" is referring to the pipeline for the one field right? Not the entire migration.

phenaproxima’s picture

Status: Needs work » Needs review
StatusFileSize
new15.49 KB

Here's an initial patch, with tests.

Status: Needs review » Needs work

The last submitted patch, 21: 2487568-21.patch, failed testing.

phenaproxima’s picture

Status: Needs work » Needs review
StatusFileSize
new22.03 KB
new6.53 KB

As @chx and @mikeryan and I discussed on IRC, this patch adds entity validation to the EntityContentBase destination.

It introduces a new exception called DestinationValidationException, which wraps a list of validation constraint violations. This is a temporary workaround for the fact that the destination is never given the MigrateExecutable and therefore cannot directly save validation error messages. So it throws this exception instead, which is caught and logged by the MigrateExecutable. This patch includes a test to ensure that EntityContentBase throws DestinationValidationException at all.

phenaproxima’s picture

StatusFileSize
new21.71 KB
new6.04 KB

Removed DestinationValidationException in favor of altering MigrateException so that it can hold more than one message. @mikeryan and @chx agreed that this is a more generic and reusable solution.

The last submitted patch, 23: 2487568-23.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 24: 2487568-24.patch, failed testing.

phenaproxima’s picture

Status: Needs work » Needs review
StatusFileSize
new21.62 KB

Re-rolling.

Status: Needs review » Needs work

The last submitted patch, 27: 2487568-27.patch, failed testing.

The last submitted patch, 27: 2487568-27.patch, failed testing.

phenaproxima’s picture

Status: Needs work » Needs review
StatusFileSize
new27.88 KB

Interim patch that provides default filter formats. This, by itself, should fix several broken tests, although it will not pass all of them.

phenaproxima’s picture

Priority: Normal » Major

This issue is major. It affects a lot of tests, and adding entity-level validation to destination plugins is a Very Big Deal. This change also throws into sharp relief a lot of the assumptions that have been written into the D8 Migrate API. The Migration process plugin's (former) ability to skip entire rows could also render entire migrations unstable or unpredictable. To me, this all adds up to "major".

Status: Needs review » Needs work

The last submitted patch, 30: 2487568-30.patch, failed testing.

phenaproxima’s picture

Status: Needs work » Postponed

I just discussed this on IRC with Moshe. He thinks, and I'm inclined to agree, that this does not need to block work on D7 migrations. It's a major issue for sure, but it creates an enormous amount of extra work (especially once entity validation is introduced), and the D7 stuff really does not need any more delays. Especially given that, under most circumstances, the migrations work as expected. So I'm postponing this issue. It's a known bug and will need to be fixed eventually, but I think things are "good enough" to proceed on D7 migration work for now.

benjy’s picture

Status: Postponed » Active

I don't see how wanting to start D7 makes this postponed, lets at least leave it as active in case someone else wants to pick it up.

Personally, I disagree. As you said last night on the call, this patch broke a large amount of the tests, every test broke in a different way which made them hard to debug and not something we could ask for help on from novices.

If we have 50 hard problems now, we'll have a 100 when D7 is done.

quietone’s picture

Status: Active » Needs review
StatusFileSize
new27.15 KB

Reroll

Status: Needs review » Needs work

The last submitted patch, 35: 2487568-34.patch, failed testing.

quietone’s picture

StatusFileSize
new28.09 KB

Try again.

quietone’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 37: 2487568-37.patch, failed testing.

quietone’s picture

Status: Needs work » Needs review
StatusFileSize
new28.1 KB

Third time lucky?

Status: Needs review » Needs work

The last submitted patch, 40: 2487568-40.patch, failed testing.

mikeryan’s picture

We need to get back to this, at least the original issue. While perhaps we shouldn't prevent all process plugins from skipping rows (perhaps there are custom scenarios where this makes sense), we should not have the default behavior of the core process plugins (I'm looking at you, migration!) do this - rather, they should return NULL and the migration config can choose (by adding skip_row_if_empty) to skip if desired.

Right now this is causing trouble with #2484405: User pictures do not need a dedicated process plugin, forcing us to implement a custom process plugin for user picture fields just to catch the SkipRow.

mikeryan’s picture

Going back over all the discussion here and remembering now all the stuff around the general principle, ugh. I think, though, that in the narrow case of the migration process plugin that it should definitely not throw SkipRow itself (if skipping the row makes sense for a particular migration, it can always follow the migration plugin with skip_row_is_empty), so I think I'll open a separate issue just for that narrow use case.

benjy’s picture

(if skipping the row makes sense for a particular migration, it can always follow the migration plugin with skip_row_is_empty)

I quite like that approach, +1

phenaproxima’s picture

phenaproxima’s picture

Status: Needs work » Closed (won't fix)

This issue was originally opened to deal with the problem addressed in #2560671: The Migration process plugin should not skip rows. Since Migration no longer throws MigrateSkipRowException, any further discussion is really just a matter of best practice and/or policy. This thread has outlived its usefulness. :)

heddn’s picture

Yes, +1 on not changing this functionality. I'm having to override the CCKFile right now so it *will not* catch the skip row exception.