Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to load a StatementInterceptor #5471

Closed
JasonLunn opened this issue Nov 27, 2018 · 11 comments
Closed

Unable to load a StatementInterceptor #5471

JasonLunn opened this issue Nov 27, 2018 · 11 comments

Comments

@JasonLunn
Copy link
Contributor

Prelude

MySQL Connect Java supports an interceptor pattern for specifying classes to use to preprocess SQL commands or postprocess results. Its easy to configure from rails by appending something like:

properties:
    statementInterceptors: com.mysql.jdbc.LoadBalancedAutoCommitInterceptor

to the db properties in database.yml. The above example uses a class provided inside the MySQL jar and works without issue.

Specifying a StatementInterceptor class defined in JRuby fails with the error message:

ActiveRecord::JDBCError: Unable to load statement interceptor 'com.example.mysql.MyStatementInterceptor'.

I'm assuming that I'm running into a classloader issue, but since (I think?) the MySQL Connector J is being loaded by JRuby via the jdbc-mysql gem, I thought the classloader used by the MySQL jar would be able to see any JRuby defined classes. I've tried defining the class with and without an explicit package without a change in results.

I've already been able to confirm (via jrubyc) that the class I defined properly implements com.mysql.jdbc.StatementInterceptorV2 and provides valid methods with appropriate type signatures.

Is this a bug, am I doing it wrong, or is this a limitation of JRuby?

Environment

  • rails 5.1.6
  • jdbc-mysql 5.1.46
  • activerecord-jdbc-adapter 50.0
  • activerecord-jdbcmysql-adapter 50.0
  • JRuby 9.1.17.0
  • Darwin brakebills.local 17.7.0 Darwin Kernel Version 17.7.0: Wed Oct 10 23:06:14 PDT 2018; root:xnu-4570.71.13~1/RELEASE_X86_64 x86_64

Expected Behavior

I expected Java code loaded by a Ruby gem to be able to invoke Class.forName and successfully load a Java class that was defined by JRuby.

Actual Behavior

ActiveRecord::JDBCError: Unable to load statement interceptor 'com.example.mysql.CommitAnnotationStatementInterceptor'.
@dr-itz
Copy link
Contributor

dr-itz commented Nov 27, 2018

Where and how do you define/load your custom class/jar?

The reason I ask is that I think it's a problem of "what is loaded when". Thing is, the JDBC driver jar is only loaded the first time a connection is established. Now if you do something with your custom class before that, it cannot be loaded since it can't find the MySQL JDBC stuff. And then when the MySQL stuff is available, your class is not...

You can force these to be available by simply doing

require 'jdbc/mysql'
::Jdbc::MySQL.load_driver(:require)

before you load your custom class.

Another thing: for Rails 5.1.x you should use activerecord-jdbc-adapter 51.x (currently 51.2) instead of 50.x

@JasonLunn
Copy link
Contributor Author

JasonLunn commented Nov 27, 2018

In our first initializer, we require 'my_statement_interceptor'

In lib/my_statement_interceptor.rb, we have something like:

require 'java'
require 'jdbc/mysql'
Jdbc::MySQL.load_driver(:require)

java_import "com.mysql.jdbc.StatementInterceptorV2"

java_import "com.mysql.jdbc.ResultSetInternalMethods"
java_import "com.mysql.jdbc.Statement"
java_import "com.mysql.jdbc.Connection"
java_import "java.util.Properties"
java_import "java.sql.SQLException"

java_package "com.example.mysql"
class MyStatementInterceptor
    java_implements com.mysql.jdbc.StatementInterceptorV2

    java_signature 'void init( Connection connection, Properties properties )'
    def init _, _
        nil
    end

    java_signature 'void destroy()'
    def destroy
    end

    java_signature 'boolean executeTopLevelOnly()'
    def executeTopLevelOnly
        true
    end

    java_signature 'ResultSetInternalMethods preProcess( String sql, Statement interceptedStatement, Connection connection ) throws SQLException'
    def preProcess _, _, _
        nil
    end

    java_signature 'ResultSetInternalMethods postProcess( String sql, Statement interceptedStatement, ResultSetInternalMethods originalResultSet,
            Connection connection, int warningCount, boolean noIndexUsed, boolean noGoodIndexUsed, SQLException statementException ) throws SQLException'
    def postProcess _, _, _, _, _, _, _, _
        nil
    end

    become_java!
end

I can confirm that MyStatementInterceptor is defined before the first attempt to establish a database connection. I've tried both load_driver and load_driver(:require).

As for why we're on 50.0, we were impacted by a variation of the issue described in jruby/activerecord-jdbc-adapter#897 and have put off that upgrade until we upgrade to rails 5.2.

@JasonLunn
Copy link
Contributor Author

I can reproduce the problem in a fresh rails 5.2 app with the latest activerecord-jdbc-adapter - see https://github.com/JasonLunn/StatementInterceptor. Reproduce by cloning and running ./bin/rails s - you'll see:

=> Booting Puma
=> Rails 5.2.1 application starting in test 
=> Run `rails server -h` for more startup options
Exiting
ActiveRecord::JDBCError: Unable to load statement interceptor 'com.example.mysql.MyStatementInterceptor'.
                                       initialize at arjdbc/jdbc/RubyJdbcConnection.java:551
...

@dr-itz
Copy link
Contributor

dr-itz commented Nov 27, 2018

@JasonLunn
Copy link
Contributor Author

JasonLunn commented Nov 28, 2018

Wow - I owe you a beer. I'm going to close this issue as you've been able to rework the example to successfully load.

If you have the time, @dr-itz, I'd love you're opinion on whether you consider any of the following bugs that deserve their own independent issues filed:

  1. Calling become_java! without previously invoking require 'jruby/core_ext' doesn't raise an exception or log a warning
  2. The optional parameter to become_java isn't documented in the wiki
  3. java_package doesn't change the generated package name
  4. Fully qualified package names are needed in the arguments to java_signature
  5. java_implements only impacts jrubyc

@enebo enebo added this to the Invalid or Duplicate milestone Nov 28, 2018
@dr-itz
Copy link
Contributor

dr-itz commented Nov 28, 2018

@JasonLunn I agree there's room for improvement, especially 3. and 5.
But I think one issue should be enough to discuss the problems, possible solutions, priorities etc.

btw. here's how I debugged this since I basically had no idea about the whole thing (my first wrong assumption was based on AR-JDBC being involved, then I got curios):

  • a "git grep become_java" in the jruby repo pointed me to 'core/src/main/java/org/jruby/java/addons/ClassJavaAddons.java' where i learnt about the two optional arguments

    • one about the class loader
    • the other one a path where the generated .class is dumped to
  • setting some path and then using javap...

  • realizing it's not generating what it should...

  • going to the wiki, figuring out 1.

  • fix all the java_signature errors

  • "include" instead of "java_implements" was again somewhere in the wiki

  • figuring out what is jrubyc-only is basically more git grep :)

anyway, it was kinda fun 😸

@JasonLunn
Copy link
Contributor Author

@enebo - any preference on whether the feature requests / documentation enhancements above are filed together vs separately?

@enebo
Copy link
Member

enebo commented Nov 29, 2018

@JasonLunn file each separately (and possibly just update the wiki for 2 if you understand point of the parameter).

@JasonLunn
Copy link
Contributor Author

I'll open issues for the topics mentioned above that aren't covered by the wiki update later today.

@JasonLunn
Copy link
Contributor Author

Thanks again, @dr-itz - I've opened issues and updated the wiki based on your work. Please let me know if you spot any omissions or misstatements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants