Repository navigation
fix: Index no page of a site users don't navigate, and create each init plugin's metadata on its own EXO-91183 - #6245
Conversation
…'t indexed EXO-91183 A page of a site template, a user site or another site users don't navigate got no page document in search, but its content blocks were indexed: on a full reindex, and whenever the page was saved or restored through Layout. Search shows a block to every user its page grants access to. - No document is built for a page of a site users don't navigate, whether a content block or the page itself. - A full reindex doesn't list the content blocks of such a page. - When such a page is updated through Layout, the documents the index holds for it are removed. A page saved outside Layout keeps them until the next full reindex. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a of the others from being created EXO-91183 The metadata of every init plugin was created at startup in one transaction. A plugin whose metadata couldn't be stored was logged, and the metadata of every plugin was rolled back with it. Now that a method whose transaction was marked rollback-only fails instead of returning (Meeds-io/portal#1338), that start would fail the portal container start. - The metadata of each plugin is created in a transaction of its own: a plugin that fails is logged, and the metadata of the others are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Self-review close-out — rounds 7 to 10This PR was opened after round 8. Its two fixes came out of rounds 7 and 8, so its table covers every round it was in. The last round found nothing left to fix in it. Status table
Verified conformThe last rounds checked the body's before-state against the code and the libraries: Hibernate 7.4.5, Tests
ClassificationThis change is classified N1 (proposed, recall-first) at Open items for a human
🤖 Generated with Claude Code |
ahamdi
left a comment
There was a problem hiding this comment.
AI review — Round #1
🟡 Medium — The Knowledge: line misses the corpus PR that this fix makes stale
The body has Knowledge: Meeds-io/eng-standards#586, #587. Both PRs carry the search half of the change only, and neither mentions MetadataServiceImpl#start. Draft Meeds-io/eng-standards#560, under the same transaction topic, lists it as exposed:
- `social` `MetadataServiceImpl#start` (`MetadataServiceImpl.java:685-700`) catches `Exception` around each plugin's metadata read and creation, logs a WARN and goes on. Exposed: one failed creation rolls back every plugin's.
Once this PR lands, that line is wrong: each plugin's creation commits on its own. It also cites :685-700, while start() is at :683-698 at this head.
Fix: update #560's line and its permalink range to this PR's state: no longer exposed, fixed by #6245. Add #560 to the Knowledge: line, since it is the corpus PR that carries the metadata half of the update.
Verified conform
- Metadata start, transaction scope. Removing
@ExoTransactionalfromstart()is safe. The Kernel'sLifecycleVisitor.startwraps eachStartable#startinRequestLifeCycle.begin(container)(startTransaction=true; checked in the 7.3.x-maintenanceexo.kernel.containerjar). That bindsEntityManagerService's EntityManager with no transaction, so eachMetadataDAO#createbegins and ends its own@ExoTransactionaltransaction, and the reads share that EntityManager.testStartCreatesTheMetadataOfTheOtherPluginsWhenOneFailsruns under the same wiring, throughAbstractCoreTest#setUp'sbegin(), so the pin is representative. - Connector. The site guard sits in
PageContentIndexingConnector#build, after the draft check and before the block/bare dispatch, socreateandupdateboth returnnullfor block and bare documents alike.#queueLiveBlockDocumentsis only reached fromcreatePageDocument, behind that guard.getAllBlockIdsfilters onisIndexablePageReferencebefore skip and limit, and an unparseable reference keeps its old behaviour (listed, then resolved bybuild). - Listener.
PageContentBlockIndexingListener#onEventchecks the site after the draft check and after the page lookup. For such a page it reindexes nothing and unindexes everythingfindIndexedDocumentIdsreturns. It does this on bothlayout.page.updatedandlayout.page.permissions.updated(literals unchanged).PageSavedIndexingListenerkeepsPAGE_UPDATED→reindex, andcreatestays onbuild(id, true)(pitfall page-index-converges-only-through-events). - Body claims. The known limit holds: a save outside Layout yields a
nulldocument, which the indexing framework skips. The upgrade impact holds for portal, group, template and user sites. The release order matches Meeds-io/portal#1338 and Meeds-io/layout#581, both open onfeature/maintenance. #586 and #587 are drafts, as they should be until the code reachesdevelop. - Tests. The three new connector and listener tests are JUnit 4 classes, already registered in the module's suites. Each asserts the behaviour named in the body.
The PR does several things well. The site rule now lives in one guard on the path every create and update takes, and getAllBlockIds applies the same predicate before paging, so a full reindex and a single update cannot disagree. The listener removes the documents already indexed for a template page instead of only stopping new ones. The metadata fix is the smallest change that gives each plugin its own transaction, and it is pinned through a real container with a real column limit.
This change is classified N1: a transaction boundary over kernel DAO writes at container start, and the documents search exposes under page permissions. It must be validated by an Architect or Developer who knows it is N1, not auto-merged on AI review alone; author ≠ approver.
🤖 Generated with Claude Code
|
Round #1 🟡 (
No code change: the head is still |
ahamdi
left a comment
There was a problem hiding this comment.
AI review — Round #2 (final)
The head is still c2106c8844, the commit Round #1 reviewed, so this round covers only the body change.
| Round | # | Sev | Finding | Status |
|---|---|---|---|---|
| 1 | 1 | 🟡 | The Knowledge: line misses Meeds-io/eng-standards#560, which this fix makes stale |
✅ Fixed: verified in the live body and in #560 at its head 7b4c85c817 |
Verified conform
- Body. The body now ends with
Knowledge: Meeds-io/eng-standards#409, #560, #586, #587. All four PRs are open drafts, which is right while the code has not reacheddevelop. - #560 carries the metadata half. At its head, the pitfall
an-entitymanager-exception-caught-inside-an-exotransactional-transaction-rolls-it-back-without-a-word.mdgivesMetadataServiceImpl#start(MetadataServiceImpl.java:683-698) as not exposed. It explains why: each plugin's metadata is created in the transaction its DAOcreatebegins and commits. ItsVerifiedstamp leaves the socialdevelopSHA asTBDuntil this code lands there. Thedevelopstate (@ExoTransactional,:685-700at3b9d6c6e1a) is kept only as history in the change fragment. The cited range matchesstart()at this PR's head, and so does the WARN (:695-697).
All findings from previous rounds are resolved. Nothing is outstanding on the AI review side.
This change is classified N1: a transaction boundary over kernel DAO writes at container start, and the documents search exposes under page permissions. It must be validated by an Architect or Developer who knows it is N1, not auto-merged on AI review alone; author ≠ approver.
🤖 Generated with Claude Code
… pages EXO-91183 (#1338) **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 (`global` has six: multifactor-authentication, agenda, documents, processes, sites, digital-workplace) logs, for every second configuration (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: ... ``` and leaves pages missing, pages whose layout names deleted windows, and the `dw` site's People and Spaces nodes without a page. **Cause 1**: `SiteStorageImpl.save` replaced a site's layout one kernel DAO call at a time, each `@ExoTransactional` call 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.restoreSite` then 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_PAGE` is `ON 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 throws `RollbackException`. 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. `SiteStorageImpl` `create(PortalData)`, `save(PortalData)` and `remove(SiteKey)` use it. - The site, layout, page, navigation and description caches evict again once the transaction completes (`TransactionCallbacks.afterCompletion`), after invalidating the loads in flight; `FutureCache.clearFutures` invalidates every load it removes. - A site read whose root container is missing is read again once (`SiteDAO#refresh`), then flagged `layoutUnresolved` with an empty layout; a save of it keeps the stored layout. - `restoreSite` applies the previous site's settings to the imported config before its save, instead of saving the site a second time. - A site restore (`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, then `NavigationOrphanRemover` deletes 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. - A restore imports the pages of every configuration of the site, whatever was imported before: the check of `NewPortalConfigListener#initPageDB` (`createdOwners`, filled by the startup import and by a site-layout restore) no longer applies to it. - Each orphan page is removed on its own: a page that can't be removed, as one removed since the orphans were selected, is logged (WARN) and the next ones are removed. - `PageStorageImpl` deletes a page, and replaces a page layout, in one transaction, and broadcasts the page event once that write returns. - A page delete, a page layout save, a page clone, a site save and a site removal read their row again (`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 Meeds-io/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. - `PageEntity` is `@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. - A failing atomic call that joined an active kernel transaction marks it rollback-only and logs a WARN naming the method. - `ExoTransactionalAspect` rolls back and throws `RollbackException` when the method it began a transaction for returns over a rollback-only transaction, as `ContainerTransactionalAspect` already 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. - At startup, a configuration whose site type or import mode is unknown is left out of the overwritten sites; on a site no other configuration overwrites, its own import logs the error as before. - Pinned by `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`, `TestRestoreSiteConfigurations` in the new `InitContainer10TestSuite` (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, in `InitContainer1TestSuite`), `TestSiteDataImportOverwrite` and `TestImportNavigationOverwrite`. **Upgrade impact**: - A page delete, a page layout or properties save, a page clone, and a site save or removal read their row once more (a SELECT by id). - A site whose stored body names a missing root container is kept, read with an empty layout flagged unresolved (WARN) and cached so; a save of it keeps the stored layout (WARN). A restore with layout, or a save that sets a layout, repairs it. - `restoreSite` makes one save per configuration that declares the site: `PORTAL_CONFIG_UPDATED`, or `PORTAL_CONFIG_CREATED` for a site the import creates, and none when the import skips the site. - A write failure now rolls back the whole site create, save or remove. Inside an enclosing kernel transaction, a failed site create, save or remove, page delete or page layout save marks that transaction rollback-only and logs a WARN naming the method. - **An `@ExoTransactional` method that began the transaction and returns over it marked rollback-only now throws `jakarta.persistence.RollbackException`** after 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 converted `PersistenceException` but `NoResultException`, `NonUniqueResultException`, `LockTimeoutException` and `QueryTimeoutException`). So any such method that catches the failure and returns now fails visibly. The callers checked: - `JPASettingServiceImpl#set` catches every exception of its body: a `SettingService#set` call made outside a transaction now throws where it logged an ERROR. - social `LayoutUpgradePlugin#updateLayout`: the exception leaves `processUpgrade`, the upgrades after it don't run, and `UpgradeProductService` doesn'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. - social `MetadataServiceImpl#start`: a failing metadata init plugin would fail the start of the portal container; Meeds-io/social#6245 creates each plugin's metadata in its own transaction. - social `SocialUserEventListenerImpl#postDelete` throws, `UserDAOImpl` logs it (WARN), and the user is deleted as before. - gamification `ProgramSpaceListener` and commons `JPAQueueMessageImpl#send` keep their outcome: the exception is dropped by an unread future, or logged by the job (ERROR), and the writes are rolled back as before. - A kernel component whose `start()` makes such a call outside a transaction fails its start (`ContainerException`), and the portal container is left partly started. - An overwrite restore with pages keeps the pages and nodes of every configuration of the site; the pages it imports keep their identifier, so the nodes of other sites keep their page. The pages and nodes that no configuration defines are deleted once, after every import for the nodes. - A restore imports the site layout of every configuration, then their pages, then their navigation: a node that references a page of a later configuration of the site is linked to it. - A restore-defaults restore with navigation keeps the nodes of every configuration, whatever their order; a node nested in one configuration that another one attaches a fragment to keeps its custom children. - The search index of a restored site's pages is refreshed by Meeds-io/layout#581. With this PR alone, an overwrite restore updates the pages in place with the portal's page update event only: their indexed blocks keep their content and permissions until the page is next saved through Layout, or the next full reindex, as the restore-defaults restore already did. - When the navigation, or the pages, file of one configuration can't be parsed, a restore deletes no node, or no page, of the site. A file a configuration can't open counts as defining none: the pages and nodes only it defines are deleted as orphans, as the previous whole-site removal deleted every page. - A pages restore without the site layout ("Restore pages") restores the pages of a site that neither this start nor a site-layout restore since then imported (after a restart, a site no configuration overrides); before, it restored only its navigation. A REST restore with layout and pages whose site layout import fails now still imports the pages. - **Startup contract change**: when one configuration of a site is in overwrite mode (an extension, or a property such as `exo.agenda.portalConfig.metadata.importmode=overwrite`), every start that runs that configuration (its `override` true, or a first start, or a reimport) imports the pages and the navigation of all the configurations of the site, whatever their own mode and `override`, 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. - The shipped configurations (meeds sites, digital-workplace, agenda, documents, processes, multifactor-authentication) default to insert or conserve: the change applies to a deployment that sets an overwrite mode. - A configuration of that site whose `pages.xml` or `navigation.xml` can't be parsed: no page, or no node, is deleted. - A configuration of that site with an unknown import mode: its pages and navigation are imported in merge mode; its site layout import still logs the mode. - The site layout import, and the shipped defaults, are unchanged. - A page delete or a page layout save that fails midway rolls back its windows and permissions; before, the page kept a layout naming deleted windows (`Can't find Window with ids`). **Known limits**: - `PAGE_DESTROYED` is broadcast before the commit when the page removal joins a site removal (`SiteStorageImpl#remove`); no production listener. - A node write drops every navigation and node load in flight, for all sites, before evicting; the after-completion repeat is pinned, the in-flight invalidation is not. - A page delete, layout save or clone, and a site save or removal read their row without a lock: a layout another request commits between that read and the write is missed. - `PAGE_REMOVED` carries the page as `LayoutService#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. - A user site's pages are never deleted by the orphan removal, as the page import never did (not reachable today: no user-type configuration ships). - The template clone (`savePortalFromTemplate`) keeps the unresolved flag, and `create` ignores it. - The in-flight load pin covers the site cache; the `clearFutures` race fix is not pinned. - The banner write of the `PortalConfig` overloads runs before, outside the transaction. - A reader that straddles the commit still misses containers: a missing root is re-read once, then flagged; a missing nested container is skipped. - Spring: a Spring write inside an atomic method joined to an outer non-atomic kernel transaction keeps its own transaction. With a Spring synchronization already active and no EntityManager of its own, the atomic transaction collects none of the Spring callbacks. A Spring `@CacheEvict` evicts at once, before the commit. A Spring call made while the commit flushes (an entity listener) does not join. **Follow-ups**: the per-session `UIPortal` cache is not refreshed on a meta-site layout change; `global` is 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/layout` PR of EXO-91183, which refuses to hand on an unresolved site's layout and compiles against `PortalConfig#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 `@ContainerTransactional` transaction that Spring transactional methods and Spring Data repositories join), the outcome of every `@ExoTransactional` method, 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](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>
… pages EXO-91183 (#1357) **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 (`global` has six: multifactor-authentication, agenda, documents, processes, sites, digital-workplace) logs, for every second configuration (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: ... ``` and leaves pages missing, pages whose layout names deleted windows, and the `dw` site's People and Spaces nodes without a page. **Cause 1**: `SiteStorageImpl.save` replaced a site's layout one kernel DAO call at a time, each `@ExoTransactional` call 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.restoreSite` then 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_PAGE` is `ON 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 throws `RollbackException`. 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. `SiteStorageImpl` `create(PortalData)`, `save(PortalData)` and `remove(SiteKey)` use it. - The site, layout, page, navigation and description caches evict again once the transaction completes (`TransactionCallbacks.afterCompletion`), after invalidating the loads in flight; `FutureCache.clearFutures` invalidates every load it removes. - A site read whose root container is missing is read again once (`SiteDAO#refresh`), then flagged `layoutUnresolved` with an empty layout; a save of it keeps the stored layout. - `restoreSite` applies the previous site's settings to the imported config before its save, instead of saving the site a second time. - A site restore (`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, then `NavigationOrphanRemover` deletes 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. - A restore imports the pages of every configuration of the site, whatever was imported before: the check of `NewPortalConfigListener#initPageDB` (`createdOwners`, filled by the startup import and by a site-layout restore) no longer applies to it. - Each orphan page is removed on its own: a page that can't be removed, as one removed since the orphans were selected, is logged (WARN) and the next ones are removed. - `PageStorageImpl` deletes a page, and replaces a page layout, in one transaction, and broadcasts the page event once that write returns. - A page delete, a page layout save, a page clone, a site save and a site removal read their row again (`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 Meeds-io/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. - `PageEntity` is `@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. - A failing atomic call that joined an active kernel transaction marks it rollback-only and logs a WARN naming the method. - `ExoTransactionalAspect` rolls back and throws `RollbackException` when the method it began a transaction for returns over a rollback-only transaction, as `ContainerTransactionalAspect` already 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. - At startup, a configuration whose site type or import mode is unknown is left out of the overwritten sites; on a site no other configuration overwrites, its own import logs the error as before. - Pinned by `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`, `TestRestoreSiteConfigurations` in the new `InitContainer10TestSuite` (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, in `InitContainer1TestSuite`), `TestSiteDataImportOverwrite` and `TestImportNavigationOverwrite`. **Upgrade impact**: - A page delete, a page layout or properties save, a page clone, and a site save or removal read their row once more (a SELECT by id). - A site whose stored body names a missing root container is kept, read with an empty layout flagged unresolved (WARN) and cached so; a save of it keeps the stored layout (WARN). A restore with layout, or a save that sets a layout, repairs it. - `restoreSite` makes one save per configuration that declares the site: `PORTAL_CONFIG_UPDATED`, or `PORTAL_CONFIG_CREATED` for a site the import creates, and none when the import skips the site. - A write failure now rolls back the whole site create, save or remove. Inside an enclosing kernel transaction, a failed site create, save or remove, page delete or page layout save marks that transaction rollback-only and logs a WARN naming the method. - **An `@ExoTransactional` method that began the transaction and returns over it marked rollback-only now throws `jakarta.persistence.RollbackException`** after 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 converted `PersistenceException` but `NoResultException`, `NonUniqueResultException`, `LockTimeoutException` and `QueryTimeoutException`). So any such method that catches the failure and returns now fails visibly. The callers checked: - `JPASettingServiceImpl#set` catches every exception of its body: a `SettingService#set` call made outside a transaction now throws where it logged an ERROR. - social `LayoutUpgradePlugin#updateLayout`: the exception leaves `processUpgrade`, the upgrades after it don't run, and `UpgradeProductService` doesn'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. - social `MetadataServiceImpl#start`: a failing metadata init plugin would fail the start of the portal container; Meeds-io/social#6245 creates each plugin's metadata in its own transaction. - social `SocialUserEventListenerImpl#postDelete` throws, `UserDAOImpl` logs it (WARN), and the user is deleted as before. - gamification `ProgramSpaceListener` and commons `JPAQueueMessageImpl#send` keep their outcome: the exception is dropped by an unread future, or logged by the job (ERROR), and the writes are rolled back as before. - A kernel component whose `start()` makes such a call outside a transaction fails its start (`ContainerException`), and the portal container is left partly started. - An overwrite restore with pages keeps the pages and nodes of every configuration of the site; the pages it imports keep their identifier, so the nodes of other sites keep their page. The pages and nodes that no configuration defines are deleted once, after every import for the nodes. - A restore imports the site layout of every configuration, then their pages, then their navigation: a node that references a page of a later configuration of the site is linked to it. - A restore-defaults restore with navigation keeps the nodes of every configuration, whatever their order; a node nested in one configuration that another one attaches a fragment to keeps its custom children. - The search index of a restored site's pages is refreshed by Meeds-io/layout#581. With this PR alone, an overwrite restore updates the pages in place with the portal's page update event only: their indexed blocks keep their content and permissions until the page is next saved through Layout, or the next full reindex, as the restore-defaults restore already did. - When the navigation, or the pages, file of one configuration can't be parsed, a restore deletes no node, or no page, of the site. A file a configuration can't open counts as defining none: the pages and nodes only it defines are deleted as orphans, as the previous whole-site removal deleted every page. - A pages restore without the site layout ("Restore pages") restores the pages of a site that neither this start nor a site-layout restore since then imported (after a restart, a site no configuration overrides); before, it restored only its navigation. A REST restore with layout and pages whose site layout import fails now still imports the pages. - **Startup contract change**: when one configuration of a site is in overwrite mode (an extension, or a property such as `exo.agenda.portalConfig.metadata.importmode=overwrite`), every start that runs that configuration (its `override` true, or a first start, or a reimport) imports the pages and the navigation of all the configurations of the site, whatever their own mode and `override`, 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. - The shipped configurations (meeds sites, digital-workplace, agenda, documents, processes, multifactor-authentication) default to insert or conserve: the change applies to a deployment that sets an overwrite mode. - A configuration of that site whose `pages.xml` or `navigation.xml` can't be parsed: no page, or no node, is deleted. - A configuration of that site with an unknown import mode: its pages and navigation are imported in merge mode; its site layout import still logs the mode. - The site layout import, and the shipped defaults, are unchanged. - A page delete or a page layout save that fails midway rolls back its windows and permissions; before, the page kept a layout naming deleted windows (`Can't find Window with ids`). **Known limits**: - `PAGE_DESTROYED` is broadcast before the commit when the page removal joins a site removal (`SiteStorageImpl#remove`); no production listener. - A node write drops every navigation and node load in flight, for all sites, before evicting; the after-completion repeat is pinned, the in-flight invalidation is not. - A page delete, layout save or clone, and a site save or removal read their row without a lock: a layout another request commits between that read and the write is missed. - `PAGE_REMOVED` carries the page as `LayoutService#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. - A user site's pages are never deleted by the orphan removal, as the page import never did (not reachable today: no user-type configuration ships). - The template clone (`savePortalFromTemplate`) keeps the unresolved flag, and `create` ignores it. - The in-flight load pin covers the site cache; the `clearFutures` race fix is not pinned. - The banner write of the `PortalConfig` overloads runs before, outside the transaction. - A reader that straddles the commit still misses containers: a missing root is re-read once, then flagged; a missing nested container is skipped. - Spring: a Spring write inside an atomic method joined to an outer non-atomic kernel transaction keeps its own transaction. With a Spring synchronization already active and no EntityManager of its own, the atomic transaction collects none of the Spring callbacks. A Spring `@CacheEvict` evicts at once, before the commit. A Spring call made while the commit flushes (an entity listener) does not join. **Follow-ups**: the per-session `UIPortal` cache is not refreshed on a meta-site layout change; `global` is 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/layout` PR of EXO-91183, which refuses to hand on an unresolved site's layout and compiles against `PortalConfig#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 `@ContainerTransactional` transaction that Spring transactional methods and Spring Data repositories join), the outcome of every `@ExoTransactional` method, 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](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>



Symptom:
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 asIllegalStateException: Error retrieving value with key …), and the whole start was silently rolled back.Cause:
PageContentIndexingConnectorbuilds 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.PageContentBlockIndexingListenerreindexed them onlayout.page.updated.MetadataServiceImpl#startcreated the metadata of every plugin in one@ExoTransactionaltransaction, and caught each plugin's failure to go on.Fix:
PageContentIndexingConnectorbuilds 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.layout.page.updated,layout.page.permissions.updated),PageContentBlockIndexingListenerremoves every document the index holds for it, so the blocks indexed before leave search.MetadataServiceImpl#startcreates 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.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) andMetadataServiceTest#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:
Known limits:
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/coreatc2106c8844: BUILD SUCCESS, 1357 tests, 0 failures, 0 errors (MetadataServiceTest32,PageContentIndexingConnectorTest33,PageContentBlockIndexingListenerTest11), against the local7.3.x-maintenance-SNAPSHOTof Meeds-io/portal#1338, which carries itsRollbackException.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