How a reviewed pull request fixed a LazyInitializationException that stopped Keycloak upgrades to 26.7.x, and what the review process changed.
Réponse courte
Upgrading Keycloak to 26.7.x could crash during startup when an instance had more than one realm. The cause was in the realm cache. Model adapters it had already handed out still pointed at JPA entities after the migration cleared the persistence context between realms.
The fix clears those cached adapters at the same point the persistence context is cleared. Keycloak maintainers reviewed it, and it was merged into keycloak/keycloak as PR #51945, closing issue #51304.
The problem: an upgrade that would not finish
Keycloak is the open-source identity and access management server. When it starts on an older database it runs model migrations, and one of them goes through every realm.
Issue #51304 reported that upgrades from 26.1.x to 26.7.0 failed at the last migration step with:
org.hibernate.LazyInitializationException: Cannot lazily initialize collection of role
org.keycloak.models.jpa.entities.RealmEntity.components ...
Keycloak would not start. According to the report, the crash depended on realm iteration order. If a custom realm was processed before master, which could happen after a database dump and restore, the upgrade failed.
The thread grew to 35 comments. Users reported the crash on 26.7.0, 26.7.1, 26.7.2 and 26.7.3, on PostgreSQL and H2. In the meantime they shared workarounds:
- Upgrade in two steps, first to 26.6.4 and then to 26.7.x.
- Start once with the realm cache disabled (
--spi-realm-cache--default--enabled=false). - Physically reorder the
realmtable somasteris migrated first.
One commenter summed it up: "Workarounds: 4 / Pull Requests: 1 / Fixes: 0." That came from an operator who couldn't use any of the workarounds and needed a real fix.
Root cause
RealmMigration.migrate() already empties the JPA persistence context before each realm with EntityManagers.flush(session, true). That keeps memory bounded. However, the Infinispan-backed RealmCacheSession keeps its own per-session maps of managed adapters for realms, clients, client scopes, roles and groups.
Once the persistence context was cleared, those adapters still delegated to entities that were now detached. When a later realm touched one of them (for example master's components collection), Hibernate could no longer lazy-load it, and the upgrade crashed.
The fix
The pull request adds one method to the cache provider contract and calls it right after the persistence context is cleared:
// RealmMigration.migrate()
CacheRealmProvider realmCache = session.getProvider(CacheRealmProvider.class);
session.realms().getRealmsStream().forEach(realm -> {
// empty out the persistence context for each realm
EntityManagers.flush(session, true);
if (realmCache != null) {
realmCache.clearManagedModels();
}
...
// RealmCacheSession
@Override
public void clearManagedModels() {
managedRealms.clear();
managedApplications.clear();
managedClientScopes.clear();
managedRoles.clear();
managedGroups.clear();
}
No cached adapter outlives the persistence context it was built on. The change doesn't touch realm ordering, the database or the migration steps themselves.
By the numbers
- Repository: keycloak/keycloak, 37,676 stars (Nov 3, 2026), Java, Apache-2.0
- Files changed: 4 (3 production files, 1 new test file)
- Lines: +135 / -0, of which 118 are the new integration test
- Commits: 2
- Opened: Aug 21, 2026. Merged: Sep 9, 2026. Time to merge: 18 days, 21 hours
- Review: approved by one Keycloak maintainer, merged by another
How the review went
The first version changed three files and had no test. An automated reviewer on the repository flagged that gap: a fix for a nondeterministic startup failure should come with regression coverage.
The follow-up commit added RealmMigrationCacheTest, an integration test that runs on the Keycloak server. It primes cached realm, client and role adapters, runs a RealmMigration, and asserts that the migration receives fresh model instances rather than the ones that were cached earlier.
Before maintainers picked it up, people affected by the bug built and tested the PR themselves. One posted in the issue that they had "Tested on PostgreSQL (docker) and H2 (raw binaries). No crash. Looks like it is safe to merge." Another wrote that they "built and tested the PR to run on Windows/Postgres and it solved the migration issue we were seeing."
A maintainer asked for DCO sign-off and arranged a CI run. Another approved the change ("This changes looks good") and raised a broader point. Any caller of flush(session, true) could, in principle, leave managed references with lazy collections behind, so this cleanup might belong inside the EntityManager helper itself. The PR was merged the same day, and the maintainers opened issue #52594 to consider generalizing the approach.
That is the review outcome you want. The narrow fix shipped, and the bigger design question went to the backlog where the project's owners can weigh it.
What this shows about the remediation loop
- The signal was a real production failure. It came with a stack trace and many independent reproductions.
- The patch was small and reversible. One new interface method, its implementation, and a single call site.
- Review made it better. The regression test was added because a reviewer asked for it.
- Humans stayed in control. Maintainers chose when to run CI, approved the change and merged it.
FAQ
Did the fix change how Keycloak migrates data?
No. The migration steps and realm ordering are the same. The change only drops cached, session-scoped model adapters after the persistence context is cleared, so later lookups build fresh ones.
Was the change accepted as submitted?
The production change was the same three files from the first commit. The second commit added the regression test after review feedback. Maintainers approved and merged it, and opened a separate issue for a possible broader follow-up.
Where can I read the original discussion?
Everything is public: the pull request, the original issue, and the follow-up issue.
Le but
The issue thread had a reproduction, a root-cause theory and four workarounds, but no fix. What was missing was a reviewable change in the code. When a pull request arrives with its evidence, a narrow patch and a test, maintainers can spend their time on what they care about, including whether the idea should go further.