Hello everyone,
We have a project in which we wanted to re-autotranslate to several languages when the base language changed, but only for published entities. A continous job was created, with google as the provider.
So we set in a presave() hook a to load and save translations while setting the 'content_translation_outdated' field via ContentTranslationMetadataWrapper::setOutdated()
(Instead of doing this manually, we also tried https://www.drupal.org/project/drupal/issues/2950627 this patch which provides a checkbox to set this property via the UI)
This results in an endless loop since on in the function ContentEntitySource::shouldCreateContinuousItem
web/modules/contrib/tmgmt/sources/content/src/Plugin/tmgmt/Source/ContentEntitySource.php:473
$translation = $entity->hasTranslation($job->getTargetLangcode()) ? $entity->getTranslation($job->getTargetLangcode()) : NULL;
$metadata = isset($translation) ? $translation_manager->getTranslationMetadata($translation) : NULL;
// If a translation exists and is not marked as outdated, no new job items
// needs to be created.
if (isset($translation) && !$metadata->isOutdated()) {
return FALSE;
}
else {
it will continue to fail this check !$metadata->isOutdated() since the continous job won't reset 'content_translation_outdated'.
What happens is, that the newly created job item is created, the auto translation will succeed, and the node translation will be saved.
The custom presave hook will run but it won't do anything if the activeLanguage of the node is not the source language so this will pass.. But the tmgmt update hook will trigger, after the save of the node, and start the cycle anew, wanting to create a job item for the saved translation etc,..
Steps to reproduce
- Setup a fresh ddev instance of Drupal https://ddev.readthedocs.io/en/latest/users/quickstart/#drupal
- Require tmgmt with composer and install via drush.
- Enable the tmgmt_test module via drush so the test translator is available.
- Enable German as a language.
- Enable Spanish as a language
- Make the default article bundle translatable along with all fields
- In the provider settings enable auto accept translations
- In tmgmt settings ensure submit items on cron is checked.
- Setup a continuous job for the article in German using test translator
- Setup a continuous job for the article in Spanish using test translator
- Create an article and save it
- Run the following SQL and you should see two inactive continuous jobs
select tji.item_id, tji.tjiid, tj.target_language from tmgmt_job_item as tji join tmgmt_job as tj on tji.tjid = tj.tjid where tj.job_type = 'continuous' and tji.state = 0; - Run ddev drush php-eval "tmgmt_cron();" and then run the SQL above again and there should be no inactive job items.
- Go to the article in the original language and click mark translation as outdated then save it.
- Now if you run the SQL again there should be two inactive items
- Now run ddev drush php-eval "tmgmt_cron();" again and when you run your SQL you will see one job item remaining.
Now matter how many times you run tmgmt_cron() via drush from there there is always one remaining inactive item and it alternates between de and es. It's like one is always marking the other as outdated.
| Comment | File | Size | Author |
|---|
Issue fork tmgmt-3063378
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
crizStill not sure what is happening here, but this is a patch that disables creating new job_items for a continuous job when a translation is outdated.
Comment #3
achapSo I think this is actually a bug and not just with moderated entities. Initially I thought it was due to the referenced core bug (which is why marking revisions as outdated was hidden in the first place in core for moderated entities) but I was actually able to reproduce it for unmoderated entities too on a freshly installed site without any of my custom code.
Steps to reproduce:
select tji.item_id, tji.tjiid, tj.target_language from tmgmt_job_item as tji join tmgmt_job as tj on tji.tjid = tj.tjid where tj.job_type = 'continuous' and tji.state = 'inactive';ddev drush php-eval "tmgmt_cron();"and then run the SQL above again and there should be no inactive job items.ddev drush php-eval "tmgmt_cron();"again and when you run your SQL you will see one job item remaining.Now matter how many times you run tmgmt_cron() via drush from there there is always one remaining inactive item and it alternates between de and es. It's like one is always marking the other as outdated.
Would appreciate if someone else can confirm my steps to reproduce. Would potentially think about marking this as major if it's confirmed as this can cause a huge amount of revisions/costs from re translation.
Versions: Drupal core 11.1.8, tmgmt 1.17.0
Comment #5
achapCreating a test to try and highlight the bug
Comment #6
achapWhen adding an additional continuous job in Spanish and duplicating the steps that were created in German I can replicate it in a test:
As best I can tell, when the translation is accepted and saved, tmgmt_content_entity_update() is called, at which point the untranslated entity is again sent to the continuous manager but at this point in shouldCreateContinuousItem the metadata returns that both of the translations are still outdated. For whatever reason, this doesn't occur with a single translation job.
The German node was previously being marked as unoutdated correctly.Comment #7
achapExplicitly marking the translation as outdated when it is saved in ContentEntitySource means the test passes but I would have thought that would have been done automatically by Drupal? I'm not sure if this is the correct fix but it does stop the loop. I would appreciate if someone could review it and share their thoughts especially if they can figure out why this only occurs with a second job.
(The fail in the MR is unrelated to me. It's something to do with composer which I haven't touched).
Comment #8
achapI did a bit more digging on this and I'm fairly confident that default Drupal behavior doesn't automatically mark a translation as unoutdated when you save a translation so I think my fix is correct. I also found a corresponding issue on the 7.x branch that had the same problem and was committed: https://www.drupal.org/project/tmgmt/issues/1619786
As for my earlier statement that "The German node was previously being marked as unoutdated correctly." after checking again it definitely wasn't reset to 0 automatically but the issue definitely only starts to occur once you have 2 languages in 2 jobs affecting the same node type. Hence why I added a second Spanish job to the test.
Honestly I think this should be marked as major, as with this bug a new revision will be created on each cron run and it will also be sent for translation continuously costing money.
I guess not a lot of people are using this functionality and instead are just re-requesting the translation via the UI when needed but this is an important feature for me.
Comment #9
charginghawk commentedWorking for us!
Comment #10
achapRecently switched from workbench moderation to content moderation and am using the patch from https://www.drupal.org/project/drupal/issues/2950626 to allow marking moderatable entities as outdated again. Simply setting
setOutdated(FALSE)is fine for non moderatable entities but won't work for moderatable entities with the core patch in its current form. So I have created: https://www.drupal.org/project/tmgmt/issues/3560685 to handle both cases. Will set this one as needs review and keep it open as technically it could get merged on its own. However, https://www.drupal.org/project/tmgmt/issues/3560685 handles both cases.Comment #11
achapI've updated my MR for the related core issue, https://www.drupal.org/node/2950626, so hopefully the complexity of keeping track of the moderated entities' outdated status can be handled there and we can just get call setOutdated like we are doing. Closed the other issue and rebased this with upstream fixes. Tests are now passing so this shouldn't be blocked by anything. Setting back to RTBC.
Comment #12
balazswmann commentedThank you for opening this issue. I run into the very same problem on a project where I'm only using the
tmgmt(8.x-1.8) module along with tmgmt_deepl so there is no content moderation.I can confirm the bug which seemingly only appears if a content is translated to two or more languages. So if you have continuous jobs, then two or more translation jobs are created when you update the content. I also did a quick test with only one continuous job (one language) but I couldn't reproduce the issue that way. The steps mentioned in comment #3 to reproduce the issue are accurate and helped me to clarify that I run into a same bug.
I also tried out the patch from the opened MR and after doing some quick tests I can confirm that it seemingly resolves the problem, but I haven't had time to fully understand why it works.
Comment #13
idebr commentedComment #14
claudiu.cristeaThe test shows the issue and the fix is common sense but I could not reproduce using #3. When I run 1st SQL it returns an empty set. Looking into DB, I see the 2 items with state 2 (active) but I expect to see them with state 0 (inactive). Any chance to revisit the steps to reproduce?
Comment #15
achapJust went through the steps again and the missing steps were to enable auto accept on the provider and submit items on cron. Have added to the issue summary. Thanks for taking a look!
Comment #16
claudiu.cristeaThank you, I've managed to reproduce the bug manually. However, this comment gives me headaches
I want to understand why. Also, only one inactive item is added every time, maybe here is the issue
Comment #17
claudiu.cristeaI couldn't wrap my head around why it happens with ≥2 target languages but not with one language. Finally I've asked some help from Claude. Following the report and the code it makes perfectly sense. Here's Claude's assessment:
The three pieces of code involved
tmgmt_content_entity_update()(sources/content/tmgmt_content.module:313) fires on any save of the entity — including TMGMT's own save of a translation — and callstmgmt_content_create_continuous_job_items(), which loops over all continuous jobs (one per target language) and callsContinuousManager::addItem()for each.ContentEntitySource::shouldCreateContinuousItem()(sources/content/src/Plugin/tmgmt/Source/ContentEntitySource.php:706) returns TRUE whenever the target translation is missing or$metadata->isOutdated(). SincedoSaveTranslations()never callssetOutdated(FALSE)(that's what MR !122 adds), once a translation is flagged outdated it stays "outdated" forever, even after a fresh translation is saved over it.ContinuousManager::addItem()(src/ContinuousManager.php:143-148) refuses to create a new job item if the most recent item for that entity in that job is still active (!isAborted() && !isAccepted()).Why one language doesn't loop
Note the ordering in
JobItem::acceptTranslation()(src/Entity/JobItem.php:938-945):saveTranslation()runs before$this->accepted(). So when the German translation is saved andhook_entity_update()fires, the German job item is still in "needs review" state — still active.Single language (DE only)
saveTranslation()saves the DE translation →tmgmt_content_entity_update()fires →addItem()for the DE job →shouldCreateContinuousItem()says TRUE (flag is still stale), but J1 is still active (not yet marked accepted) → guard atContinuousManager.php:145blocks it, logging "changes were ignored as job item is still active."accepted()runs, J1 becomes accepted. But nothing saves the entity again, so the hook never fires again. Chain terminates.The DE flag is still wrongly TRUE, but there's no trigger left to act on it — the bug is latent, not visible.
Why two languages loop forever
With DE + ES you have two continuous jobs, and each save of one language's translation acts as a fresh trigger for the other language, at a moment when the other's guard no longer protects it:
So the ≥2-languages condition isn't about the outdated flag propagating between translations — it never changes at all, which is precisely the bug. It's that the loop needs an external save event hitting a job whose last item is already accepted. A language's own save always happens while its own item is still active (blocked by the guard); only another language's save arrives after acceptance. That's why the fix in MR !122 — calling
setOutdated(FALSE)when TMGMT saves the translation — breaks the cycle: after acceptance,shouldCreateContinuousItem()correctly returns FALSE regardless of who triggers the re-check.Comment #18
claudiu.cristeaLet's fix some minor issues here
Comment #19
achapYes it was quite hard to debug what was going on because the bug only surfaces with more than one continuous job. I have fixed up those items on the MR.
Comment #20
claudiu.cristeaThank you
Comment #22
claudiu.cristeaEverything looks good, thank you. Merged