Skip to content

fix: extend OSIV to manage sessions for all datasources#15425

Merged
jdaugherty merged 7 commits into7.0.xfrom
fix/multi-datasource-osiv
Feb 25, 2026
Merged

fix: extend OSIV to manage sessions for all datasources#15425
jdaugherty merged 7 commits into7.0.xfrom
fix/multi-datasource-osiv

Conversation

@jamesfredley
Copy link
Contributor

Summary

Fixes #14333 and #11798 - both have the same root cause.

GrailsOpenSessionInViewInterceptor only registered a session for the default datasource's SessionFactory. Calling withSession on a secondary datasource during a web request threw No Session found for current thread because no session was bound for that SessionFactory in TransactionSynchronizationManager.

Root Cause

In HibernateDatastoreSpringInitializer (lines 189-194), only one OSIV interceptor is created referencing hibernateDatastore (the default). The interceptor's setHibernateDatastore() called setSessionFactory(hibernateDatastore.getSessionFactory()) - binding only the default SessionFactory.

Call chain for #14333:

  1. GormEntity.withSession -> AbstractHibernateGormStaticApi.withSession
  2. -> AbstractHibernateDatastore.withSession -> getHibernateTemplate().execute(callable)
  3. -> GrailsHibernateTemplate.doExecute -> getSession() -> sessionFactory.getCurrentSession()
  4. -> GrailsSessionContext.currentSession() checks TransactionSynchronizationManager.getResource(sessionFactory) for the secondary SessionFactory
  5. -> Nothing bound because OSIV only registered for default -> No Session found

#11798 has the same root cause: domain class mapped to non-default datasource used as command object fails validation because validate() needs a session but OSIV never opened one.

Fix

Modified GrailsOpenSessionInViewInterceptor to handle multiple datasource SessionFactory instances in a single interceptor:

  • setHibernateDatastore() now iterates all connection sources from HibernateDatastore and stores non-default datasource SessionFactory references in an additionalSessionFactories list
  • preHandle() opens sessions and binds SessionHolder to TransactionSynchronizationManager for each additional datasource (skipping any that already have a session bound)
  • postHandle() flushes additional sessions if their flush mode warrants it
  • afterCompletion() unbinds and closes additional sessions in reverse order

This approach keeps a single interceptor bean (backward compatible) and avoids the complexity of registering per-datasource OSIV beans.

Tests

Unit Tests (5 tests)

  • MultiDataSourceSessionSpec - Tests OSIV interceptor directly:
    • Default datasource session binding
    • Secondary datasource session binding
    • Cleanup of all datasource sessions
    • Skip-if-already-bound behavior
    • CRUD operations on secondary datasource

Functional/Integration Tests (4 tests via HTTP)

Existing Tests

  • All 10 grails-data-hibernate5 tests pass
  • All 42 grails-data-hibernate5-core connection tests pass
  • All 6 existing grails-multiple-datasources integration tests pass

Changed Files

File Change
GrailsOpenSessionInViewInterceptor.java Multi-datasource OSIV support
grails-data-hibernate5/grails-plugin/build.gradle Added servlet-api test dependency
MultiDataSourceSessionSpec.groovy New unit tests
SecondaryBookController.groovy New test controller
UrlMappings.groovy URL mappings for test controller
MultiDataSourceWithSessionSpec.groovy New functional tests via HTTP
grails-multiple-datasources/build.gradle Added HTTP client + URL mappings dependencies

GrailsOpenSessionInViewInterceptor only registered a session for the
default datasource SessionFactory. Calling withSession on a secondary
datasource during a web request threw 'No Session found for current
thread' because no session was bound for that SessionFactory.

The interceptor now iterates all connection sources from the
HibernateDatastore and opens/binds sessions for each non-default
datasource in preHandle, flushes them in postHandle, and unbinds/closes
them in afterCompletion. Existing sessions that are already bound (e.g.
by a transaction) are left untouched.

Fixes #14333
Fixes #11798

