Problem/Motivation

In #2541580: Remove obsoleted functionality from core Migrate we nearly put a little clause in MigrateExecutable::import() to interrupt a migration before going to the next row by checking the migrate_result keyvalue store for a STATUS_STOPPING condition. However, first let's think through any scenarios we can anticipate where we would want to interrupt an import.

  • #2403421: Implement migrate-stop command for D8 - it's very useful to have a means on the front-end to cleanly interrupt a migration, in scenarios like "OMG I didn't mean to run that", or "This is running way too slow, I should start over and profile it". This would be an asynchronous signal from a separate request/process, which suggests using state/keyvalue. The import() loop would check this just before next(), and Migration::RESULT_STOPPED should be returned so the front-end can indicate that the import was stopped in the middle rather than running to completion.
  • #2432983: Implement --limit option on migrate-import - on the front-end, it's very useful to be able to run a sample, say 10 items, for testing. The front-end can do the counting in a POST_ROW_SAVE or MAP_SAVE event, and needs a way to signal when it's reached the limit that we should break the loop before next(). An exception would break too soon (before the id map row is actually saved, or the highwater mark if that's in play. I think that, again, state/keyvalue would be the place to signal the stoppage, but this time we want import() to return MigrationInterface::RESULT_COMPLETED (since the entire import process as requested has completed).
  • #2432977: Implement --idlist option on migrate-import - while for SQL sources we can alter the original query to filter on the provided IDs and just let the import() loop run through the results, to support the general case for any source plugin we need to fetch each source row and check its ID against the list. We would want to optimize this so after the last ID in the list has been processed, we exit at that point rather than fruitlessly keep scrolling through the source iterator. The behavior should be pretty much like the --limit case - track the IDs we've matched, and leave a signal that's checked before next(), ultimately returning MigrationInterface::RESULT_COMPLETED.
  • Memory management - if we've crossed the memory threshold, and failed to reclaim "enough" memory, before next() we want to exit and return MigrationInterface::RESULT_INCOMPLETE, signalling the tool doing the running to start a new batch/process to continue the migration.
  • Are there scenarios we can anticipate where a process plugin or destination import() might decide to abort the whole migration? Can't think of any, but if they are they should be able to specify what RESULT_... to return.

The original not-quite-implemented approach was $migration->setMigrationResult(MigrationInterface::STATUS_STOPPING) which is just plain wrong (setting a value meant to be a RESULT_ constant to a STATUS_ constant). Setting it to Migration::RESULT_STOPPED makes only slightly more sense - it's defaulting to the first scenario above, and lacks context to rewrite to the appropriate result for the other scenarios. I think it makes more sense to have a specific keyValue() key for this purpose, whose value will be the result that import() should return.

Proposed resolution

  • In MigrateExecutable::import(), just before the next() call, retrieve the 'migrate_interruption' value for the current migration's id from the key/value store - if it exists, break from the loop and return the value.
  • Tools wishing to interrupt the migration will set that 'migrate_interruption' key to the MigrationInterface::RESULT_* value they want returned.

Remaining tasks

  1. Make a patch.
  2. Get patch reviewed.
  3. Commit patch.

User interface changes

N/A

API changes

The usage of the interruption key needs to be documented for the use of tool-builders and custom migration developers, so I guess that counts as part of the API.

Data model changes

N/A

Comments

mikeryan created an issue. See original summary.

mikeryan’s picture

Status: Active » Needs review
Issue tags: +Needs tests
StatusFileSize
new2.41 KB

As I prepare to hit Save, it occurs to me that some mocking in existing tests may be necessary... Well, let's see...

This has been verified to work for the --limit scenario with the patch at #2432983: Implement --limit option on migrate-import.

Status: Needs review » Needs work

The last submitted patch, 2: handle_various-2545672-2.patch, failed testing.

mikeryan’s picture

StatusFileSize
new8.34 KB
new6.87 KB
mikeryan’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests

