# ConcurrentModificationException building ListMenu under concurrent logins — shared MenuConfig icon mutated by `ComponentUtils.copyIcon`

**Jmix:** 2.8.2 (regression vs 2.7.x) · **Vaadin:** 24.10.6 · **Spring Boot:** 3.5.14
**Modules:** jmix-flowui / jmix-flowui-kit 2.8.2 · TabbedMode · OIDC/Keycloak login

## Summary
When several already-open authenticated sessions reconnect at the same time (typical right
after a server restart — e.g. multiple SSO sessions auto-reconnect concurrently), building the
main `ListMenu` throws `java.util.ConcurrentModificationException` (intermittently a Vaadin
internal `AssertionError`). The affected user receives an unhandled-exception error page.
It is a timing-dependent race, so it does not occur on every restart.

## Root cause
`io.jmix.flowui.menu.MenuItem` is created once and cached in the singleton `MenuConfig`; it holds
the icon as a single **shared** `Component` instance (`MenuItem.icon`).

For every UI, `MenuItem.getIconComponent()` (MenuItem.java:256) calls
`ComponentUtils.copyIcon(icon)` → `copyAbstractIconAttributes(source, target)`
(ComponentUtils.java:138), which does effectively:

```java
target.setTooltipText(source.getTooltip().getText());
```

`AbstractIcon.getTooltip()` → `Tooltip.forHasTooltip(source)` **lazily creates and attaches a
tooltip element to the slot of the *source* icon** on first call — mutating the StateNode of the
**shared** `MenuConfig` icon. When two UI threads build their menus concurrently, they mutate the
same StateNode in parallel → `HashMap.computeIfAbsent` in `StateNode.getChangeTracker` → CME.

## Stack trace (representative)
```
java.util.ConcurrentModificationException
	at java.base/java.util.HashMap.computeIfAbsent(HashMap.java:1229)
	at com.vaadin.flow.internal.StateNode.getChangeTracker(StateNode.java:966)
	at com.vaadin.flow.internal.nodefeature.NodeList.add(NodeList.java:240)
	at com.vaadin.flow.component.shared.SlotUtils.addToSlot(SlotUtils.java:130)
	at com.vaadin.flow.component.shared.Tooltip.forHasTooltip(Tooltip.java:164)
	at com.vaadin.flow.component.shared.HasTooltip.getTooltip(HasTooltip.java:64)
	at io.jmix.flowui.kit.component.ComponentUtils.copyAbstractIconAttributes(ComponentUtils.java:138)
	at io.jmix.flowui.kit.component.ComponentUtils.copyFontIcon(ComponentUtils.java:176)
	at io.jmix.flowui.kit.component.ComponentUtils.copyIcon(ComponentUtils.java:110)
	at io.jmix.flowui.menu.MenuItem.getIconComponent(MenuItem.java:256)
	at io.jmix.flowui.menu.ListMenuBuilder.setIcon(ListMenuBuilder.java:166)
	at io.jmix.flowui.menu.ListMenuBuilder.createViewMenuItem(ListMenuBuilder.java:216)
	at io.jmix.flowui.menu.provider.MenuConfigListMenuItemProvider.convertToMenuItems(MenuConfigListMenuItemProvider.java:52)
	at io.jmix.flowui.xml.layout.loader.component.ListMenuLoader.loadMenuConfig(ListMenuLoader.java:49)
	...
```
Depending on timing the same race also surfaces as:
```
java.lang.AssertionError
	at com.vaadin.flow.internal.StateNode.setParent(StateNode.java:270)
	at com.vaadin.flow.internal.nodefeature.NodeFeature.attachPotentialChild(NodeFeature.java:80)
	at com.vaadin.flow.internal.nodefeature.StateNodeNodeList.add(StateNodeNodeList.java:55)
	...
```

## Minimal reproduction (no UI session, no Spring needed)
Concurrently copy a single shared icon; classpath only needs jmix-flowui-kit + Vaadin:

```java
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import io.jmix.flowui.kit.component.ComponentUtils;
import java.util.concurrent.*;

public class CopyIconRace {
    public static void main(String[] args) throws Exception {
        Icon shared = VaadinIcon.HOME.create();   // cold: tooltip not yet created
        // shared.getTooltip();                    // <-- uncommenting this (warm-up) makes the race disappear
        int threads = 16;
        ExecutorService pool = Executors.newFixedThreadPool(threads);
        CountDownLatch start = new CountDownLatch(1);
        for (int i = 0; i < threads; i++) {
            pool.submit(() -> {
                start.await();
                for (int j = 0; j < 500; j++) ComponentUtils.copyIcon(shared);
                return null;
            });
        }
        start.countDown();
        pool.shutdown();
        pool.awaitTermination(30, TimeUnit.SECONDS);
    }
}
```
Result: `ConcurrentModificationException` / internal Vaadin `AssertionError` from the worker
threads. Pre-calling `shared.getTooltip()` once (single-threaded) eliminates it — confirming the
lazy mutation of the *source* is the trigger.

Real-world trigger: restart the app with ≥2 open authenticated sessions; they reconnect
concurrently and build the menu in parallel.

## Suggested fix
`copyAbstractIconAttributes` must not mutate the source icon. Read the tooltip text without
triggering lazy creation (e.g. only copy a tooltip if the source already has one / read from the
element attribute), so that copying a cached `MenuConfig` icon is a pure read. More generally,
`MenuItem.getIconComponent()` / `copyIcon` on a shared `MenuConfig` icon should be side-effect-free.

## Our workaround (until fixed)
A `SmartInitializingSingleton` that — at startup, single-threaded, before the web server accepts
requests — walks `MenuConfig.getRootItems()` recursively and calls `MenuItem.getIconComponent()`
on every item once. This performs the lazy tooltip initialization on each shared icon exactly once,
so subsequent concurrent menu builds only read it.
