enhancement
major
minor
major
minor
critical
#29682
TL Views: ConcurrentModificationException in ReactToolbarControl.replaceGroups on a page reload shows the "Internal error" page
Problem
Reloading a view window (/tl-dev/view/<window>/) after an application restart answered with the JSP "Internal error" page, logged as:
ERROR ThrowableErrorPage_jsp - Internal error: 'java.util.ConcurrentModificationException' … uri: '/tl-dev/view/v92af7e2015613f85/' java.util.ConcurrentModificationException at java.util.ArrayList$Itr.next at ReactToolbarControl.replaceGroups(ReactToolbarControl.java:188) at AppBarElement … (266) at CommandScope.fireChanged at DashboardElement.contributeEditCommands (196) … ViewServlet.renderAgain
It occurred twice in the TL Development app, snapshot build 42. The page has a <dashboard> ("Meine Arbeit") and an <app-bar> whose toolbar is rebuilt when the command scope changes.
Cause
A reload produces several requests for the same window that touch its control tree in parallel:
- the beforeunload beacon of the old page (windowClosed with unload: true), which the ReactServlet answers with ReactWindowRegistry.windowUnloaded() and thereby detaches the window's tree,
- the page request of the new page, which the ViewServlet answers by rendering the tree again (renderAgain), which attaches it, and
- the event stream connect of the new page (SSEServlet), which sends the full state of the tree and thereby attaches it as well.
None of them held the session's request lock (Interaction) that serializes all other requests against the control trees. Detaching and attaching the dashboard removes and adds its edit commands in the CommandScope; each change rebuilds the toolbar showing the scope's commands (ReactToolbarControl.replaceGroups). Two threads rebuild the same toolbar at the same time, and the iteration over its children fails.
The missing ordering also has a second effect: a beacon that arrives only after the new page has been rendered detaches the tree that is displayed and marks the window as unloaded, so that the window is torn down once the grace period has passed, although the page is still open.
Solution
- The ViewServlet builds and renders the window's tree inside an Interaction, like every command, so page rendering is serialized with commands and window lifecycle reports of the same session.
- The SSEServlet establishes the event stream connection (full state send) inside an Interaction.
- The ReactServlet handles the unload report inside an Interaction as well.
- Every rendering of a window's page gets a page-load token from the server (ReactWindowRegistry.issuePageLoad(), unique within the session), which the page carries as <body data-page-load="…"> and sends with its unload report. windowUnloaded ignores a report whose token does not name the page currently displayed, so a late beacon of the previous page neither detaches the freshly rendered tree nor marks the window as unloaded. A report without a token is handled as before.
ReactToolbarControl.replaceGroups stays as it is: under the request lock, no second thread changes the toolbar during a rebuild.