This issue has been created
There is 1 update.
 
 
XWiki Platform / cid:jira-generated-image-avatar-c2dcf149-0619-4f7a-b1bb-ba6b25021b3d XWIKI-24907 Open

Importing an extension in the repository saves a stale document and overwrites the concurrent last-version update

 
View issue   ยท   Add comment
 

Issue created

 
cid:jira-generated-image-avatar-76af5b68-7b58-4c47-a5f9-eb15665c09c0 Vincent Massol created this issue on 15/Sep/26 14:17
 
Summary: Importing an extension in the repository saves a stale document and overwrites the concurrent last-version update
Issue Type: cid:jira-generated-image-avatar-c2dcf149-0619-4f7a-b1bb-ba6b25021b3d Bug
Affects Versions: 17.10.12
Assignee: Unassigned
Components: Repository
Created: 15/Sep/26 14:17
Priority: cid:jira-generated-image-static-major-13c5aa3c-7051-4107-9199-a1b04c68df71 Major
Reporter: Vincent Massol
Description:

Problem

When an extension is imported (refreshed) in the Repository application, the extension page has two concurrent writers and the import wins with content it read minutes earlier:

  • RepositoryManager.tryUpdateExtension clones the extension document up front (RepositoryManager.java:703), then does the long work - resolving every version from the remote repository, creating/updating/deleting the version pages - and only at the very end saves that now-stale clone (RepositoryManager.java:980).
  • Since XWIKI-24669, ExtensionUpdaterListener also reacts to version page create/update/delete and re-loads and saves the extension page as a minor edit (ExtensionUpdaterListener.java:97-108, calling RepositoryManager.updateLastExtensionVersion at RepositoryManager.java:489).

The listener's negated observationContext.isIn(IMPORT_PROCESS) guard is meant to suppress exactly this during an import, and it does not hold: a version page saved in another execution context during the import still triggers the listener. In the history of extensions:Extension.Release Notes Application.WebHome on extensions.xwiki.org, revision 88.2 "Updated last version of the extension" and revision 89.1 "Imported extension" are 5 seconds apart (30 Aug 2026) - the same operation.

Two consequences:

  • The listener's update is silently overwritten, since the import saves a document it read before that update.
  • On MariaDB with innodb_snapshot_isolation ON (the default since 11.6.2), the database refuses the stale write instead, and the import fails with Record has changed since last read in table 'xwikidoc', wrapped as an opaque Error number 3201. That is what happens on extensions.xwiki.org, where an import fails on the first attempt and succeeds when retried immediately - see XINFRA-496.

The final save of a stale clone is older than XWIKI-24669, but it was harmless while the import was the only writer of that page.

Proposal

Re-read the extension document just before the final save and re-apply the changes, instead of saving the clone taken at the start of the import. Widening the guard is not sufficient on its own, since it cannot cover a save that happens in another execution context.

Side note

updateLastExtensionVersion (RepositoryManager.java:489-501) saves through xcontext.getWiki().saveDocument(...) without setting the author, unlike the private RepositoryManager.saveDocument helper, so those edits keep the author of the last import (they show up as XWiki.Admin on extensions.xwiki.org). Cosmetic, but it makes the history misleading.

 
 

1 update

 
cid:jira-generated-image-avatar-76af5b68-7b58-4c47-a5f9-eb15665c09c0 Changes by Vincent Massol on 15/Sep/26 14:19
 
Description: h2. Problem

When an extension is imported (refreshed) in the Repository application, the extension page has two concurrent writers and the import wins with content it read minutes earlier:

* {{RepositoryManager.tryUpdateExtension}} clones the extension document up front (RepositoryManager.java:703), then does the long work - resolving every version from the remote repository, creating/updating/deleting the version pages - and only at the very end saves that now-stale clone (RepositoryManager.java:980).
* Since XWIKI-24669, {{ExtensionUpdaterListener}} also reacts to version page create/update/delete and re-loads and saves the extension page as a minor edit (ExtensionUpdaterListener.java:97-108, calling {{RepositoryManager.updateLastExtensionVersion}} at RepositoryManager.java:489).

The
self-conflict was foreseen: the import is bracketed by {{ExtensionImportStartingEvent}} / {{ExtensionImportFinishedEvent}} (RepositoryManager.java:887 and :987) and the listener 's negated skips the update while {{observationContext.isIn(IMPORT_PROCESS)}} guard is meant to suppress exactly this during an import , and it so within one import thread the guard does not hold: a cover the version page saved saves made at RepositoryManager.java:972.

It is not enough
in another execution context during the import still triggers the listener practice . In the history of {{extensions:Extension.Release Notes Application.WebHome}} on extensions.xwiki.org, revision 88.2 "Updated last version of the extension" and revision 89.1 "Imported extension" are 5 seconds apart (30 Aug 2026) - . Two explanations fit, and the server log or a reproduction is needed to tell them apart:

* a version page is saved outside the import fold, in another execution context, so the guard does not apply; or
* two imports of the
same operation extension overlap, the listener firing in one while the other sits between its clone and its final save . The "Updated last version of the extension" revisions are attributed to {{XWiki.Admin}} because of the author problem described below, so they are not evidence of a separate session by that user.

Either way, the final save at RepositoryManager.java:980 writes a document that was read before those updates.

Two consequences:

* The listener's update is silently overwritten, since the import saves a document it read before that update.
* On MariaDB with {{innodb_snapshot_isolation}} ON (the default since 11.6.2), the database refuses the stale write instead, and the import fails with {{Record has changed since last read in table 'xwikidoc'}}, wrapped as an opaque {{Error number 3201}}. That is what happens on extensions.xwiki.org, where an import fails on the first attempt and succeeds when retried immediately - see XINFRA-496.

The final save of a stale clone is older than XWIKI-24669, but it was harmless while the import was the only writer of that page.

h2. Proposal

Re-read the extension document just before the final save and re-apply the changes, instead of saving the clone taken at the start of the import. Widening the guard is not sufficient on its own, since it cannot cover a save
that happens in another made outside the import's execution context , nor a second import running in parallel .

h2. Side note

{{updateLastExtensionVersion}} (RepositoryManager.java:489-501) saves through {{xcontext.getWiki().saveDocument(...)}} without setting the author, unlike the private {{RepositoryManager.saveDocument}} helper, so those edits keep the author of the last import (they show up as {{XWiki.Admin}} on extensions.xwiki.org). Cosmetic, but it makes the history misleading.