When using a Source Plugin that uses Generators in their initializeIterator() function, drush migrate-import cannot be run with the --sync option, because rewind() is called twice on the iterator (once for the sync, and again for the import). Generator iterators cannot be rewound if they've already been used.
See for example migrate_source_csv's CSV::initializeIterator():
public function initializeIterator() {
$header = $this->getReader()->getHeader();
if ($this->configuration['fields']) {
// If there is no header record, we need to flip description and name so
// the name becomes the header record.
$header = array_flip($this->fields());
}
return $this->getGenerator($this->getReader()->getRecords($header));
}
protected function getGenerator(\Iterator $records) {
foreach ($records as $record) {
yield $record;
}
}
It's likely migrate_source_csv has done this to save memory.
Options:
1) Force classes that extend SourcePluginBase's initializeIterator function to not use Generators
2) Create a new migration object for the sync, and then for the import, so that the iterator isn't reused.
Original Description:
If I use migrate_source_csv, and attempt to run a migrate import with --sync to delete previously imported objects, drush throws the following error:
drush mim my_import --sync
[notice] Rolled back 32 items - done with 'my_import'
[error] Migration failed with source plugin exception: Cannot rewind a generator that was already run
It's not clear to me how Migrate Source CSV is at fault here. The generator rewinds, then steps through the generator twice. The first time in migrate_tools, then later in core's migrate.
First, the sync runs. and migrate_tools' MigrateImportSync::sync() runs the following block of code, rewinding then stepping through $source:
$source = $migration->getSourcePlugin();
$source->rewind();
$source_id_values = [];
while ($source->valid()) {
$source_id_values[] = $source->current()->getSourceIdValues();
$source->next();
}Later, migrate's MigrateExecutable::import() runs this block of code, rewinding then attempting to (and failing) to step through $source:
$source = $this->getSource();
$id_map = $this->getIdMap();
try {
$source->rewind();
} catch (\Exception $e) {
$this->message->display(
$this->t('Migration failed with source plugin exception: @e', ['@e' => $e->getMessage()]), 'error');
$this->migration->setStatus(MigrationInterface::STATUS_IDLE);
return MigrationInterface::RESULT_FAILED;
}There's no call whatsoever to rewind or next in migrate_source_csv.
Both $source's appear to be the same object through. The first is a CSV object, the second is a migrate_tools SourceFilter, though rewind() seems to reference the same object.
Could it be that the $source's iterator needs to be somehow reset between sync and import, and migrate_source_csv isn't properly doing this? The second call from MigrateExecutable::import() is returning the same iterator previous used in MigrateImportSync::sync()
BOTH could be cloned and it might work, let me try that...
| Comment | File | Size | Author |
|---|---|---|---|
| #12 | 3096252-12.patch | 41.23 KB | heddn |
| #12 | interdiff_11-12.txt | 3.26 KB | heddn |
Comments
Comment #2
trevorbradley commentedComment #3
trevorbradley commentedNOTE: I am not suggesting the following as a solution!
For reference, if I modify migrate's SourcePluginBase to add the following function:
And then modify MigrationImportSync::sync() to add a single line at the end of the if loop:
then migrate import with sync works exactly as expected.
Aha! I found another solution:
If instead I swap out the following line in MigrationImportSync::sync():
Replace:
With:
and ensure to run drush cim --sync with "--skip-progress-bar", it also works. The issue here is that MigrateDrushCommandProgress::initializeProgress calls count(), which initializes the generator early. If the progress bar is skipped, the generator isn't initialized until sync is actually run.
Comment #4
trevorbradley commentedI don't suggest this as a proper solution, but it *does* work.
Comment #5
heddnThis could use a test. A simple wrapper around embed that yields its results would be enough me thinks.
Comment #6
trevorbradley commentedI've spent yesterday afternoon and this morning figuring out PHPUnit and comparing my migrate-import script with migrate_tools' DrushCommandsTest.
Without my patch, for some reason, the migrate_tools test is just fine, but my migrate import using drush and migrate_source_csv fails.
DrushCommandsTest's version seems to instantiate a new MigrateExecutable between the sync and the import (creating a new Generator), where as my drush import csv doesn't.
Logic suggests it's either a migrate_source_csv issue, or a user error of some kind, but I'm not sure which.
I'm going to continue to investigate.
Comment #7
trevorbradley commentedOK, I understand the problem at least now. I'm using two different SourcePlugins.
DrushCommandsTest uses an EmbeddedDataSource, as defined in migrate_plus.migration.fruit_terms.yml.
EmbeddedDataSource::initializeIterator() is defined as:
However, my CSV code is using migrate_source_csv's CSV source plugin. There, CSV::initializeIterator() is defined as:
In short, EmbeddedDataSource uses an ArrayIterator. CSV uses a Generator iterator. Generators cannot be rewound, but ArrayIterators can.
It all makes sense, migrate_source_csv likely did this to save memory on large CSV imports.
I'm going to update the ticket title and description. Not clear where to proceed from here.
@heddn - is this a migrate_source_csv issue, or a migrate_tools issue?
Comment #8
trevorbradley commentedComment #9
heddnThis means that we need to bump our support for migrate source csv to be ^3.0. Let's see what that means for various tests.
Comment #10
heddnHmm, here we do a drastic thing and remove the web ui components for managing csv imports.
Comment #11
heddnComment #12
heddnComment #13
trevorbradley commentedJust a note that migrate_source_csv:3.1 and migrate_source_csv's current dev branch still both use generators instead of simple iterators. This isn't as simple as switching migrate_source_csv to ^3.
Comment #14
heddnGoing to commit this to a new 5.x branch in prep for 8.8+/9.0 support.
Comment #15
heddnWe can entertain backporting to 4.x, but given all the moving parts... it might be difficult. If someone wants to backport the fix only without any test changes, that might be the best way to go about backporting.