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
- Add two new exceptions and remove MigrateSkipProcessException
- Add getters and setters in Row for fallen properties
- 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.
| Comment | File | Size | Author |
|---|---|---|---|
| #40 | 2487568-40.patch | 28.1 KB | quietone |
| #37 | 2487568-37.patch | 28.09 KB | quietone |
| #35 | 2487568-34.patch | 27.15 KB | quietone |
| #30 | 2487568-30.patch | 27.88 KB | phenaproxima |
| #27 | 2487568-27.patch | 21.62 KB | phenaproxima |
Comments
Comment #1
phenaproximaA patch so simple and tiny, it's downright adorable.
Comment #2
phenaproximaAdded explicit return statement.
Comment #3
phenaproximaAt @mikeryan's request, changed Migration::transform()'s default return value to an empty array.
Comment #7
chx commentedTo 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.
Comment #8
mikeryanLet's look closely at the scenario where this happens:
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:
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.
Comment #9
chx commentedContinuing 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
Rowto 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.Comment #10
phenaproximaUpgrading 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).
Comment #11
chx commentedThe 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 .
Comment #12
phenaproximaThere is already a MigrateSkipProcessException for precisely this purpose -- can't we just use that?
Comment #13
phenaproximaRe-titling.
Comment #14
phenaproximaDoes this deprecate the skip_row_if_not_set and skip_row_on_empty plugins? I'm not sure it does, or should.
Comment #15
chx commented> 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 .
Comment #16
phenaproximaComment #17
chx commentedComment #18
phenaproximaComment #19
phenaproximaComment #20
benjy commentedIssue 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.
Comment #21
phenaproximaHere's an initial patch, with tests.
Comment #23
phenaproximaAs @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.
Comment #24
phenaproximaRemoved 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.
Comment #27
phenaproximaRe-rolling.
Comment #30
phenaproximaInterim patch that provides default filter formats. This, by itself, should fix several broken tests, although it will not pass all of them.
Comment #31
phenaproximaThis 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".
Comment #33
phenaproximaI 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.
Comment #34
benjy commentedI 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.
Comment #35
quietone commentedReroll
Comment #37
quietone commentedTry again.
Comment #38
quietone commentedComment #40
quietone commentedThird time lucky?
Comment #42
mikeryanWe 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.
Comment #43
mikeryanGoing 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.
Comment #44
benjy commentedI quite like that approach, +1
Comment #45
phenaproximaCreated the issue @mikeryan alluded to in #43: #2560671: The Migration process plugin should not skip rows.
Comment #46
phenaproximaThis 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. :)
Comment #47
heddnYes, +1 on not changing this functionality. I'm having to override the CCKFile right now so it *will not* catch the skip row exception.