Repository navigation
fix: Restoring the built-in sites can leave the main sites with empty pages EXO-91183 - #1338
Conversation
Self-review close-out (AI-assisted loop) — head cf3c9b5Three reviewer rounds (the last a fresh reviewer: CLEAN — nothing to fix) and twelve delivery-checks passes (the last: 0 entries). cf3c9b5 is the squash of the reviewed head 5f7c162370, same tree. Reviewer findings
Delivery checks (passes 1–12)
Verified conform
Evidence
ClassificationThis change is classified N1 at cf3c9b5 (the Kernel ↔ Spring bridge: |
b77b126 to
2ed55ad
Compare
cf3c9b5 to
d9a953a
Compare
|
This PR also fixes a page upgraded by a Mechanism: Checked on a local 7.3.x-maintenance server, database restored to the same pre-upgrade checkpoint, one restart each:
|
…4293) **Symptom**: on a new platform, My Workspace > More > Content (the `global` site page `news`) displays an empty page instead of the Content app. **Cause**: the `global/news` page in `pages.xml` was made only of the `top-news-container`, `news-container` and `bottom-news-container` add-on containers. Their only contribution was the Content repository's former News portlet, which has been removed, so nothing fills the page. **Fix**: the containers are replaced by the `content/ContentListView` portlet declared directly in the page. The `global` site is imported in `insert` mode, so the change reaches only new installs; a `LayoutUpgradePlugin` entry (`GlobalNewsPageContentListUpgrade`, `content` profile, run once) re-imports the `news` page in `merge` mode on existing instances. No automated test covers these descriptors. **Depends on Meeds-io/portal#1338**, to merge after it. The upgrade's save evicts the page from `CachePageStorage` inside the upgrade's transaction, before the commit; social's asynchronous `PageSavedIndexingListener` reacts to the page update with `LayoutService.getPage`, reloads the previous committed page and caches it, so the restart that runs the upgrade keeps serving the empty containers until the next restart. portal#1338 evicts the page caches again once the transaction completes. Checked on a local server restored to a database whose `global/news` still holds the three containers, restarted once with this PR's `sites.war`: without portal#1338 the upgrade runs and stores the new page, while `/layout/rest/pages/layout?pageRef=portal::global::news` still returns the three `addonContainer`s; with portal#1338's jars it returns the `content/ContentListView` window. **Upgrade impact**: - On existing instances, `merge` replaces the whole stored `global/news` page with its shipped definition: an administrator's layout changes, or narrowed access or edit permissions, on that page are reset and must be re-applied. - The `top-news-container`, `news-container` and `bottom-news-container` add-on containers no longer exist on that page: a customer `dynamic-container-configuration.xml` that contributes to them no longer renders there. Knowledge: Meeds-io/eng-standards#488, Meeds-io/eng-standards#492 This change is classified N1 (upgrade plugin rewriting persisted page data, page permissions included), computed at 23586c4 — its approver must be an Archi/Dev who knows it is N1, not an approval on AI review alone; author ≠ approver. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…4294) **Symptom**: on a new platform, My Workspace > More > Content (the `global` site page `news`) displays an empty page instead of the Content app. **Cause**: the `global/news` page in `pages.xml` was made only of the `top-news-container`, `news-container` and `bottom-news-container` add-on containers. Their only contribution was the Content repository's former News portlet, which has been removed, so nothing fills the page. **Fix**: the containers are replaced by the `content/ContentListView` portlet declared directly in the page. The `global` site is imported in `insert` mode, so the change reaches only new installs; a `LayoutUpgradePlugin` entry (`GlobalNewsPageContentListUpgrade`, `content` profile, run once) re-imports the `news` page in `merge` mode on existing instances. No automated test covers these descriptors. **Depends on Meeds-io/portal#1338**, to merge after it. The upgrade's save evicts the page from `CachePageStorage` inside the upgrade's transaction, before the commit; social's asynchronous `PageSavedIndexingListener` reacts to the page update with `LayoutService.getPage`, reloads the previous committed page and caches it, so the restart that runs the upgrade keeps serving the empty containers until the next restart. portal#1338 evicts the page caches again once the transaction completes. Checked on a local server restored to a database whose `global/news` still holds the three containers, restarted once with this PR's `sites.war`: without portal#1338 the upgrade runs and stores the new page, while `/layout/rest/pages/layout?pageRef=portal::global::news` still returns the three `addonContainer`s; with portal#1338's jars it returns the `content/ContentListView` window. **Upgrade impact**: - On existing instances, `merge` replaces the whole stored `global/news` page with its shipped definition: an administrator's layout changes, or narrowed access or edit permissions, on that page are reset and must be re-applied. - The `top-news-container`, `news-container` and `bottom-news-container` add-on containers no longer exist on that page: a customer `dynamic-container-configuration.xml` that contributes to them no longer renders there. Knowledge: Meeds-io/eng-standards#488, Meeds-io/eng-standards#492 This change is classified N1 (upgrade plugin rewriting persisted page data, page permissions included), computed at 23586c4 — its approver must be an Archi/Dev who knows it is N1, not an approval on AI review alone; author ≠ approver. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- Ensure to provide github issue and task id in the title --> <!-- Choose between feat and fix in the title to differenciate a new feature from a fix --> <!-- Title format must be : feat: FEATURE TITLE - MEED-XXXX - Meeds-io/MIPs#1234 or fix: Fix TITLE - MEED-XXXX - #1234 --> <!-- Description : describe the feature/the fix by answering theses questions : --> <!-- Why is this change needed?--> <!-- Prior to this change, ...--> <!-- How does it address the issue?--> <!-- This change ...--> <!-- Tips : Try To Limit Each Line to a Maximum Of 72 Characters Provide links or keys to any relevant tickets, articles or other resources Remember to - Capitalize the subject line - Use the imperative mood in the subject line - Do not end the subject line with a period - Separate subject from body with a blank line - Use the body to explain what and why vs. how - Can use multiple lines with "-" for bullet points in body --> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
d9a953a to
2ea8e42
Compare
ahamdi
left a comment
There was a problem hiding this comment.
AI review — Round #1
First independent round, at 2ea8e4201f — a rebase of the self-reviewed cf3c9b53a3 onto 15e587bb1b (identical patch-id). Whole diff read (24 files), Hibernate 7.4.5 / Spring 7.0.9 claims checked against their sources. Two 🟡, two 🟢 inline, plus the body-level items below.
Body-level
- 🟢 Test evidence is on the pre-rebase SHA. The close-out's result line (
mvn -B -ntp clean verify javadoc:javadoc -Pcoverage→ 1688 tests) ran atcf3c9b53a3; the head now sits on a newer base (EXO-91484). Same patch-id, different base: please post the result line at2ea8e4201f(theTestDataStorageruns in InitContainer5 and, throughTestModelStorage, InitContainer8 included). - 🟢 The classification line cites a SHA
feature/maintenancewill never contain. "classified N1 (at cf3c9b5: …)" lands in the squash commit message (squash_merge_commit_message: PR_BODY). Fix: drop "at cf3c9b5" and keep the surfaces.
Verified conform
- Spring bridge (spring-orm/spring-tx 7.0.9):
JpaTransactionManager.doGetTransactionpicks the boundThreadEntityManagerHolder,hasTransaction()reads the overriddenisTransactionActive(), so aREQUIREDcall participates;JpaTransactionObject.setRollbackOnlymarks the kernelEntityTransaction;synchronizedWithTransaction = truekeepsEntityManagerFactoryUtilsfrom joining/closing the EM. The resource key is the same object on both sides (PersistenceUnitIntegration.entityManagerFactory()returnsentityManagerService.getEntityManagerFactory()). - Aspect paths: every exit of
proceedInNewTransaction(begin failure, throw, rollback-only, failed commit, throwing after-commit) reachesend(status), which unbinds and clears;close()is idempotent. Nesting: atomic-in-atomic and atomic-in-@ExoTransactionaljoin; atomic inside a Spring transaction holding its own EM getsNONE. - Hibernate 7.4.5: local synchronizations fire on commit, failed commit and rollback, and the registry is cleared after each notification (no pile-up on a request-long EM);
isActive()is false after a failed commit, so theRollbackExceptionbranch reportsROLLED_BACKcorrectly. - Cache evictions: all five caches still evict at once and only add the after-completion repeat;
TransactionCallbacksreturnsfalsewith no active transaction, so nothing previously immediate is deferred or lost. TheCacheSiteStorageoverrides run their eviction aftersuper's advised execution has committed. - Site read/save:
SiteDAOImpl.refresh(detach + find) reads the database (no second-level cache); thelayoutUnresolvedflag is not copied byrestoreSite'skeepSiteSettings, so a restore repairs an unresolved site;remove(PortalData)dispatches to the newCacheSiteStorage.remove(SiteKey)override. restoreSite: one save per declaring config,PORTAL_CONFIG_UPDATED/CREATEDas the Upgrade impact says, none on a skipped import.- Accepted limits, stated in the body and not re-raised: the template clone ignoring the flag on
create,PAGE_DESTROYEDbroadcast before the commit (no production listener onEventType.PAGE_*), theclearFuturesinterleaving not pinned. - Tests:
TestDataStoragepins atomicity through the woven advice on a real kernel container;ContainerTransactionalRepositoryTestuses a real Spring Data proxy over HSQLDB; new unit tests on Jupiter +MockitoExtension, matched by surefire's default includes. - Hygiene: new-file headers only carry 2026; no change history in comments; no i18n change.
Knowledge:— Meeds-io/eng-standards#405 merged, #409 a draft carrying this delivery's norm/pitfall update, as expected for code not yet ondevelop.
What the PR does well: the Spring join is built on the engine's own contracts (isTransactionActive override, synchronizedWithTransaction, scope closed before the commit) rather than around them; every trade-off is either pinned or written in Known limits with precise wording; the in-flight-load race is pinned deterministically with latches; and the fix also closes the stale-page-after-LayoutUpgradePlugin case reported in the thread.
Classification: this change is classified N1 (the Kernel ↔ Spring bridge — ContainerTransactionalAspect / ContainerTransactional#atomic, woven into every addon — and the SiteStorageImpl DAO writes). It must be validated by an Architect/Developer who knows it is N1, not auto-merged on AI review alone, and not approved by its author.
🤖 Generated with Claude Code
2ea8e42 to
5df08c5
Compare
|
Rebased onto
Only |
|
On review 5572003775's body items, at
@MayTekayaa, for EXO-91419: the eviction your upgrade case relies on is unchanged. |
ahamdi
left a comment
There was a problem hiding this comment.
AI review — Round #2 (follow-up)
Reviewed at 5df08c5600. The branch was rebased onto faf570360f (#1344's merge); 739cbf8cd9 is patch-identical to the Round #1 head and was not re-read. The six new commits (189a82a087 … 5df08c5600, 37 files) and the live PR body were read in full.
Status of Round #1
| # | Finding | Status |
|---|---|---|
| 1 | 🟡 A joined atomic call dooms the caller's transaction silently | ➖ The marking is accepted, per the thread reply. The WARN is in (ContainerTransactionalAspect.java:110-115) and Upgrade impact lists the marking. The outcome change 1fc83a6b45 adds on top of it is new finding R2-1 below. |
| 2 | 🟡 Nested types SpringTransactionScope / ThreadEntityManagerHolder |
✅ Fixed in 927b301845, verified: both are top-level package-private classes in io.meeds.common. |
| 3 | 🟢 Spring scope not asserted closed after the call; begin() failure unpinned |
✅ Fixed in 927b301845, verified: post-call isSpringScopeClosed() in the commit and rollback tests, plus testCloseTheSpringScopeWhenTheTransactionCantBegin. |
| 4 | 🟢 Node-write evictions unpinned | ✅ Fixed in 8a42ffb983, verified: TestDataStorage#testUpdatedNodeCacheIsClearedAgainOnceTheTransactionCompletes. The in-flight invalidation stays a stated Known limit. |
| 5 | 🟢 Test evidence not at head | ❌ Still open, see below. |
| 6 | 🟢 Classification line cites cf3c9b53a3 |
✅ Fixed: the line names no SHA. |
Body-level
-
🟡
Knowledge: pending — EXO-91183: not approvable until replaced (pr-conventions.md§3). The line must name the corpus PR(s) this delivery owes (the new restore/startup contract, theExoTransactionaloutcome change), ornone — issue-first|doc-neutral. That update has to amend the pending pitfallan-entitymanager-exception-caught-inside-an-exotransactional-transaction-rolls-it-back-without-a-word(Meeds-io/eng-standards#560, draft). Its title and Check ("rolls the whole transaction back at its end, and nothing is thrown") turn false with1fc83a6b45. -
🟡 The Fix bullet on unknown import modes contradicts the code. The bullet says "At startup, a configuration whose site type or import mode is unknown is left out of the sites imported from all their configurations, and imported on its own"; commit
5df08c5600says the same. That holds for an unknown type only. For an unknown mode:getOverwrittenSitesskips only that configuration, so its site stays overwritten;withoutSitesremoves the site from the configuration's owninitPageDB/initPageNavigationDB(NewPortalConfigListener.java:233-248);getPortalConfigsstill returns it, soimportSitePages/importSiteNavigationimport its pages and navigation inMERGE, with no error logged.
The Startup contract change sub-bullet states this correctly, so the two bullets disagree. Fix: scope the Fix bullet (and the commit message) to an unknown site type.
-
🟢 R1-5, still open: test evidence is not at head. The full run (1720 tests) "ran on the tree of the head before the rebase onto #1344's merge" and names no SHA. The 43-test line after the rebase covers only
component/common:TestRestoreSiteConfigurations,TestDataStorageand the import tests have no result at5df08c5600, which now sits on #1344's change to the DAO proxy's exceptions. Fix: post the result line of thecomponent/portalsuites (InitContainer1, 5, 8, 10 at least) at5df08c5600. -
🟢 Follow-ups describe a state the base has already ended. "web-conferencing task 91299 (kernel DAO JPA exceptions arrive wrapped, so its storage error handling never runs)": #1344 fixed that, and its merge
faf570360fis this PR's base. Fix: drop the item, or reword it to what is left on the web-conferencing side. -
🟢 The Startup contract change should say outright that admin-created content goes. "then deletes the pages and nodes that none of them defines" is accurate. On an irreversible-deletion surface, say plainly that this includes the pages and nodes administrators created in the UI in that site, at every start. That behaviour is unchanged from the previous
PageImporterOVERWRITEremovePagesand navigationdestroyOrphan. Fix: add "pages and nodes created in the UI in that site are deleted at every start, as before".
Verified conform
ExoTransactionalAspect, every path (ExoTransactionalAspectTestpins all six through the package-privateproceedseam):- a method that throws keeps its own exception, and a commit failure still replaces it;
- only a method that returned over a rollback-only transaction it began throws
RollbackException; a joined call never does; - the EntityManager the advice started is closed in
finallyon every outcome.
ContainerTransactionalAspectafter the extraction: behaviour-identical, plus the WARN.PageStorageImpl:deletePage/savePageLayoutare atomic with their former bodies, and thePAGE_DESTROYED/PAGE_UPDATEDbroadcast comes after the atomic call returns.TestDataStoragepins a page delete and a layout save failing midway through the woven advice.- Restore model:
- every configuration is imported without deleting, then what none defines is deleted once;
NavigationOrphanRemover/DefinedNavigationNodereproduceImportMode'sdestroyOrphan/destroyNestedOrphanand the fragment importer's top-level depth rule;- user sites are skipped by the page-orphan removal;
- all pages are imported before any navigation;
- a configuration whose
pages.xml/navigation.xmlfails to parse aborts the deletion:getConfigthrows and the outer catch deletes nothing.
- Startup: an unknown site type no longer aborts the startup import (fixture
contribution9). - Tests run:
InitContainer10TestSuitematches surefire's**/*TestSuite.javainclude.TestNavigationFragmentImporterRestoreDefaultsnow runs inInitContainer1TestSuite.TestRestoreSiteConfigurationsasserts what the body claims: a later configuration's page is linked, another site's node keeps its page across an overwrite and a startup, nested custom nodes are removed or kept per mode, and nothing is deleted on an unparsable configuration.
What the push does well: the restore is rebuilt around one rule: import everything without deleting, then delete once what no configuration defines. That removes the order dependence behind Symptom 2 rather than patching each mode. The fixtures are precise (a cross-site page reference, a node pointing at a later configuration's page, a malformed file, an unknown-type configuration). The ExoTransactionalAspect change is small and keeps every prior outcome except the one it targets, pinned path by path.
Classification: this change is classified N1, as the body states: the Kernel ↔ Spring bridge, the outcome of every @ExoTransactional method, the JPA transaction boundaries of the site and page writes, and the irreversible deletion of pages and navigation nodes by a restore and by the startup import. It must be validated by an Architect/Developer who knows it is N1, not auto-merged on AI review alone, and not approved by its author.
🤖 Generated with Claude Code
faf5703 to
7b35dfa
Compare
5df08c5 to
6ac620b
Compare
|
Rebased onto
Then four new commits:
Meeds-io/social#6245 goes with this PR: without it, a failing metadata init plugin fails the start. |
|
A correction to my comment 6271760414 for EXO-91419: the outcome "a page write fails, so the upgrade fails visibly" needs two commits, not one.
A site write needs |
Self-review close-out — rounds 4 to 10Rounds 1 to 3 were closed out in an earlier comment. Rounds 4 to 9 ended NOT CLEAN; round 10, a fresh reviewer continued by message to verify its own items, ended CLEAN. The last rounds found no defect in the code paths, only wordings of this body. Status table
Verified conformThe last two rounds checked the body's every new sentence against the code at the pushed head. They found these hold: the startup and restore contract (pages in place, one orphan removal, nothing removed when a configuration can't be parsed), the root rule of the navigation sweep ( Tests
ClassificationThis change is classified N1 at Open items for a human
🤖 Generated with Claude Code |
ahamdi
left a comment
There was a problem hiding this comment.
AI review — Round #3 (follow-up)
Reviewed at 5a4f5ef206.
- The rebase changed nothing. The branch was rebased onto
7b35dfa4c2, the force-pushedfeature/maintenance:git diff faf570360f 7b35dfa4c2is empty, andgit range-diffmaps the seven reviewed commits=. - Read in full: the four new commits (
e793a92f50,dca02911cd,982bcbb527,5a4f5ef206) and the live PR body.
Status of previous findings
| # | Finding | Status |
|---|---|---|
| R1-1 | 🟡 A joined atomic call marks the caller's transaction rollback-only | ➖ Architects Lead decision; the author's reasons are in the thread. The thread is left for a human to close. |
| R1-2 – R1-4, R1-6 | Nested types, Spring-scope pins, node-eviction pin, classification SHA | ✅ (Round #2) |
| R1-5 | 🟢 Test evidence not at head | ✅ The Tests line runs on the tree of 5a4f5ef206 (c1852b61fa, matched against HEAD^{tree}): 1727 tests, 0 failures. |
| R2-1 | 🟡 Callers of the new RollbackException missing from Upgrade impact |
✅ for the body, ➖ for the shipping. The body now names the class of callers and each caller's outcome. I verified the outcomes in their repos on feature/maintenance. Social's UserDAOImpl runs each listener in an organization-service request, not a JPA transaction, so the listener's own transaction throws and its exception is logged as a WARN. Gamification's ProgramSpaceListener runs in runAsync, whose future nobody reads. Commons' SendEmailNotificationJob catches it and logs an ERROR. Architect decision: the throw ships for every caller, as argued in the thread. MetadataServiceImpl#start is handled by Meeds-io/social#6245, which the Release order puts before or with this PR. |
| R2 orphan race | 🟡 A concurrently removed orphan page stopped the removal of the others | ✅ Fixed in 5a4f5ef206, verified: per-page try with a WARN, pinned by NewPortalConfigListenerTest#testOverwriteRemovesTheOtherOrphanPagesWhenOneCantBeRemoved. |
| R2-2 | 🟢 "can't be read" vs "can't be parsed" | ✅ Fixed in 982bcbb527, verified: both Javadocs and the body say "can't be parsed", and that an unopenable file defines nothing. |
| R2 body | 🟡 Fix bullet on unknown import modes | ✅ Now scoped: "left out of the overwritten sites; on a site no other configuration overwrites, its own import logs the error". It matches the Startup contract change sub-bullet. |
| R2 body | 🟢 Stale web-conferencing follow-up | ✅ Now points at exoplatform/web-conferencing#421. |
| R2 body | 🟢 Admin-created content deleted at start | ✅ The Startup contract change says it plainly, including the broader node sweep, which is a change from before. |
| R2 body | 🟡 Knowledge: pending |
❌ Partly: see below. |
Body-level
-
🟡 The
Knowledge:line misses two corpus updates this PR owes. The line now readsMeeds-io/eng-standards#586, #587(drafts: the restore and startup pitfalls, and the portal, layout and digital-workplace domain docs). Two things are missing:- Meeds-io/eng-standards#409 is no longer named. It is still open, and it carries ADR-0010 and the
backend-spring.md§4 /container-transactional-opens-no-database-transactionrewrite for@ContainerTransactional(atomic = true), which this PR introduces. - No open corpus PR amends the pitfall this PR makes false.
an-entitymanager-exception-caught-inside-an-exotransactional-transaction-rolls-it-back-without-a-word(Meeds-io/eng-standards#560, draft, atde71ed7e) is still titled "…rolls the whole transaction back at its end, and nothing is thrown". Since1432792e05, the method that began the transaction throwsRollbackException. Neither #586, #587 nor #409 touches that file, and #409's norm and ADR say nothing about the@ExoTransactionalthrow.
Fix: name #409 again, and amend that pitfall (in #560, or in one of this delivery's PRs) to state the new outcome; then list the PR that carries it in the line.
- Meeds-io/eng-standards#409 is no longer named. It is still open, and it carries ADR-0010 and the
Verified conform
importSitePagesnow callscreatePageper configuration (e793a92f50), withoutinitPageDB'screatedOwnersgate:- on a restore, that is the fix: a site no configuration overrides is no longer skipped after a restart;
- at startup, the gate still holds, because
getOverwrittenSitesonly selects owners increatedOwners; createPageopens its ownRequestLifeCycle, and a configuration that fails is logged per configuration and doesn't stop the others;- pinned by
TestRestoreSiteConfigurations#testRestorePagesImportsThePagesOfASiteThatTheStartupDidntImport.
@ExoTransactionaland@ContainerTransactional#atomicJavadoc (dca02911cd) match the aspects. A method that returned throwsRollbackException. A method that threw keeps its exception, unless ending the transaction fails. A joined method never throws it.- The caller list in the Upgrade impact, as above.
What this push does well: every Round #2 item is answered in the code or the body, and the replies give each decision's reasons rather than its outcome alone. The restore-pages gap found by the author's own loop (e793a92f50) is fixed with a pin that reproduces the restart condition.
Classification: this change is classified N1: the Kernel ↔ Spring bridge, the outcome of every @ExoTransactional method, the JPA transaction boundaries of the site and page writes, and the irreversible deletion of pages and navigation nodes by a restore and at startup. It must be validated by an Architect/Developer who knows it is N1, not auto-merged on AI review alone, and not approved by its author.
🤖 Generated with Claude Code
… pages EXO-91183 A site save replaced its layout one DAO call at a time: a concurrent reader could read the site row while its root container was already deleted and get an empty layout, which the site restore then saved back over the restored one. - @ContainerTransactional(atomic = true) runs the method in one JPA transaction, committed when it returns and rolled back when it throws or when a failed call marked it rollback-only (RollbackException). The kernel EntityManager writes join it; when the method begins it, the Spring transactional methods and Spring Data repositories join it too and their callbacks run with its outcome. SiteStorageImpl create, save and remove use it. - The site, layout, page, navigation and description caches evict their entries again once the transaction completes, after invalidating the loads in flight (TransactionCallbacks.afterCompletion); FutureCache.clearFutures invalidates every load it removes. - A site read with a missing root container is read again once and flagged as unresolved; saving it keeps the stored layout. - The site restore applies the previous site settings to the imported site instead of reading the site again after the import and saving it a second time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…figuration of the site EXO-91183 A site restore imported each configuration of the site in turn with the requested mode. In overwrite mode, each configuration deleted the pages the previous ones had just imported, and the navigation nodes they had imported under the nodes it defines or attaches a fragment to. In restore-defaults mode, a configuration deleted the nested nodes that a previous configuration had imported under a node they both define, unless it defined them too: the nodes a restore kept depended on the order of the configurations. An overwrite also deleted and re-created every page of the site, so a navigation node of another site that references one of them lost its page: an overwrite restore of the global site left the People and Spaces nodes of the dw site without a page (reproduced on a local server). An overwrite restore of the global site logged, for every second of its configurations (reproduced on a local server): NewPortalConfig error: Error while committing the transaction [org.hibernate.TransientPropertyValueException: Persistent instance of 'org.exoplatform.portal.jdbc.entity.NodeEntity' references an unsaved transient instance of 'org.exoplatform.portal.jdbc.entity.PageEntity' (persist the transient instance before flushing) [org.exoplatform.portal.jdbc.entity.NodeEntity.page -> org.exoplatform.portal.jdbc.entity.PageEntity]] Can't find Window with ids: ... The page deletion was refused at commit, while the page windows were already deleted in their own transactions: the page kept a layout whose windows no longer exist, and the pages of that configuration weren't imported. - An overwrite deletes the site pages that none of the configurations defines, then imports the pages of each configuration in place, so that they keep the nodes of other sites that reference them. Nothing is deleted when the pages of a configuration can't be read. - The navigation of each configuration is imported without deleting any node, then NavigationOrphanRemover deletes the nodes that none of the configurations defines, as the requested mode does: every such node in overwrite mode, the nodes nested in a default node in restore-defaults mode. As the import of each fragment did, a node directly under a fragment attachment point, or under one of the fragment top-level nodes, is a top-level section that restore-defaults mode keeps, and a node only in the path of an attachment point keeps its child nodes. Nothing is deleted when the navigation of a configuration can't be read. - PageStorageImpl deletes a page, and replaces a page layout, in one transaction, and broadcasts the page event once that write returns: after its commit, unless the write joins an active transaction, as a site removal does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…om all its configurations EXO-91183 At startup, each configuration of a site was imported with its own mode, one after the other. A configuration in overwrite mode (a deployer setting such as exo.agenda.portalConfig.metadata.importmode=overwrite, or an extension) deleted every page of the site, and the navigation nodes the other configurations imported under the nodes it defines, at every start: the pages and nodes of the configurations imported before it were lost, and the pages it imported again got new identifiers, so the nodes of other sites that reference them lost their page. - A site that one of its configurations overwrites is imported from all its configurations, whatever their own mode, as an overwrite restore of the site does: the pages that no configuration defines are deleted, the pages of every configuration are imported in place, the navigation of every configuration is imported without deleting any node, then the nodes that no configuration defines are deleted. The pages are imported before any navigation, as before. - NewPortalConfigListener#importSitePages and #importSiteNavigation hold that sequence for the startup import and for UserPortalConfigService#restoreSite. The startup import keeps importing the site layout of each configuration with its own mode. - A restore imports the site layout of every configuration (NewPortalConfigListener#importSiteLayout), then their pages, then their navigation: a node that references a page of a later configuration of the site is now linked to it. An extension that overwrites a site no longer deletes the pages and nodes that the other configurations of the site define: they are kept, and the extension's own pages and nodes are merged over them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…led back EXO-91183 A @ContainerTransactional(atomic = true) method that fails inside an active kernel transaction marks it rollback-only, and ExoTransactionalAspect rolls a rollback-only transaction back without throwing: an @ExoTransactional caller that catches the failure and goes on, such as social's LayoutUpgradePlugin#updateLayout, lost all its writes with no trace. - The aspect logs a warning naming the method and the failure when it marks the joined transaction rollback-only. - SpringTransactionScope and ThreadEntityManagerHolder are top-level, package-private classes. - The Spring scope is closed when the transaction can't begin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-review close-out — round 11Round 11 answers the external nit 4323707015. It has no reviewer round. Instead, two delivery-checks passes and a refuter covered the change, and their entries are fixed or listed below. The head is The branch was rebased onto
Status table
Tests
ClassificationThis change is classified N1 at Open items for a human
🤖 Generated with Claude Code |
ahamdi
left a comment
There was a problem hiding this comment.
AI review — Round #4 (final)
Reviewed at 2d20d07490.
- The rebase changed nothing. The branch was rebased onto
feature/maintenance@3fd575ffd7(#1345's merge), andgit range-diffmaps the eleven reviewed commits=. - Read in full: the one new commit
2d20d07490and the live PR body.
Status of previous findings
| # | Finding | Status |
|---|---|---|
| R1-1 | Joined atomic call marks the caller's transaction rollback-only | ➖ Architects Lead decision; the thread is left for a human to close |
| R1-2 – R1-6 | Nested types, Spring-scope and node-eviction pins, test evidence, classification SHA | ✅ (Rounds #2–#3) |
| R2-1 | Callers of the new RollbackException |
➖ Architect decision (ships for every caller); the body lists each caller's outcome. The thread is left for a human to close |
| R2 orphan race, R2-2, R2 body items | Per-page removal, "can't be parsed", unknown import modes, follow-ups, admin-created content | ✅ (Round #3) |
| R3 | 🟡 Knowledge: line |
✅ Now Meeds-io/eng-standards#409, #560, #586, #587 (all drafts, as expected for code not yet on develop). #409 is named again. The #560 pitfall is retitled "…and the method that began it throws RollbackException", matching 1432792e05. |
| R3 | 🟢 Javadoc of the orphan selection window | ✅ Fixed in 2d20d07490, verified: the removePageOrphans Javadoc states the single selection, the per-page re-read, and that a page created since is kept unless it is named after an orphan |
Verified conform: 2d20d07490
PageStorageImpl#deletePagere-reads the page row. It callsPageDAO#refresh, built likeSiteDAO#refresh: detach, thenfind. So it walks the layout as stored, not the state the request's persistence context kept from an earlier read (thefindPagesselection, or a restore's navigation read).- It runs inside the atomic transaction, and
nullwhen the page is already gone returnsfalse, as before. - The in-memory test DAO implements
refresh.
- It runs inside the atomic transaction, and
- The pin is
TestRestoreSiteConfigurations#testOverwriteRemovesTheLayoutSavedOnAnOrphanPageSinceTheRequestReadIt, inInitContainer10TestSuite, on a real container. APAGE_REMOVEDlistener saves a container on the other orphan from another request between the two removals, and the test asserts that container is gone after the restore. - The windows that remain are stated in Known limits: the re-read takes no lock, a page layout replace and the site writes still walk the first-read body, and the
PAGE_REMOVEDpayload can be older than the row the delete removes. The layout replace and site writes are also listed as a follow-up. This is accepted as stated, given the hot-path cost the body names.
Note (non-blocking)
- The full-suite result line names tree
c32ee4f22f, described as "the head without theUserACLchange of #1345". That object can't be found locally or on GitHub, and the head's tree is9303ebe813. Each part is evidenced at its own head (this PR's suite, and #1345's suite atde4674777c), but the combined tree isn't. A result line at2d20d07490would close it.
What the push does well: pinning the Javadoc nit on a real engine turned up a real defect (the stale-body delete), which the author fixed at its root, deletePage, so every page delete benefits, not only the restore. Every window that remains is written down with its reason.
All findings from previous rounds are resolved or recorded as owner decisions; nothing is outstanding from the AI review side.
Classification: this change is classified N1: the Kernel ↔ Spring bridge, the outcome of every @ExoTransactional method, the JPA transaction boundaries and DAO reads of the site and page writes, and the irreversible deletion of pages and navigation nodes by a restore and at startup. It must be validated by an Architect/Developer who knows it is N1, not auto-merged on AI review alone, and not approved by its author.
🤖 Generated with Claude Code
A page layout save, a page properties save, a page clone, a site save and a site removal read their entity through findByKey. When the request had already read that page or site, for instance through a node load that reads the pages its nodes reference, the query read the row, but Hibernate kept the state of the instance it already managed. Each write then worked from that earlier state: - a page layout save, a site save and a site removal left the containers and windows another request had saved since in storage, unreferenced; - a page properties save wrote the earlier page body back, every column being written, over the layout saved since; - a page clone copied the earlier layout. Each of these writes now reads its row again (PageDAO#refresh, SiteDAO#refresh), as a page delete does. The row is read without a lock: a layout committed between that read and the write is still missed, and a properties save still writes the body it read back over it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-review close-out — round 12Head The commit extends Status table
TestsAt ClassificationThis change is classified N1 at 🤖 Generated with Claude Code |
A page row has two writers, its properties save and its layout save, and PageEntity carries no version. Hibernate wrote every column of the row on each update, so a properties save wrote back the page body it had read, over a layout another request had committed since. PageEntity is @DynamicUpdate: an update writes only the columns it changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-review close-out — round 13Head A page row has two writers, its properties save and its layout save, and no version. That is backend-spring §4's multi-writer case, so Status table
TestsThe mutation ran on the tree before a comment-only amend. At the head ClassificationThis change is classified N1 at 🤖 Generated with Claude Code |
EXO-91183 With PageEntity's dynamic update, the properties save's re-read no longer guards the page body: it makes each property be compared with the row as stored. A property another request changed since the request read the page, saved back to the value the request first read, is written only when the save compares it with the stored row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-review close-out — round 14Head
Tests at This change is classified N1 at 🤖 Generated with Claude Code |
|
ahamdi
left a comment
There was a problem hiding this comment.
AI review — Round #5 (final)
Reviewed at 65f8e9a240. The branch was not rebased: 2d20d07490 (the Round #4 head) is an ancestor of the head. Read in full: the three new commits (a2991682af, 3b43334f47 and 65f8e9a240), PageDAOImpl#refresh and SiteDAOImpl#refresh, the mappings of PageEntity and NodeEntity, and the live PR body. A delivery-checks pass over 2d20d07490..65f8e9a240 returned no entry.
Status of previous findings
| # | Finding | Status |
|---|---|---|
| R1-1 | A joined atomic call marks the caller's transaction rollback-only | ➖ Architects Lead decision |
| R1-2 – R1-6 | Nested types, Spring-scope and node-eviction pins, test evidence, classification SHA | ✅ (Rounds #2–#3) |
| R2-1 | Callers of the new RollbackException |
➖ Architect decision. The body lists each caller's outcome |
| R2 orphan race, R2-2, R2 body items | Per-page removal, "can't be parsed", unknown import modes, follow-ups, admin-created content | ✅ (Round #3) |
| R3 | Knowledge: line; Javadoc of the orphan selection window |
✅ (Round #4) |
| R4 note | The full-suite result line named a tree that isn't the head | ✅ The Tests line now gives the targeted run at 65f8e9a240 (18 classes, 148 tests, 0 failures) with its command, and says the full suite wasn't run at this head |
Verified conform: 2d20d07490..65f8e9a240
- The five writes re-read their row the way
deletePagedoes. These arePageStorageImpl#savePageLayout,#savePage(PageContext)and#clone, andSiteStorageImpl#save(PortalData)and#remove(SiteKey). Each one callsrefresh(detach, thenfind) afterfindByKey.- Detaching drops no pending write.
findByKeyis a JPQL named query onPORTAL_PAGESorPORTAL_SITES, so under AUTO flush it flushes the dirty entities of that table before the detach. - Detaching doesn't cascade.
PageEntity#ownerdeclares no cascade, and the managedNodeEntityrows that point at the old instance keep a non-transient reference with an id. SiteStorageImpl#removenow throwsNoSuchDataExceptionwhen the row is gone betweengetPortalConfigand the write. Before, it threw an NPE.
- Detaching drops no pending write.
@DynamicUpdateonPageEntityfollowsbackend-spring.md§4: two writers, no@Version. It has the real-SQL column-set pin that the norm asks for,testPageRowUpdateWritesOnlyTheColumnsItChanged. That pin parses the storedPAGE_BODYafter a stale-instancePageDAO#update.- Each re-read has its own pin, and each pin asserts that the other request's save landed first (
addPageContainerInAnotherRequestandaddSiteContainerInAnotherRequest). Without that, the pins would prove nothing. The@DynamicUpdateannotation makes the layout-keeping pin pass even without the properties save's re-read.testSavePagePropertiesWritesBackAPropertyChangedSinceTheRequestReadThePagecloses that gap: without the re-read, the stale managed instance already holdsOriginal, Hibernate sees no dirty column, and the row keepsOther. The test asserts that outcome. - The body matches the code. Fix and Upgrade impact list the same five re-reads and the extra SELECT by id. Known limits no longer counts the properties save among the no-lock writes, and the stale-body follow-up of Round #4 is gone because this push fixes it. The commit messages state the remaining no-lock window.
Knowledge:Meeds-io/eng-standards#409, #560, #586, #587are all open drafts, as expected for code not yet ondevelop. #586's pending ledger file covers all six re-reads, the@DynamicUpdateannotation and the four new pins.
What the push does well: it closes, in this PR, the follow-up that Round #4 accepted as a Known limit. It also caught that @DynamicUpdate had silently made one of its own pins vacuous, and wrote the pin that tells the re-read and the annotation apart.
All findings from previous rounds are resolved or recorded as owner decisions; nothing is outstanding from the AI review side.
Classification: this change is classified N1: the Kernel ↔ Spring bridge, the outcome of every @ExoTransactional method, the JPA entity and the JPA transaction boundaries and DAO reads of the site and page writes, and the irreversible deletion of pages and navigation nodes by a restore and at startup. It must be validated by an Architect/Developer who knows it is N1, not auto-merged on AI review alone, and not approved by its author. The approval given at 2d20d07490 was dismissed by the new commits, so it is owed again at 65f8e9a240.
🤖 Generated with Claude Code
… reload EXO-91183 (#232) **Symptom**: the comment on the `administration` declaration of `portal-configuration.xml` says that reloading only the exo part of the site makes the `PageImporter` remove Meeds' administration pages, and recommends `io.meeds.administration.portalConfig.metadata.importmode=overwrite` to reload either part. With Meeds-io/portal#1338 the first is false, and the recommended property also reloads the `global` site, which the comment doesn't say. **Cause**: with Meeds-io/portal#1338, a startup that runs an `overwrite` declaration of a site imports the pages and the navigation of every declaration of that site, and removes the pages and nodes none of them defines. The `io.meeds.administration` properties also set the mode of this file's `global` declaration. **Fix**: the comment says that, with `overwrite`, either property reloads the pages and the navigation of all the administration declarations, Meeds' included, and removes the pages and nodes none of them defines, and that the `io.meeds.administration` property reloads `global` as well. Comment only; `xmllint --noout` on the file at `943e39f`: no output (well-formed). **Release order**: merge with or after Meeds-io/portal#1338, whose behaviour the comment describes. Knowledge: Meeds-io/eng-standards#409, #560, #586, #587 This change is classified N3: no REST/DAO/schema/ACL surface touched (a comment in a configuration file). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…it plugin's metadata on its own EXO-91183 (#6245) **Symptom**: - Search showed the content blocks of the pages of a space template, and of other sites users don't navigate (portal templates, user sites), to every user the page grants access to, with a link that leads nowhere. They were indexed on a full reindex, on every save of such a page through Layout, and on a space template's "Restore default pages", which refreshes the index of every page of the template with Meeds-io/layout#581. - With Meeds-io/portal#1338, a method whose transaction was marked rollback-only fails instead of returning. A metadata init plugin whose metadata can't be stored at startup would then fail the start of `MetadataServiceImpl`, and with it the portal container start. Before, the outcome depended on the database. On MySQL/MariaDB (identity ids, the INSERT runs at persist), the start logged a WARN, and the metadata of every plugin of that start were silently rolled back. On the databases whose ids come from a sequence, the INSERT waits for the flush: it failed at the commit of the start's transaction, failing the container start with no WARN, or, when a later plugin's lookup flushed it first, the start logged a WARN under that later plugin's metadata (its lookup threw the flush failure as `IllegalStateException: Error retrieving value with key …`), and the whole start was silently rolled back. **Cause**: - `PageContentIndexingConnector` builds no bare page document for a page of a site users don't navigate (`isIndexableSite`: portal and group sites), but built its content block documents, and listed them for a full reindex. `PageContentBlockIndexingListener` reindexed them on `layout.page.updated`. - `MetadataServiceImpl#start` created the metadata of every plugin in one `@ExoTransactional` transaction, and caught each plugin's failure to go on. **Fix**: - `PageContentIndexingConnector` builds no document, block or bare, for a page of a site users don't navigate, and a full reindex doesn't list the content blocks of such a page. - When such a page is updated through Layout (`layout.page.updated`, `layout.page.permissions.updated`), `PageContentBlockIndexingListener` removes every document the index holds for it, so the blocks indexed before leave search. - `MetadataServiceImpl#start` creates each plugin's metadata in a transaction of its own: a plugin that fails is logged (WARN), and the metadata of the others are created. - Pinned by `PageContentIndexingConnectorTest` (a block of a template page builds no document; a full reindex lists none), `PageContentBlockIndexingListenerTest` (a template page update unindexes its documents and reindexes none) and `MetadataServiceTest#testStartCreatesTheMetadataOfTheOtherPluginsWhenOneFails` (a real container: a metadata whose name is longer than its column, then a valid one). Each pin was mutation-verified on the committed tree. **Upgrade impact**: - The content blocks of the pages of space templates, portal templates and user sites (dashboards) leave search: on the next full reindex, or when such a page is next updated through Layout. The bare page documents of those sites were already not indexed. - At startup, the metadata of the init plugins are created one plugin at a time: a plugin that fails is logged under its own name, no longer rolls back the metadata of the others, and, on the databases whose ids come from a sequence, no longer fails the container start. **Known limits**: - A page of such a site saved outside Layout (a configuration re-import, for instance) builds no document, and the indexing framework skips a missing document instead of removing it: the blocks indexed before stay until the next full reindex, or the next update of the page through Layout. **Release order**: merge it before, or with, Meeds-io/portal#1338 and Meeds-io/layout#581. The metadata fix is needed by Meeds-io/portal#1338, which would otherwise turn a failing metadata init plugin into a failed start. The search fix is needed by Meeds-io/layout#581, whose space template restore would otherwise index the template's content blocks. **Tests**: `mvn -B -ntp -o clean verify javadoc:javadoc -Pcoverage -pl component/core` at `c2106c8844`: BUILD SUCCESS, 1357 tests, 0 failures, 0 errors (`MetadataServiceTest` 32, `PageContentIndexingConnectorTest` 33, `PageContentBlockIndexingListenerTest` 11), against the local `7.3.x-maintenance-SNAPSHOT` of Meeds-io/portal#1338, which carries its `RollbackException`. Knowledge: Meeds-io/eng-standards#409, #560, #586, #587 This change is classified N1 (proposed, recall-first) — the JPA transaction boundary over the kernel DAO writes of a component's start, whose failure fails the portal container start with Meeds-io/portal#1338, and which permission-bearing page documents search exposes. Its approver must be an Archi/Dev who knows it is N1, not an approval on AI review alone; author ≠ approver. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…d a site restore refreshes its pages' search index EXO-91183 (#581) **Symptom**: when the layout of a site couldn't be read, portal reads it with an empty layout (Meeds-io/portal#1338). Opening that site in the layout editor, creating a site from it as a template, or saving it as a site template would copy the empty layout over a real one. Today, creating the draft fails only by accident: a 500 from a NullPointerException in `LayoutModel`. **Cause**: `SiteLayoutService` `getSiteLayout`, `createDraftSite` and `createSite`, and `SiteTemplateService.saveAsSiteTemplate`, hand on the layout they read without checking that it was read. **Fix**: - These four methods throw `SiteLayoutUnresolvedException` for a site read with `PortalConfig#isLayoutUnresolved`, before anything is written, through one check (`SiteLayoutUnresolvedException#checkResolved`). `createSite` checks the template it copies; the other three check the site itself. - `SiteLayoutRest` and `SiteTemplateRest` answer that refusal with 409 Conflict, on five endpoints: `GET` and `PUT` of `sites/layout` (the update returns the site layout read again), `POST sites` (a site created from a template), `POST sites/draft`, and the site template created from a site. The layout editor and the site form show their error message, and the site template drawer stays open. The other failures of these requests keep their outcome. - Tests in `SiteLayoutServiceTest`, `SiteTemplateServiceTest`, `SiteLayoutRestTest` and `SiteTemplateRestTest`: - the refusal, and that nothing is written: no draft removed, no site or template created; - the resolved path; - the 409 for the new exception, and no 409 for another `IllegalStateException`. **Search index after a site restore**: a restore saves the pages and rebuilds the navigation of the site without the events of the editors. Since Meeds-io/portal#1338 saves the pages in place, the portal's page update event alone reaches the index, and it only queues the blocks the index doesn't hold yet. So a block the restore removed stayed in search, a block whose content it changed kept its old document, and a page whose last node it removed kept its documents. - After a restore of pages or navigation, `SiteLayoutService#restoreSite` compares the pages a node of the site leads to before and after the restore: - a page a node of the site leads to after the restore gets `layout.page.updated`, so its blocks and its path are reindexed, as after a page save; for a page of a site users don't navigate (a site template, a user site), Meeds-io/social#6245 removes its documents instead; - a page whose last node in the site was removed gets `layout.page.unreachable`, so its documents are unindexed, as after a node deletion; - a page no node of the site leads to, before or after the restore (reached, if at all, from another site, where the index resolves no path for it either), gets `layout.page.updated` when the restore covers the pages, and nothing otherwise. - A failure to read the navigation, before or after the restore, logs a WARN and leaves the restore done. - `NavigationLayoutService#getReachablePages` holds the walk that the node deletion already used. - Tests in `SiteLayoutServiceTest` (each case above, a navigation-only restore, both failures) and `NavigationLayoutServiceTest`. - Cost: two loads of the site's navigation tree, and one event per page of the site, per restore of pages or navigation. **Known limits**: - A restore checks which pages are still reached in the restored site's navigation only: a page whose last node in its own site the restore removes is unindexed, even when a node of another site still leads to it, as after a node deletion. - `SiteTemplateService.createSiteTemplate` copies the `basic` template through portal's template clone, which keeps the flag. - The site template export (`SiteTemplateDatabindPlugin`) writes the empty layout into the archive. - The site template drawer shows no message when its save fails. **Release order**: merge after Meeds-io/portal#1338 and its published `7.3.x-maintenance` snapshot. This PR compiles against `PortalConfig#isLayoutUnresolved`, so its CI fails until then. Merge it with or after Meeds-io/social#6245: without it, a space template's "Restore default pages" indexes the template's content blocks. **Tests**: - `mvn -B -ntp -o clean verify javadoc:javadoc -Pcoverage -pl layout-service` at `01d1b2bd`: BUILD SUCCESS, 388 tests, 0 failures, 0 errors. - It compiled against a local `7.3.x-maintenance-SNAPSHOT` of Meeds-io/portal#1338. No signature of #1338's `component/api` changed since its first pushed commit: its later commits change only `ExoTransactional`'s Javadoc there. Knowledge: Meeds-io/eng-standards#409, #560, #586, #587 This change is classified N2 — a REST contract change (409 when a site's layout couldn't be read, on the site layout read and update, the site and draft site creation, and the site template created from a site) and a cross-domain seam (a site restore broadcasts the page updated and page unreachable events that social's search indexing consumes to reindex or unindex the site's pages). Its approver must be an Archi/Dev who knows it is N2, not an approval on AI review alone. It requires Meeds-io/portal#1338 (N1) to merge first. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…it plugin's metadata on its own EXO-91183 (#6245) **Symptom**: - Search showed the content blocks of the pages of a space template, and of other sites users don't navigate (portal templates, user sites), to every user the page grants access to, with a link that leads nowhere. They were indexed on a full reindex, on every save of such a page through Layout, and on a space template's "Restore default pages", which refreshes the index of every page of the template with Meeds-io/layout#581. - With Meeds-io/portal#1338, a method whose transaction was marked rollback-only fails instead of returning. A metadata init plugin whose metadata can't be stored at startup would then fail the start of `MetadataServiceImpl`, and with it the portal container start. Before, the outcome depended on the database. On MySQL/MariaDB (identity ids, the INSERT runs at persist), the start logged a WARN, and the metadata of every plugin of that start were silently rolled back. On the databases whose ids come from a sequence, the INSERT waits for the flush: it failed at the commit of the start's transaction, failing the container start with no WARN, or, when a later plugin's lookup flushed it first, the start logged a WARN under that later plugin's metadata (its lookup threw the flush failure as `IllegalStateException: Error retrieving value with key …`), and the whole start was silently rolled back. **Cause**: - `PageContentIndexingConnector` builds no bare page document for a page of a site users don't navigate (`isIndexableSite`: portal and group sites), but built its content block documents, and listed them for a full reindex. `PageContentBlockIndexingListener` reindexed them on `layout.page.updated`. - `MetadataServiceImpl#start` created the metadata of every plugin in one `@ExoTransactional` transaction, and caught each plugin's failure to go on. **Fix**: - `PageContentIndexingConnector` builds no document, block or bare, for a page of a site users don't navigate, and a full reindex doesn't list the content blocks of such a page. - When such a page is updated through Layout (`layout.page.updated`, `layout.page.permissions.updated`), `PageContentBlockIndexingListener` removes every document the index holds for it, so the blocks indexed before leave search. - `MetadataServiceImpl#start` creates each plugin's metadata in a transaction of its own: a plugin that fails is logged (WARN), and the metadata of the others are created. - Pinned by `PageContentIndexingConnectorTest` (a block of a template page builds no document; a full reindex lists none), `PageContentBlockIndexingListenerTest` (a template page update unindexes its documents and reindexes none) and `MetadataServiceTest#testStartCreatesTheMetadataOfTheOtherPluginsWhenOneFails` (a real container: a metadata whose name is longer than its column, then a valid one). Each pin was mutation-verified on the committed tree. **Upgrade impact**: - The content blocks of the pages of space templates, portal templates and user sites (dashboards) leave search: on the next full reindex, or when such a page is next updated through Layout. The bare page documents of those sites were already not indexed. - At startup, the metadata of the init plugins are created one plugin at a time: a plugin that fails is logged under its own name, no longer rolls back the metadata of the others, and, on the databases whose ids come from a sequence, no longer fails the container start. **Known limits**: - A page of such a site saved outside Layout (a configuration re-import, for instance) builds no document, and the indexing framework skips a missing document instead of removing it: the blocks indexed before stay until the next full reindex, or the next update of the page through Layout. **Release order**: merge it before, or with, Meeds-io/portal#1338 and Meeds-io/layout#581. The metadata fix is needed by Meeds-io/portal#1338, which would otherwise turn a failing metadata init plugin into a failed start. The search fix is needed by Meeds-io/layout#581, whose space template restore would otherwise index the template's content blocks. **Tests**: `mvn -B -ntp -o clean verify javadoc:javadoc -Pcoverage -pl component/core` at `c2106c8844`: BUILD SUCCESS, 1357 tests, 0 failures, 0 errors (`MetadataServiceTest` 32, `PageContentIndexingConnectorTest` 33, `PageContentBlockIndexingListenerTest` 11), against the local `7.3.x-maintenance-SNAPSHOT` of Meeds-io/portal#1338, which carries its `RollbackException`. Knowledge: Meeds-io/eng-standards#409, #560, #586, #587 This change is classified N1 (proposed, recall-first) — the JPA transaction boundary over the kernel DAO writes of a component's start, whose failure fails the portal container start with Meeds-io/portal#1338, and which permission-bearing page documents search exposes. Its approver must be an Archi/Dev who knows it is N1, not an approval on AI review alone; author ≠ approver. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…it plugin's metadata on its own EXO-91183 (#6245) **Symptom**: - Search showed the content blocks of the pages of a space template, and of other sites users don't navigate (portal templates, user sites), to every user the page grants access to, with a link that leads nowhere. They were indexed on a full reindex, on every save of such a page through Layout, and on a space template's "Restore default pages", which refreshes the index of every page of the template with Meeds-io/layout#581. - With Meeds-io/portal#1338, a method whose transaction was marked rollback-only fails instead of returning. A metadata init plugin whose metadata can't be stored at startup would then fail the start of `MetadataServiceImpl`, and with it the portal container start. Before, the outcome depended on the database. On MySQL/MariaDB (identity ids, the INSERT runs at persist), the start logged a WARN, and the metadata of every plugin of that start were silently rolled back. On the databases whose ids come from a sequence, the INSERT waits for the flush: it failed at the commit of the start's transaction, failing the container start with no WARN, or, when a later plugin's lookup flushed it first, the start logged a WARN under that later plugin's metadata (its lookup threw the flush failure as `IllegalStateException: Error retrieving value with key …`), and the whole start was silently rolled back. **Cause**: - `PageContentIndexingConnector` builds no bare page document for a page of a site users don't navigate (`isIndexableSite`: portal and group sites), but built its content block documents, and listed them for a full reindex. `PageContentBlockIndexingListener` reindexed them on `layout.page.updated`. - `MetadataServiceImpl#start` created the metadata of every plugin in one `@ExoTransactional` transaction, and caught each plugin's failure to go on. **Fix**: - `PageContentIndexingConnector` builds no document, block or bare, for a page of a site users don't navigate, and a full reindex doesn't list the content blocks of such a page. - When such a page is updated through Layout (`layout.page.updated`, `layout.page.permissions.updated`), `PageContentBlockIndexingListener` removes every document the index holds for it, so the blocks indexed before leave search. - `MetadataServiceImpl#start` creates each plugin's metadata in a transaction of its own: a plugin that fails is logged (WARN), and the metadata of the others are created. - Pinned by `PageContentIndexingConnectorTest` (a block of a template page builds no document; a full reindex lists none), `PageContentBlockIndexingListenerTest` (a template page update unindexes its documents and reindexes none) and `MetadataServiceTest#testStartCreatesTheMetadataOfTheOtherPluginsWhenOneFails` (a real container: a metadata whose name is longer than its column, then a valid one). Each pin was mutation-verified on the committed tree. **Upgrade impact**: - The content blocks of the pages of space templates, portal templates and user sites (dashboards) leave search: on the next full reindex, or when such a page is next updated through Layout. The bare page documents of those sites were already not indexed. - At startup, the metadata of the init plugins are created one plugin at a time: a plugin that fails is logged under its own name, no longer rolls back the metadata of the others, and, on the databases whose ids come from a sequence, no longer fails the container start. **Known limits**: - A page of such a site saved outside Layout (a configuration re-import, for instance) builds no document, and the indexing framework skips a missing document instead of removing it: the blocks indexed before stay until the next full reindex, or the next update of the page through Layout. **Release order**: merge it before, or with, Meeds-io/portal#1338 and Meeds-io/layout#581. The metadata fix is needed by Meeds-io/portal#1338, which would otherwise turn a failing metadata init plugin into a failed start. The search fix is needed by Meeds-io/layout#581, whose space template restore would otherwise index the template's content blocks. **Tests**: `mvn -B -ntp -o clean verify javadoc:javadoc -Pcoverage -pl component/core` at `c2106c8844`: BUILD SUCCESS, 1357 tests, 0 failures, 0 errors (`MetadataServiceTest` 32, `PageContentIndexingConnectorTest` 33, `PageContentBlockIndexingListenerTest` 11), against the local `7.3.x-maintenance-SNAPSHOT` of Meeds-io/portal#1338, which carries its `RollbackException`. Knowledge: Meeds-io/eng-standards#409, #560, #586, #587 This change is classified N1 (proposed, recall-first) — the JPA transaction boundary over the kernel DAO writes of a component's start, whose failure fails the portal container start with Meeds-io/portal#1338, and which permission-bearing page documents search exposes. Its approver must be an Archi/Dev who knows it is N1, not an approval on AI review alone; author ≠ approver. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…it plugin's metadata on its own EXO-91183 (#6263) **Symptom**: - Search showed the content blocks of the pages of a space template, and of other sites users don't navigate (portal templates, user sites), to every user the page grants access to, with a link that leads nowhere. They were indexed on a full reindex, on every save of such a page through Layout, and on a space template's "Restore default pages", which refreshes the index of every page of the template with Meeds-io/layout#581. - With Meeds-io/portal#1338, a method whose transaction was marked rollback-only fails instead of returning. A metadata init plugin whose metadata can't be stored at startup would then fail the start of `MetadataServiceImpl`, and with it the portal container start. Before, the outcome depended on the database. On MySQL/MariaDB (identity ids, the INSERT runs at persist), the start logged a WARN, and the metadata of every plugin of that start were silently rolled back. On the databases whose ids come from a sequence, the INSERT waits for the flush: it failed at the commit of the start's transaction, failing the container start with no WARN, or, when a later plugin's lookup flushed it first, the start logged a WARN under that later plugin's metadata (its lookup threw the flush failure as `IllegalStateException: Error retrieving value with key …`), and the whole start was silently rolled back. **Cause**: - `PageContentIndexingConnector` builds no bare page document for a page of a site users don't navigate (`isIndexableSite`: portal and group sites), but built its content block documents, and listed them for a full reindex. `PageContentBlockIndexingListener` reindexed them on `layout.page.updated`. - `MetadataServiceImpl#start` created the metadata of every plugin in one `@ExoTransactional` transaction, and caught each plugin's failure to go on. **Fix**: - `PageContentIndexingConnector` builds no document, block or bare, for a page of a site users don't navigate, and a full reindex doesn't list the content blocks of such a page. - When such a page is updated through Layout (`layout.page.updated`, `layout.page.permissions.updated`), `PageContentBlockIndexingListener` removes every document the index holds for it, so the blocks indexed before leave search. - `MetadataServiceImpl#start` creates each plugin's metadata in a transaction of its own: a plugin that fails is logged (WARN), and the metadata of the others are created. - Pinned by `PageContentIndexingConnectorTest` (a block of a template page builds no document; a full reindex lists none), `PageContentBlockIndexingListenerTest` (a template page update unindexes its documents and reindexes none) and `MetadataServiceTest#testStartCreatesTheMetadataOfTheOtherPluginsWhenOneFails` (a real container: a metadata whose name is longer than its column, then a valid one). Each pin was mutation-verified on the committed tree. **Upgrade impact**: - The content blocks of the pages of space templates, portal templates and user sites (dashboards) leave search: on the next full reindex, or when such a page is next updated through Layout. The bare page documents of those sites were already not indexed. - At startup, the metadata of the init plugins are created one plugin at a time: a plugin that fails is logged under its own name, no longer rolls back the metadata of the others, and, on the databases whose ids come from a sequence, no longer fails the container start. **Known limits**: - A page of such a site saved outside Layout (a configuration re-import, for instance) builds no document, and the indexing framework skips a missing document instead of removing it: the blocks indexed before stay until the next full reindex, or the next update of the page through Layout. **Release order**: merge it before, or with, Meeds-io/portal#1338 and Meeds-io/layout#581. The metadata fix is needed by Meeds-io/portal#1338, which would otherwise turn a failing metadata init plugin into a failed start. The search fix is needed by Meeds-io/layout#581, whose space template restore would otherwise index the template's content blocks. **Tests**: `mvn -B -ntp -o clean verify javadoc:javadoc -Pcoverage -pl component/core` at `c2106c8844`: BUILD SUCCESS, 1357 tests, 0 failures, 0 errors (`MetadataServiceTest` 32, `PageContentIndexingConnectorTest` 33, `PageContentBlockIndexingListenerTest` 11), against the local `7.3.x-maintenance-SNAPSHOT` of Meeds-io/portal#1338, which carries its `RollbackException`. Knowledge: Meeds-io/eng-standards#409, #560, #586, #587 This change is classified N1 (proposed, recall-first) — the JPA transaction boundary over the kernel DAO writes of a component's start, whose failure fails the portal container start with Meeds-io/portal#1338, and which permission-bearing page documents search exposes. Its approver must be an Archi/Dev who knows it is N1, not an approval on AI review alone; author ≠ approver. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…d a site restore refreshes its pages' search index EXO-91183 (#603) **Symptom**: when the layout of a site couldn't be read, portal reads it with an empty layout (Meeds-io/portal#1338). Opening that site in the layout editor, creating a site from it as a template, or saving it as a site template would copy the empty layout over a real one. Today, creating the draft fails only by accident: a 500 from a NullPointerException in `LayoutModel`. **Cause**: `SiteLayoutService` `getSiteLayout`, `createDraftSite` and `createSite`, and `SiteTemplateService.saveAsSiteTemplate`, hand on the layout they read without checking that it was read. **Fix**: - These four methods throw `SiteLayoutUnresolvedException` for a site read with `PortalConfig#isLayoutUnresolved`, before anything is written, through one check (`SiteLayoutUnresolvedException#checkResolved`). `createSite` checks the template it copies; the other three check the site itself. - `SiteLayoutRest` and `SiteTemplateRest` answer that refusal with 409 Conflict, on five endpoints: `GET` and `PUT` of `sites/layout` (the update returns the site layout read again), `POST sites` (a site created from a template), `POST sites/draft`, and the site template created from a site. The layout editor and the site form show their error message, and the site template drawer stays open. The other failures of these requests keep their outcome. - Tests in `SiteLayoutServiceTest`, `SiteTemplateServiceTest`, `SiteLayoutRestTest` and `SiteTemplateRestTest`: - the refusal, and that nothing is written: no draft removed, no site or template created; - the resolved path; - the 409 for the new exception, and no 409 for another `IllegalStateException`. **Search index after a site restore**: a restore saves the pages and rebuilds the navigation of the site without the events of the editors. Since Meeds-io/portal#1338 saves the pages in place, the portal's page update event alone reaches the index, and it only queues the blocks the index doesn't hold yet. So a block the restore removed stayed in search, a block whose content it changed kept its old document, and a page whose last node it removed kept its documents. - After a restore of pages or navigation, `SiteLayoutService#restoreSite` compares the pages a node of the site leads to before and after the restore: - a page a node of the site leads to after the restore gets `layout.page.updated`, so its blocks and its path are reindexed, as after a page save; for a page of a site users don't navigate (a site template, a user site), Meeds-io/social#6245 removes its documents instead; - a page whose last node in the site was removed gets `layout.page.unreachable`, so its documents are unindexed, as after a node deletion; - a page no node of the site leads to, before or after the restore (reached, if at all, from another site, where the index resolves no path for it either), gets `layout.page.updated` when the restore covers the pages, and nothing otherwise. - A failure to read the navigation, before or after the restore, logs a WARN and leaves the restore done. - `NavigationLayoutService#getReachablePages` holds the walk that the node deletion already used. - Tests in `SiteLayoutServiceTest` (each case above, a navigation-only restore, both failures) and `NavigationLayoutServiceTest`. - Cost: two loads of the site's navigation tree, and one event per page of the site, per restore of pages or navigation. **Known limits**: - A restore checks which pages are still reached in the restored site's navigation only: a page whose last node in its own site the restore removes is unindexed, even when a node of another site still leads to it, as after a node deletion. - `SiteTemplateService.createSiteTemplate` copies the `basic` template through portal's template clone, which keeps the flag. - The site template export (`SiteTemplateDatabindPlugin`) writes the empty layout into the archive. - The site template drawer shows no message when its save fails. **Release order**: merge after Meeds-io/portal#1338 and its published `7.3.x-maintenance` snapshot. This PR compiles against `PortalConfig#isLayoutUnresolved`, so its CI fails until then. Merge it with or after Meeds-io/social#6245: without it, a space template's "Restore default pages" indexes the template's content blocks. **Tests**: - `mvn -B -ntp -o clean verify javadoc:javadoc -Pcoverage -pl layout-service` at `01d1b2bd`: BUILD SUCCESS, 388 tests, 0 failures, 0 errors. - It compiled against a local `7.3.x-maintenance-SNAPSHOT` of Meeds-io/portal#1338. No signature of #1338's `component/api` changed since its first pushed commit: its later commits change only `ExoTransactional`'s Javadoc there. Knowledge: Meeds-io/eng-standards#409, #560, #586, #587 This change is classified N2 — a REST contract change (409 when a site's layout couldn't be read, on the site layout read and update, the site and draft site creation, and the site template created from a site) and a cross-domain seam (a site restore broadcasts the page updated and page unreachable events that social's search indexing consumes to reindex or unindex the site's pages). Its approver must be an Archi/Dev who knows it is N2, not an approval on AI review alone. It requires Meeds-io/portal#1338 (N1) to merge first. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… reload EXO-91183 (#233) **Symptom**: the comment on the `administration` declaration of `portal-configuration.xml` says that reloading only the exo part of the site makes the `PageImporter` remove Meeds' administration pages, and recommends `io.meeds.administration.portalConfig.metadata.importmode=overwrite` to reload either part. With Meeds-io/portal#1338 the first is false, and the recommended property also reloads the `global` site, which the comment doesn't say. **Cause**: with Meeds-io/portal#1338, a startup that runs an `overwrite` declaration of a site imports the pages and the navigation of every declaration of that site, and removes the pages and nodes none of them defines. The `io.meeds.administration` properties also set the mode of this file's `global` declaration. **Fix**: the comment says that, with `overwrite`, either property reloads the pages and the navigation of all the administration declarations, Meeds' included, and removes the pages and nodes none of them defines, and that the `io.meeds.administration` property reloads `global` as well. Comment only; `xmllint --noout` on the file at `943e39f`: no output (well-formed). **Release order**: merge with or after Meeds-io/portal#1338, whose behaviour the comment describes. Knowledge: Meeds-io/eng-standards#409, #560, #586, #587 This change is classified N3: no REST/DAO/schema/ACL surface touched (a comment in a configuration file). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>



Symptom 1: after the built-in sites (
global,dw,myworkspace) are restored with their layout, a displayed site's pages can render empty: the meta site was left with an empty layout.Symptom 2: an overwrite restore of a site defined by several configurations (
globalhas six: multifactor-authentication, agenda, documents, processes, sites, digital-workplace) logs, for every second configuration (reproduced on a local server):and leaves pages missing, pages whose layout names deleted windows, and the
dwsite's People and Spaces nodes without a page.Cause 1:
SiteStorageImpl.savereplaced a site's layout one kernel DAO call at a time, each@ExoTransactionalcall committing on its own. A concurrent reader could read the site row while its root container was already deleted and get, and cache, an empty layout.UserPortalConfigService.restoreSitethen read the site again after the import and saved that empty layout back over the restored one.Cause 2: a restore imported each configuration of the site in turn with the requested mode. In overwrite mode, each configuration deleted every page of the site, the pages the previous configurations had just imported included, and re-created its own pages with new identifiers: the nodes of other sites that reference them lost their page (
FK_PORTAL_NODE_PAGEisON DELETE SET NULL). Its page deletion was refused at commit while the page windows were already deleted in their own transactions. In overwrite and restore-defaults mode, each configuration also deleted the nodes the previous ones had imported under the nodes it defines. The startup import did the same for a configuration in overwrite mode, at every start.Fix:
@ContainerTransactional(atomic = true)runs the method in one JPA transaction, committed when it returns and rolled back when it throws; when a failed call marked it rollback-only, a method that returned throwsRollbackException. Kernel EntityManager writes join it. When the method begins it, Spring transactional methods and Spring Data repositories join it too, and their callbacks run with its outcome.SiteStorageImplcreate(PortalData),save(PortalData)andremove(SiteKey)use it.TransactionCallbacks.afterCompletion), after invalidating the loads in flight;FutureCache.clearFuturesinvalidates every load it removes.SiteDAO#refresh), then flaggedlayoutUnresolvedwith an empty layout; a save of it keeps the stored layout.restoreSiteapplies the previous site's settings to the imported config before its save, instead of saving the site a second time.UserPortalConfigService#restoreSite), and the startup import of a site that one of its configurations overwrites, import the site from all its configurations (NewPortalConfigListener#importSitePages,#importSiteNavigation): an overwrite deletes the pages that no configuration defines, then the pages of every configuration are imported in place; the navigation of every configuration is imported without deleting any node, thenNavigationOrphanRemoverdeletes the nodes that none of them defines, as the mode does (every such node in overwrite mode, the nodes nested in a default node in restore-defaults mode, a node only in the path of a fragment attachment point keeping its children). Nothing is deleted when the pages or the navigation file of a configuration can't be parsed; a file a configuration can't open counts as defining none. All pages are imported before any navigation.NewPortalConfigListener#initPageDB(createdOwners, filled by the startup import and by a site-layout restore) no longer applies to it.PageStorageImpldeletes a page, and replaces a page layout, in one transaction, and broadcasts the page event once that write returns.PageDAO#refresh,SiteDAO#refresh), and work from the layout that row holds. Before, they worked from the page or site as this request first read it, for instance through a node load, which reads the pages its nodes reference; with fix: Refuse to hand on the layout of a site that couldn't be read, and a site restore refreshes its pages' search index EXO-91183 layout#581, a site restore reads the site's navigation before it imports the pages. A layout another request saved since is now removed with the page or site, replaced by a layout save, and copied by a clone.PageEntityis@DynamicUpdate. A page row has two writers, its properties save and its layout save, and no version: an update now writes only the columns it changed, so a properties save no longer writes back the page body it read. It reads its row again too, so each property it saves is compared with the row as stored.ExoTransactionalAspectrolls back and throwsRollbackExceptionwhen the method it began a transaction for returns over a rollback-only transaction, asContainerTransactionalAspectalready did for its own transactions. A method that throws keeps its error, unless ending the transaction fails, as before. The EntityManager it started is closed whatever the method's outcome.ContainerTransactionalAspectTest,ExoTransactionalAspectTest,ContainerTransactionalRepositoryTest(a real Spring Data repository),TransactionCallbacksTest,FutureCacheTest,TestDataStorage(a layout write and a page delete failing midway through the woven advice, a node update evicting the node cache after the commit),SiteDAOTest,TestUserPortalConfigService,TestRestoreSiteConfigurationsin the newInitContainer10TestSuite(a restore and a startup import of sites defined by several configurations, a node of another site keeping its page, a configuration of an unknown site type, a pages restore of a site this startup didn't import, an orphan page the request has read and another request saves a layout on during the restore),TestDataStorage(a page layout save, a page properties save, a page clone, a site save and a site removal after another request saved a layout on the page or site the request had read, an update of the page properties that leaves the layout column another request wrote, and a properties save that writes back a property another request changed since the request read the page),NewPortalConfigListenerTest(an orphan page that can't be removed),TestNavigationFragmentImporterRestoreDefaults(now run, inInitContainer1TestSuite),TestSiteDataImportOverwriteandTestImportNavigationOverwrite.Upgrade impact:
restoreSitemakes one save per configuration that declares the site:PORTAL_CONFIG_UPDATED, orPORTAL_CONFIG_CREATEDfor a site the import creates, and none when the import skips the site.@ExoTransactionalmethod that began the transaction and returns over it marked rollback-only now throwsjakarta.persistence.RollbackExceptionafter the rollback, instead of returning with none of its writes committed. A method that joined an active transaction doesn't throw: the method that began it does. The transaction is marked by an atomic call above, or by a failing JPA operation (every convertedPersistenceExceptionbutNoResultException,NonUniqueResultException,LockTimeoutExceptionandQueryTimeoutException). So any such method that catches the failure and returns now fails visibly. The callers checked:JPASettingServiceImpl#setcatches every exception of its body: aSettingService#setcall made outside a transaction now throws where it logged an ERROR.LayoutUpgradePlugin#updateLayout: the exception leavesprocessUpgrade, the upgrades after it don't run, andUpgradeProductServicedoesn't record the plugin as done. The whole plugin runs again at every start, re-applying the upgrades before the failing one, until that one passes. Before, the failing upgrade's writes were rolled back and the plugin was recorded as done.MetadataServiceImpl#start: a failing metadata init plugin would fail the start of the portal container; fix: Index no page of a site users don't navigate, and create each init plugin's metadata on its own EXO-91183 social#6245 creates each plugin's metadata in its own transaction.SocialUserEventListenerImpl#postDeletethrows,UserDAOImpllogs it (WARN), and the user is deleted as before.ProgramSpaceListenerand commonsJPAQueueMessageImpl#sendkeep their outcome: the exception is dropped by an unread future, or logged by the job (ERROR), and the writes are rolled back as before.start()makes such a call outside a transaction fails its start (ContainerException), and the portal container is left partly started.exo.agenda.portalConfig.metadata.importmode=overwrite), every start that runs that configuration (itsoverridetrue, or a first start, or a reimport) imports the pages and the navigation of all the configurations of the site, whatever their own mode andoverride, then deletes the pages and nodes that none of them defines. The pages created in the UI in that site are among them, as before. So are the nodes created in the UI under a node that one of its configurations defines or attaches a fragment to, the root included when one attaches there, as one configuration of every shipped site does; before, a start deleted only those under the attachment points of the overwriting configuration's own fragments. Each configuration's pages are saved from its files in place (layout, state and permissions replaced), and its navigation merged. An extension in overwrite mode no longer deletes the pages and nodes the other configurations of the site define: they are kept and the extension's own are merged over them. On a first start, or a reimport, with an overwrite configuration, the result is the union of the configurations, where the last overwrite used to win.pages.xmlornavigation.xmlcan't be parsed: no page, or no node, is deleted.Can't find Window with ids).Known limits:
PAGE_DESTROYEDis broadcast before the commit when the page removal joins a site removal (SiteStorageImpl#remove); no production listener.PAGE_REMOVEDcarries the page asLayoutService#remove(PageKey)gets it, from the page cache or from the request's first read of the page: it may lack a layout saved since, which the delete removes.savePortalFromTemplate) keeps the unresolved flag, andcreateignores it.clearFuturesrace fix is not pinned.PortalConfigoverloads runs before, outside the transaction.@CacheEvictevicts at once, before the commit. A Spring call made while the commit flushes (an entity listener) does not join.Follow-ups: the per-session
UIPortalcache is not refreshed on a meta-site layout change;globalis displayed on community-dev but not in the packaged configuration; nested container misses; an optional guard in the QA restore hook (meeds-qa-ui#339); a Spring join inside a non-atomic kernel transaction; the web-conferencing pins of task 91299 (exoplatform/web-conferencing#421).Release order: Meeds-io/social#6245 before this PR, or with it: without it, a failing metadata init plugin fails the start. This PR before the
Meeds-io/layoutPR of EXO-91183, which refuses to hand on an unresolved site's layout and compiles againstPortalConfig#isLayoutUnresolved.Tests: at the head
65f8e9a240,mvn -B -ntp -o -Dcheckstyle.skip=true -pl component/portal -Dtest='TestDataStorage,TestModelStorage,TestPageService,TestUserPortalConfigService,TestRestoreSiteConfigurations,TestSiteDataImport*,TestImportNavigation*,TestImportFragment*,TestNavigationFragmentImporterRestoreDefaults,NewPortalConfigListenerTest' -Dsurefire.failIfNoSpecifiedTests=false -Djacoco.skip=true test: 18 classes, 148 tests, 0 failures, 0 errors. The full suite (mvn -B -ntp clean verify javadoc:javadoc -Pcoverage) wasn't run at this head.Knowledge: Meeds-io/eng-standards#409, #560, #586, #587
This change is classified N1 — Kernel ↔ Spring bridge (the atomic
@ContainerTransactionaltransaction that Spring transactional methods and Spring Data repositories join), the outcome of every@ExoTransactionalmethod, the JPA transaction boundaries over the site and page storage writes, the access and edit permissions a restored site keeps, and the irreversible deletion of pages and navigation nodes by a restore and by the startup import. Its approver must be an Archi/Dev who knows it is N1, not an approval on AI review alone; author ≠ approver.🤖 Generated with Claude Code