Hit Save a little quick there... This version adds the ability to clear the interruption (kinda important so a given migration doesn't get permanently interrupted), and a test.

mikeryan’s picture

Issue tags: +Migrate critical

This is blocking a few tools features.

mikeryan’s picture

Issue tags: +blocker

This blocks a few features for the Drush commands in progress.

benjy’s picture

Maybe the interruption stuff could be moved into $this->checkStatus() ?

In general I think import() needs some refactoring, the method is becoming a little complex to follow.

mikeryan’s picture

Maybe the interruption stuff could be moved into $this->checkStatus() ?

The interruption stuff is replacing checkStatus, which was historically a grab-bag of various reasons to interrupt a migration. The current patch here was rolled between removing the memory checking from core, and re-adding it, so for a brief shining moment checkStatus() did not exist. The follow-up in #2545632: [PP1] Move memory reclamation out of migrate executable that separates out the memory checking to an event handler should use the interruption stuff.

In general I think import() needs some refactoring, the method is becoming a little complex to follow.

Any specific thoughts? One particular thing I'd like to revisit (someday) is the queued messages business, a holdover from D7...

Status: Needs review » Needs work

The last submitted patch, 4: handle_various-2545672-4.patch, failed testing.

mikeryan’s picture

Status: Needs work » Needs review
StatusFileSize
new8.6 KB

Rerolled, with a comment on the hopefully soon-to-be-doomed checkStatus()...

benjy’s picture

  1. +++ b/core/modules/migrate/src/Entity/Migration.php
    @@ -423,6 +423,30 @@ public function getMigrationResult() {
    +  public function setInterruptionResult($result) {
    ...
    +  public function getInterruptionResult() {
    

    We already have both getMigrationResult() and setMigrationResult() which use the same RESULT_* constants. Can we use the same methods and check in migrate executable for some kind of interruption result?

    Or better yet, we could start using the STATUS_* constants that we already have and check the migration status in the loop and save result for the completed statuses only?

  2. +++ b/core/modules/migrate/src/MigrateExecutable.php
    @@ -287,9 +287,19 @@ public function import() {
    +      ¶
    

    Extra white space

benjy’s picture

Status: Needs review » Needs work
mikeryan’s picture

Status: Needs work » Needs review
StatusFileSize
new8.61 KB
new546 bytes

We already have both getMigrationResult() and setMigrationResult() which use the same RESULT_* constants. Can we use the same methods and check in migrate executable for some kind of interruption result?

The original approach was to set RESULT_STOPPED as the interruption result. However, as detailed in the IS, we want import() to return different results in different scenarios. setMigrationResult() doesn't allow us to both signal the interruption and to provide the result to use upon interruption.

Or better yet, we could start using the STATUS_* constants that we already have and check the migration status in the loop and save result for the completed statuses only?

The STATUS_* constants represent the disposition of a given source row, to be saved in the map table. The RESULT_* constants represent the final status of an import process, to be reported by the front-end. Apples & oranges...

Extra white space

Fixed.

mikeryan’s picture

The STATUS_* constants represent the disposition of a given source row, to be saved in the map table.

Oops, wait, I was thinking of the MigrateIdMapInterface::STATUS_ constants, you meant the MigrationInterface constants, which will get used when I get back to #2429085: Track current state of migrations. So, we could signal interruption by setting MigrationInterface::STATUS_STOPPING in state, but we still also need to be able to specify the RESULT_* to be returned by import().

Status: Needs review » Needs work

The last submitted patch, 15: handle_various-2545672-15.patch, failed testing.

The last submitted patch, 15: handle_various-2545672-15.patch, failed testing.

The last submitted patch, 15: handle_various-2545672-15.patch, failed testing.

mikeryan’s picture

Status: Needs work » Needs review
benjy’s picture

Oo, two lots of STATUS_* constants in migrate, not confusing at all :) I think before this issue goes in we should have a think on whether we need to overhaul how these status work, do we need them all, do we need any new ones etc?

I don't particularly like how we have a new method for the interruption result, it would be better IMO if the result/status could indicate that the migration was interrupted and why.

phenaproxima’s picture

I don't particularly like how we have a new method for the interruption result, it would be better IMO if the result/status could indicate that the migration was interrupted and why.

I agree with this. It seems kludgey to me to provide different methods for purely semantic reasons. Is there are functional difference between a result constant and a status constant? As far as I can tell, no; it's purely a question of semantics.

So I propose we have a single setStatus() method, which accepts either a status constant or result constant, and that is the global status of the migration. If getStatus() returns a status constant, it means the migration result is indeterminate, since it hasn't been finished yet. Does that make sense, or would it be confusing?

If that is too awkward, maybe just get/setStatus() and get/setResult() would be sufficient. In any event, I think we should take this opportunity to sort out the jumbled meaning and usage of the various constants.

mikeryan’s picture

MigrateIdMapInterface: these statuses represent the disposition of a given *source* row (*map* row below is confusing - although the status is stored in a map row it's describing what happened the last time the source row was processed). This is completely orthogonal to the status of a migration.

  /**
   * Codes reflecting the current status of a map row.
   */
  const STATUS_IMPORTED = 0;
  const STATUS_NEEDS_UPDATE = 1;
  const STATUS_IGNORED = 2;
  const STATUS_FAILED = 3;

MigrationInterface: these statuses represent the status of a migration process at this moment - what is it doing (if anything)? They are primarily for use by #2429085: Track current state of migrations - they enabled migration tools (drush migrate-status command, dashboard UI) to report what a migration is doing now - as well as providing a semaphore so, say, one person doesn't start a rollback operation on a migration while someone else is running an import). This is completely orthogonal to the status of any given source row handled by the migration.

  /**
   * The migration is currently not running.
   */
  const STATUS_IDLE = 0;

  /**
   * The migration is currently importing.
   */
  const STATUS_IMPORTING = 1;

  /**
   * The migration is currently being rolled back.
   */
  const STATUS_ROLLING_BACK = 2;

  /**
   * The migration is being stopped.
   */
  const STATUS_STOPPING = 3;

  /**
   * The migration has been disabled.
   */
  const STATUS_DISABLED = 4;

MigrationInterface: these result codes represent what happened with the migration process that has just completed, so the front-end runner can report what happened to the user. This has some relationship to the MigrationInterface status constants (e.g., a status of DISABLED leads to a result of DISABLED), but it's not a 1-to-1 relationship (as explained in the IS, we may want to interrupt the migration and have a result of COMPLETED).

  /**
   * All records have been processed.
   */
  const RESULT_COMPLETED = 1;

  /**
   * The process has stopped itself (e.g., the memory limit is approaching).
   */
  const RESULT_INCOMPLETE = 2;

  /**
   * The process was stopped externally (e.g., via drush migrate-stop).
   */
  const RESULT_STOPPED = 3;

  /**
   * The process had a fatal error.
   */
  const RESULT_FAILED = 4;

  /**
   * Dependencies are unfulfilled - skip the process.
   */
  const RESULT_SKIPPED = 5;

  /**
   * This migration is disabled, skipping.
   */
  const RESULT_DISABLED = 6;

So, I think what people are asking for is rather than a specific API for interruption, the client doing the interrupting should do

$migration->setStatus(MigrationInterface::STATUS_STOPPING);
$migration->setMigrationResult(MigrationInterface::RESULT_INCOMPLETE); // Or whatever result

I still like the idea of a specific API, though, that does the above - rather than setInterruptionResult($result), perhaps interruptMigration($result) would be clearer (as opposed to some obscure d.o doc page describing how to interrupt a migration and set the result).

But, now I'm looking at setMigrationResult() and getMigrationResult() and getting uncomfortable. The RESULT constants are meant for communicating the immediate result of a migration result to a front-end client - why is it being saved in the keyvalue store? It turns out getMigrationResult() is used in exactly one place - isComplete() - which is in turn used in exactly one place to validate migration_dependencies (i.e., that the dependent migrations have run to completion). This is, unfortunately, wrong - the COMPLETED result is returned to the frontend to indicate that the requested migration *operation* has completed, and if it was requested to be a partial migration (--limit, --idlist) COMPLETED is returned, although the complete migration has not been run. So, that's a problem - out-of-scope for this issue though. It is related - fixing that issue on its own would remove the need for the result to be put in the kv store for that purpose, but the approach above would still need that...

mikeryan’s picture

mikeryan’s picture

Status: Needs review » Postponed

If we're going to use setStatus(), #2429085: Track current state of migrations needs to get in first.

mikeryan’s picture

Status: Postponed » Needs work

Unblocked, will reroll the patch to use getStatus.

mikeryan’s picture

Status: Needs work » Needs review
StatusFileSize
new7.67 KB
new4.81 KB

Here we are, leveraging the status.

benjy’s picture

I much prefer the latest approach using an interrupt method rather than setting the status from outside. +1 for RTBC here.

mikeryan’s picture

StatusFileSize
new7.8 KB
new541 bytes

With a little extra test suggested by phenaproxima.

phenaproxima’s picture

Status: Needs review » Reviewed & tested by the community

After discussion with @benjy and @mikeryan on IRC, I'm OK with this. I like the explicit separation between a migration's status and its result.

webchick’s picture

Status: Reviewed & tested by the community » Fixed

This looks like a straight-forward implementation of the status API that was added in the other patch. This unblocks several things, so let's keep it moving. :)

Committed and pushed to 8.0.x. Thanks!

  • webchick committed 8f94d83 on 8.0.x
    Issue #2545672 by mikeryan, benjy, phenaproxima: Handle various...

Status: Fixed » Closed (fixed)

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