jdaugherty commented on code in PR #16631: URL: https://github.com/apache/grails-core/pull/16631#discussion_r4381632996
########## grails-data-hibernate7/core/src/main/groovy/org/apache/grails/data/hibernate7/core/querytail/QueryTailStatementInspector.groovy: ########## @@ -0,0 +1,354 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.grails.data.hibernate7.core.querytail + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import org.hibernate.SessionFactory +import org.hibernate.SessionFactoryObserver +import org.hibernate.boot.registry.StandardServiceRegistryBuilder +import org.hibernate.boot.registry.selector.spi.StrategySelectionException +import org.hibernate.boot.registry.selector.spi.StrategySelector +import org.hibernate.cfg.AvailableSettings +import org.hibernate.cfg.Configuration +import org.hibernate.dialect.AbstractTransactSQLDialect +import org.hibernate.dialect.CockroachDialect +import org.hibernate.dialect.DB2Dialect +import org.hibernate.dialect.Dialect +import org.hibernate.dialect.H2Dialect +import org.hibernate.dialect.HANADialect +import org.hibernate.dialect.HSQLDialect +import org.hibernate.dialect.MySQLDialect +import org.hibernate.dialect.OracleDialect +import org.hibernate.dialect.PostgreSQLDialect +import org.hibernate.dialect.SpannerDialect +import org.hibernate.dialect.SpannerPostgreSQLDialect +import org.hibernate.engine.spi.SessionFactoryImplementor +import org.hibernate.resource.jdbc.spi.StatementInspector + +import org.apache.grails.data.core.querytail.QueryTailCapture +import org.apache.grails.data.core.querytail.QueryTailSettings +import org.apache.grails.data.core.querytail.QueryTailSyntax +import org.grails.datastore.mapping.core.exceptions.ConfigurationException +import org.grails.orm.hibernate.cfg.HibernateMappingContextConfiguration + +/** + * The statement inspector GORM installs on a session factory when query-tail capture is enabled. It + * records every statement Hibernate is about to run to the {@link QueryTailCapture} of the + * application, without its values, and returns the statement as it found it: the database runs + * it unchanged. The inspector the session factory would run without the capture keeps running, + * wherever the application configured it: with {@code hibernate.session_factory.statement_inspector}, + * on the configuration with {@link Configuration#setStatementInspector}, or in the settings of the + * service registry, as a {@code hibernate.configClass} may. It sees the statement first, and the + * statement it returns is the one run and recorded. + * + * <p>The session factory owns the capture. The inspector observes it: it opens the capture when + * the session factory is created, takes from its dialect how the database quotes values, and + * closes the capture when the session factory closes, so a session factory that fails to start + * leaves no capture open either. An observer the application set on the configuration keeps + * being called, even one it sets as the configuration builds the session factory.</p> + * + * <p>Nothing of this is installed unless {@link QueryTailSettings#SETTING_ENABLED} is set, so an + * application that does not capture pays nothing per statement.</p> + * + * @since 8.1 + */ +@CompileStatic +class QueryTailStatementInspector implements StatementInspector, SessionFactoryObserver { + + @Serial + private static final long serialVersionUID = 1L + + /** The community dialects, which are not a dependency, that the inspector tells apart by name. */ + private static final String COMMUNITY_INFORMIX = 'org.hibernate.community.dialect.InformixDialect' + private static final String COMMUNITY_SQL_ANYWHERE = 'org.hibernate.community.dialect.SybaseAnywhereDialect' + + /** The connection source name statements are recorded under. */ + final String dataSourceName + + /** The file statements are recorded to. */ + final File file + + private SessionFactoryObserver observer + private StatementInspector delegate + private boolean wrapped + private transient volatile QueryTailCapture capture + private transient boolean closed + + /** + * @param dataSourceName the connection source the inspected session factory belongs to + * @param file the capture file statements are recorded to + */ + QueryTailStatementInspector(String dataSourceName, File file) { + this.dataSourceName = dataSourceName + this.file = file + } + + /** + * Installs the inspector on the configuration of a session factory when capture is enabled, as + * the observer and the statement inspector the configuration puts in place of those of the + * session factory when it builds the session factory. + * + * @param dataSourceName the connection source the session factory is built for + * @param settings the query-tail settings of the connection source + * @param configuration the configuration the session factory is built from + * @return the installed inspector, or null when capture is not enabled + */ + static QueryTailStatementInspector install(String dataSourceName, QueryTailSettings settings, + HibernateMappingContextConfiguration configuration) { + if (settings == null || !settings.enabled) { + return null + } + QueryTailStatementInspector inspector = new QueryTailStatementInspector(dataSourceName, settings.resolveFile()) + configuration.queryTailStatementInspector = inspector + inspector + } + + /** + * Takes the place of the statement inspector the session factory would run, and runs it: the + * one set on the configuration, which Hibernate prefers, else the one the settings of the + * service registry name, where the properties of the configuration are applied over the + * settings the registry builder came with. The configuration calls this as it builds the + * session factory, once every setting is in place. + * + * @param registry the builder of the service registry the session factory is built with + * @param configuration the configuration the session factory is built from + * @throws ConfigurationException when the inspector the application configured cannot be created + */ + void wrap(StandardServiceRegistryBuilder registry, Configuration configuration) { + if (!wrapped) { + delegate = configuration.statementInspector ?: resolve(registry.bootstrapServiceRegistry.getService(StrategySelector), + registry.settings.get(AvailableSettings.STATEMENT_INSPECTOR)) + wrapped = true + } + registry.applySetting(AvailableSettings.STATEMENT_INSPECTOR, this) + configuration.statementInspector = this + } + + /** + * Takes the place of the session factory observer the configuration holds, and tells that one, + * set by the application, of every event first. The configuration calls this as it builds the + * session factory, after the application had every chance to set its own, even as the build + * began. + * + * @param configuration the configuration the session factory is built from + */ + void observe(Configuration configuration) { + SessionFactoryObserver configured = configuration.sessionFactoryObserver + // a configuration built again holds this inspector already, in front of the observer it had + if (configured !== this) { + observer = configured + } + configuration.sessionFactoryObserver = this + } + + /** + * @return the inspector the session factory would run without the capture, or null when it + * would run none; known once the session factory is built + */ + StatementInspector getDelegate() { + delegate + } + + /** + * @return the session factory observer the application configured, or null when it configured + * none; known once the session factory is built + */ + SessionFactoryObserver getObserver() { + observer + } + + /** + * Resolves the inspector an application configured as Hibernate does: an instance, a class, a + * class name or a name registered with the strategy selector of the service registry. + * + * @param strategies the strategy selector of the service registry the session factory is built with + * @param configured the configured value, or null + * @return the inspector, or null when nothing was configured + * @throws ConfigurationException when the configured inspector cannot be loaded or created + */ + @PackageScope + static StatementInspector resolve(StrategySelector strategies, Object configured) { + if (configured == null) { + return null + } + String name = configured instanceof Class ? ((Class) configured).name : configured.toString() + try { + strategies.resolveStrategy(StatementInspector, configured) + } + catch (StrategySelectionException e) { + throw new ConfigurationException("Cannot load the statement inspector [${name}] configured with " + + AvailableSettings.STATEMENT_INSPECTOR, e) + } + catch (ClassCastException e) { + // the selector creates whatever class is named, and only the cast of what it creates tells it is no inspector + throw new ConfigurationException("The statement inspector [${name}] configured with " + + "${AvailableSettings.STATEMENT_INSPECTOR} does not implement ${StatementInspector.name}", e) + } + } + + @Override + String inspect(String sql) { + String executed = sql + if (delegate != null) { + String inspected = delegate.inspect(sql) + if (inspected != null) { + executed = inspected + } + } + QueryTailCapture current = openCapture() + if (current != null) { + current.record(dataSourceName, executed) + } + executed + } + + /** + * Opens the capture, and records the statements of the session factory in the syntax of its dialect. + */ + @Override + void sessionFactoryCreated(SessionFactory factory) { + if (observer != null) { + observer.sessionFactoryCreated(factory) + } + QueryTailCapture current = openCapture() + if (current != null && factory instanceof SessionFactoryImplementor) { + current.syntax = syntaxOf(factory.jdbcServices.dialect) + } + } + + /** + * The syntax the statements of a dialect are redacted in: that of its database, where the + * redactor knows the database's lexical rules. Cloud Spanner's PostgreSQL dialect is not known, + * as the parser of a Spanner PostgreSQL database has not been verified, and neither is SQL + * Anywhere's, a Transact-SQL dialect whose database reads {@code //} as a comment; the Informix + * dialect among the community dialects is known. Any other dialect, such as SQLite's, Firebird's + * or a dialect that is not Hibernate's, is redacted as for a database that is not known: a + * statement is then recorded only when every known syntax reads it the same way. + * + * @param dialect the dialect of a session factory + * @return the syntax its statements are redacted in + */ + static QueryTailSyntax syntaxOf(Dialect dialect) { + if (dialect instanceof SpannerPostgreSQLDialect || extendsNamed(dialect, COMMUNITY_SQL_ANYWHERE)) { + return QueryTailSyntax.UNKNOWN + } + if (dialect instanceof AbstractTransactSQLDialect) { + return QueryTailSyntax.TRANSACT_SQL + } + if (dialect instanceof SpannerDialect) { + return QueryTailSyntax.GOOGLE_SQL + } + if (dialect instanceof MySQLDialect) { + return QueryTailSyntax.MYSQL + } + if (dialect instanceof CockroachDialect) { + return QueryTailSyntax.COCKROACH + } + if (dialect instanceof PostgreSQLDialect) { + return QueryTailSyntax.POSTGRESQL + } + if (dialect instanceof H2Dialect) { + return QueryTailSyntax.H2 + } + if (dialect instanceof OracleDialect) { + return QueryTailSyntax.ORACLE + } + if (dialect instanceof DB2Dialect) { + return QueryTailSyntax.DB2 + } + if (dialect instanceof HSQLDialect) { + return QueryTailSyntax.HSQLDB + } + if (dialect instanceof HANADialect) { + return QueryTailSyntax.HANA + } + if (extendsNamed(dialect, COMMUNITY_INFORMIX)) { + return QueryTailSyntax.INFORMIX + } + QueryTailSyntax.UNKNOWN + } + + /** @return whether a dialect is, or extends, the named class, which may not be on the class path */ + private static boolean extendsNamed(Dialect dialect, String className) { + for (Class<?> type = dialect.getClass(); type != null; type = type.superclass) { + if (type.name == className) { + return true + } + } + false + } + + @Override + void sessionFactoryClosing(SessionFactory factory) { + if (observer != null) { + observer.sessionFactoryClosing(factory) + } + } + + /** + * Closes the capture. + */ + @Override + void sessionFactoryClosed(SessionFactory factory) { + try { + if (observer != null) { + observer.sessionFactoryClosed(factory) + } + } + finally { + close() + } + } + + /** + * Stops recording and closes the capture; a capture that was never opened stays unopened. + */ + synchronized void close() { Review Comment: Fixed in 52808b3641, on both Hibernate versions. Each session factory now gets its own inspector, and so its own capture handle: - `install()` stores only the capture file on the configuration. - Every `buildSessionFactory()` creates a new `QueryTailStatementInspector` for that factory, wraps the statement inspector and observer the application set, and builds the factory. - After the build, the configuration's statement inspector and observer are restored, so the next build wraps them afresh instead of wrapping the previous inspector. Closing one factory now closes only its own capture handle. The file stays open while any other factory still holds one, and a factory built after every handle has closed opens a new capture. Hibernate 5 had a second gap in the same scenario. Its registry-closing observer destroyed the configuration's most recent service registry, so closing the first factory tore down the second's registry, and the second failed with `Provider is closed`. Each closing observer now holds the registry of its own build. The new feature in both versions' `HibernateConnectionSourceFactorySpec` covers your scenario: - One configuration builds two factories, each runs a statement, the first closes, and the second runs another. All three statements are recorded. - Then the second closes, and a third factory built from the same configuration records into a new run. Against the previous code it fails on both versions. On H7 the surviving factory's statement is missing from the capture, and on H5 the surviving factory fails with `Provider is closed`. The full `grails-data-hibernate5-core` and `grails-data-hibernate7-core` suites pass locally, along with their code style checks. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
