Problem/Motivation
When using Drupal core's Content Translation content, Node Revision Delete does not retain X "Maximum number allowed" of revisions per node translation. Instead, Node Revision Delete will only retain X "Maximum number allowed" of revisions per node, which risks deleting all revisions of a single translation, including an old pending draft revision if using Drupal core's Content Moderation and Workflows (see related #3094354: Delete draft revisions safely).
Steps to Reproduce
- With Drupal core's Content Translation and Languages enabled and configured, create a node item with a translations A and B.
- Create multiple (4+) revisions of a single translation A
- Create multiple (4+) revisions of the other translation B so that these are more recent than the previously created revisions
- Using Node Revision Delete default configuration and changed both "Maximum number allowed" to 4, Delete revisions now
- Observe that all of the old revisions matching Node Revision Delete configuration are deleted, expected
- Observe all translation A revisions are deleted
I think the expected behavior for a multilingual site would be that configuration would apply consistently to node translations, so that 4 translation A revisions are kept and 4 translation B revisions are also kept...particularly if using Content Moderation and Workflows and there is a pending draft translation revision (see related #3094354: Delete draft revisions safely).
Proposed resolution
TBD
Remaining tasks
Get consensus about problem- Get consensus about how to solve it
- Patch
- TBD
User interface changes
TBD
API changes
TBD
Data model changes
TBD
Release notes snippet
TBD
Issue fork node_revision_delete-3118464
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
jasonawantComment #3
jasonawantIn addition to any UI changes to communicate the intent behind retaining "Maximum number allowed", I think the current query logic found in NodeRevisionDelete::getCandidatesRevisions() needs to account for multiple language translations.
The first thing that comes to mind is using Drupal core language manager to get available languages, loop over them and to language condition to the query for each language. However, this would multiply the number of queries by the number of active languages unless there is a way to stuff this into a single query.
Instead of looping over all available languages, the NodeRevisionDelete::getCandidatesNodes() would be refactored or replaced with logic, i.e. getCandidatesNodeTranslations(), to return Node IDs keyed by language or something like this, so NodeRevisionDelete::getCandidatesRevisions() can target its query logic on only candidate nodes and translations instead of looping on all available languages.
Comment #4
adriancidThanks for the issue and for use the Issue Summary Template.
As I say in the release
I know this is important. But at this time I concentrating all the efforts finishing the test for the actual code. Seeing we will need to refactor at some point the getCandidates method I will exclude these methods from the Unit tests for the moment.
If you have time to help me with this will be great.
When you create a new revision in a multilingual environment Drupal create a new revision for all the other configured language (here is where the module fails as it doesn't consider this behavior for the moment). If you have a content type configured to be multilingual and you have English (EN) and Spanish (ES). When you edit a node in EN a new revision is created for ES even if you don't change anything in the ES language. I don't know if is here where the database field revision_translation_affected can help us.
Comment #5
jasonawantThanks for the response!
Regarding,
How can I observe this? I'm not seeing this. If I create a node in language A, create a translation in language B, then create a second revision in language A, I only see 2 revisions in the node_revision table and see three revisions between /node/1/revisions and /es/node/1/revisions.
oh, uhm, I see 4 records in the node_field_revision table. Is this what you are referencing?
Comment #6
jasonawantI'll tell ya'; it's hard to wrap my head around the "revision_translation_affected" usage. From Entity API,
While I did observe the following
In that a node_field_revision is created with a revision_translation_affected value of NULL, I do not see any current Node Revision Delete query logic that uses (via a join) the node_field_revision table. B/c of this, I'm not sure if Node Revision Delete is impacted by this behavior.
Comment #7
jasonawantThis has become a priority for our stakeholders. We will begin to outline a solution and plan to include it in future sprint work.
Comment #8
jasonawantOk, I took a look at what needs to updated and it looks like the following.
The solution it to apply the configured "Maximum number allowed" per translation. To do that, we can make the following changes.
1. NodeRevisionDelete::getCandidatesNodes(); need to get candidate nodes by counting their translations separately; may need to track nodes by language id for #2 below; depends on the #2.
2. NodeRevisionDelete::getCandidatesRevisionsByNids(); I haven't looked at how to refactor this piece; but this where the bulk of the changes will be. This change could/should consider #3094354: Delete draft revisions safely
3. AdminSettingsForm::buildForm(); depending on #2 and #1 above
Comment #9
jasonawantAssigning to prepare a patch for this and #3094354: Delete draft revisions safely
Comment #10
jasonawantI've worked on a patch as part of #3094354-13: Delete draft revisions safely. If we want these split up, let me know.
Comment #11
adriancid@jasonawant Is better if you can split the patch, I prefer to review small patches. Thanks.
Comment #12
jasonawantThe patches are going to overlap b/c of changes to getCandidatesRevisions and getCandidatesRevisionsByNids query logic for both future revisions protection (#3094354) and translation revisions in this issue.
I've uploaded a specific patch for #3094354-14: Delete draft revisions safely there. If you could review that one first, I can then iterate on it in this issue queue to solve for per translation revisions. If you prefer this issue first, let me know and I'll work on a patch just for this issue.
Comment #13
jasonawantThis patch includes patches from the following issues
and also includes query logic to support #3094354: Delete draft revisions safely.
Comment #14
jasonawantI encountered the following error in the remote environment. I'm unable to reproduce this error locally easily, so I've added additional fields to the revision query in hopes this fixes the error in the remote.
Again, this patch includes patches from the following issues
and also includes query logic to support #3094354: Delete draft revisions safely.
Comment #15
jasonawantI was wrong about the previous fix; it didn't work. To solve my issue, I had to groupBy the nfr.changed column.
This changed solved my issue.
Again, this patch includes patches from the following issues
and also includes query logic to support #3094354: Delete draft revisions safely.
Comment #16
cachesclay commentedPatch from #15 looked pretty good when testing it out, but minimum age to delete doesn't seem to get respected, tried adding this but not quite right.
Comment #17
bobbysaul commentedI updated the patch from @jasonawant since it stopped applying after rc1. It seems like some of the changes in the patch was committed. Also, I updated it a bit to not change getCandidatesRevisionsByNids to getCandidatesRevisionsByNid.
Comment #18
google01 commentedIt is confirmed that patch # 17 does not respect the "minimum age" setting.
Comment #20
sharique commentedHere is updated patch to fix minimum age consideration.
Comment #22
sharique commentedMy bad, I name diff file extention to .patch, I also raised MR, for easier review.
Comment #23
pere orgaIncreasing priority.
Comment #24
sokru commentedI can confirm patch from #20 solves the issue from issue summary and code looks good, but I'd like to see tests included to avoid any regression on future releases.
Comment #25
adriancidI don't see where is handled here this configuration 'delete_newer'
And the 'delete_newer' configuration is not considered here either.
Comment #26
smulvih2Confirmed patch #20 fixes the issue for me.
Comment #27
google01 commentedThe patch maintains the latest revisions for all languages regardless of which language each revision belongs to.
If "Minimum number of revisions to keep" is set to 5, it effectively keeps 5 revisions, but not the last 5 for each translation, only the last 5 in all languages.
Comment #28
heikkiy commentedThe patch seems to need a reroll for the latest rc4.
Comment #30
bahbka commentedI've rerolled the patch, to check that it works for the scenario provided in https://www.drupal.org/project/node_revision_delete/issues/3262899, but as was mentioned in
- #25 "delete_newer" option is missing,
- #27 states that patch doesn't work at all
On my opinion it should include scenario where editor re-save a node without any change: in this case translation_affected is not set at all, but revision itself is pretty much redundant(doesn't contain any changes).
This discussions could be closed or resolved only with a help of tests. Currently module ships with tons of different tests, but they don't cover module core feature: the deletion of the revisions and merging this issue into dev without even confirming that it doesn't break anything is a really bad idea.
I will try to find an issue for this kind of a test or open a new one.
Comment #31
falco010Hereby a rerolled patch that should apply to the latest 1.0-rc4 release.
I also tested the claim in #27 that the patch does not work and for me, it works fine.
For example, if you configure it to keep a minimum of 3, it will keep a minimum of 3 per language!
Comment #32
adriancidComment #33
seanbHere is a first review of the patch.
Doing an array_merge() inside a foreach loop is not very efficient. Can we do the merge outside the loop using the spread operator?
https://wiki.php.net/rfc/argument_unpacking
I see this is also being done in other placed in the module, so I guess this could also be a separate followup.
It seems this method now only retrieves the candidate revisions for a single nid (the first one), not for all the nids passed? Within the module I see all calls only pass a single node ID, but I guess we should either:
- Change this method to only support a single NID (might be less complex and better for performance?)
- Fix the query to fetch all candidate revisions for all passed node IDs.
The query is quite complex, it would help a lot to add a bit more documentation. For example, explain a bit about the background of why we need a subquery to fetch the changed date from the node_field_data table etc.
This seems to be my biggest concern for now. New draft translations don't add a record to the
node_field_datatable (#3088790: Creating a new translation draft is not populating node_field_data leading to inconsistent data display.). It seems the subqueries ingetCandidatesRevisions()/getCandidatesRevisionsByNids()andgetCandidatesNodes()might not work without that being solved. Without a record innode_field_datafor example we are also not able to fetch a node type for a revision by vid and langcode.----------
I would have to think about this a bit more, but reading the patch I'm starting to wonder if a highly complex query like this is really the most robust and maintainable way to move forward. I have a feeling it would help a lot to maybe start using the queue system.
We could have several ways to add node IDs / revision IDs to the queue, and let the queue try to figure out whether that specific revision is ready to be deleted or not. This might be less performant, but I think trying to do all checks in a single query has a bunch of limits that might be tricky to overcome as well. Anyway, please excuse this sidestep, that is definitely not in scope for this issue, and I'll think a bit more on whether to open a follow-up for this.
*edit: Opened #3300328: Simplify architecture to solve issue around content moderation, translations and cron as a followup.
Comment #34
sokru commentedThe attached patch is a re-roll for 8.x-1.0-rc5 based on #31. Unfortunately not being able to produce interdiff.
The patch should resolve:
- #25.1 and #25.2 so
delete_newersetting is respected.- #33.2 by renaming
getCandidatesRevisionsByNidstogetCandidatesRevisionsByNidand adjusting the code accordingly.And I fully agree with @seanB's other points raised on #33, but I'd hope they could be resolved on follow-up issues.
Comment #36
sokru commentedThis patch should pass the CI tests, but this is still missing test coverage for multilingual.
Comment #37
sokru commentedAdjusted the patch to work with
delete_neweroption. This patch also includes test coverage. Interdiff doesn't contain the new test file.Since this is quite complex issue to review, I would appreciate any manual tests with real content. I'll write the test coverage I used in manual steps:
1. Install the site with Standard profile
drush site:install standard2. Enable modules
drush en language content_moderation content_translation node_revision_delete workflows -y3. Add a new non-english language
/admin/config/regional/language/add.4. Enable workflow to Article content type
/admin/config/workflow/workflows/manage/editorial, set "Default moderation state" as "Published".5. Enable translations and select "Limit the number of revisions" and "Minimum number of revisions to keep" as 1 for article.
/admin/structure/types/manage/article.6. Add node #1, Edit it two times.
7. Add translation to #1, edit translation two times.
8. Add node #2. Edit it two times, both times set moderation state as "Draft"
9. Add node #3.
10. Add translation to node #3 and edit translation two times.
11. Visit
/admin/config/content/node_revision_deleteand expect "Candidate nodes" to be 2 and "Candidate revisions" to be 3.12. Select "Delete revisions newer than the current revision" and submit the form.
13. Expect "Candidate nodes" to be 2 and "Candidate revisions" to be 4.
Is this working as expected or would it need some changes?
Comment #38
heikkiy commentedThe merge request diff applies against latest rc6 but there is a message in the merge request that it needs a rebase against the main branch.
Comment #39
seanbWe just added a 2.x version of the module. This is a complete rewrite based on plugins to determine which revisions can be deleted. It also uses a queue worker to actually delete revisions, and takes multiple translations of content into account. Could you please test if this solves the issues you currently experience?
Comment #40
vincent.hoehnFor me, the new version 2.x does not work. Previously, too many revisions were deleted. But now too few revisions are deleted. At least that's how it looks.
In the sandbox, the module works great at first sight. But in our system with two languages and 40k revisions, it doesn't seem to work. Too bad!
Comment #41
b-prod commentedI confirm what @vincent.hoehn say: the 2.x version does not work at all, if I specify to keep only 1 revision, nothing happens.
For example, I have a node with 2860 revisions, none is deleted.
Comment #42
alejomc commentedFor those whose need the patch for -rc6.
Comment #43
seanb@vincent.hoehn @B-Prod could you maybe try and debug what is happening in the 2.x version to see why the revisions are not being deleted? Are you getting errors in the logs? Are items being added to the queue? Is the queue being processed? Is this process running out of memory? Any additional info to help resolve the issues would be appreciated!
Comment #44
tvalimaa commented#42 looks like working with 1.0 rc-7.
I have now check and calculate revisions count per node and node translations after clicking delete revisions on node.
Comment #45
smitghelani commented#42 has one code change is missing from #37,
File CandidateNodesForm.php has below mentioned change inside the #37 patch, but which is missing inside the #42 patch i have added that change and created new patch and other changes are same as #42 and tested with latest version of module 8.x-1.-rc7.
Comment #46
adriancidClosing because the current development is being made in branch 2.x