Assisted-by: Claude Code <Claude@Claude.ai>
@github-actions github-actions bot added the bug label Feb 20, 2026
@jamesfredley jamesfredley linked an issue Feb 20, 2026 that may be closed by this pull request
@jamesfredley jamesfredley self-assigned this Feb 20, 2026
@jamesfredley jamesfredley marked this pull request as ready for review February 20, 2026 18:07
Copilot AI review requested due to automatic review settings February 20, 2026 18:07
@jamesfredley jamesfredley added this to the grails:7.0.8 milestone Feb 20, 2026
@jamesfredley jamesfredley moved this to In Progress in Apache Grails Feb 20, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request fixes a critical issue where GrailsOpenSessionInViewInterceptor only managed sessions for the default datasource, causing "No Session found for current thread" errors when using withSession on secondary datasources during web requests (issues #14333 and #11798).

Changes:

  • Extended GrailsOpenSessionInViewInterceptor to discover and manage sessions for all configured datasources, not just the default
  • Added comprehensive unit tests directly testing the interceptor's multi-datasource session management
  • Added functional integration tests that verify the fix through HTTP requests to a controller using secondary datasource operations

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
GrailsOpenSessionInViewInterceptor.java Core fix: extends OSIV to iterate all connection sources and open/bind/close sessions for each non-default datasource alongside the default
grails-plugin/build.gradle Added servlet-api test dependency for unit tests
MultiDataSourceSessionSpec.groovy New unit tests verifying session binding, cleanup, skip-if-bound behavior, and CRUD operations
SecondaryBookController.groovy Test controller providing endpoints for functional validation of withSession, validation, and executeUpdate scenarios
UrlMappings.groovy URL mappings to route requests to test controller
MultiDataSourceWithSessionSpec.groovy Integration tests exercising the fix via HTTP to verify both reported issues are resolved
grails-multiple-datasources/build.gradle Added HTTP client and URL mappings dependencies for functional tests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Wrap the additional session cleanup loop in try-finally so that
super.afterCompletion() always runs even if closing an additional
session throws. Add per-session try-catch around closeSession with
error logging, matching HibernatePersistenceContextInterceptor.destroy()
pattern.

Assisted-by: Claude Code <Claude@Claude.ai>
Include the datasource connection name in debug and error log messages
for GrailsOpenSessionInViewInterceptor to aid multi-datasource debugging.

Add a Geb integration test in the datasources test module that verifies
OSIV keeps the secondary datasource session open during GSP view
rendering, allowing lazy-loaded associations to be accessed without
LazyInitializationException.

Assisted-by: Claude Code <Claude@Claude.ai>
@jdaugherty
Copy link
Contributor

Looks like this has some legitimate test failures

Use Book.secondary.findByTitle instead of Book.secondary.first to
avoid picking up stale records from other integration tests sharing
the same H2 database.

Assisted-by: Claude Code <Claude@Claude.ai>
Per-session try-catch in postHandle ensures all additional datasource
sessions are flushed independently. If one datasource's flush fails
(e.g. constraint violation), the remaining datasources still get their
flush attempt. Exceptions are accumulated via addSuppressed and
re-thrown after all sessions are processed.

Addresses jdaugherty's review feedback that a data integrity problem
in one datasource should not prevent other independent datasources
from saving.

Assisted-by: OpenCode <opencode@opencode.ai>
…eptor

When multiple datasource sessions fail to flush, subsequent exceptions are added as
suppressed to the first. Log each suppressed exception at DEBUG level with the
datasource connection name to aid troubleshooting.

Assisted-by: OpenCode <opencode@opencode.ai>
Copy link
Contributor

@jdaugherty jdaugherty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense, but we need another reviewer for this one given the potential for impact.

Copy link
Contributor

@davydotcom davydotcom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test line up with what would happen in a real world scenario. This has been broken a while I dont think we often use multi datasource in a project. But it makes sense that these should be activated in that scenario.

@jdaugherty jdaugherty merged commit 314539f into 7.0.x Feb 25, 2026
32 checks passed
@jdaugherty jdaugherty deleted the fix/multi-datasource-osiv branch February 25, 2026 18:24
@github-project-automation github-project-automation bot moved this from In Progress to Done in Apache Grails Feb 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Grails7 - Multi DB withSession does not work Problem With Multiple Datasources

4 participants