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
Can Redisson be used for Tomcat session and application cache? Getting java.lang.ClassNotFoundException on read #1668
Comments
Could you share config of RedissonSessionManager object? |
This is what I used in the test case,
|
Thanks for shared test application. All works properly if you won't include redisson.jar into war-archive and move jar containing entity classes into apache-tomcat\lib folder. |
So I take it it is not possible otherwise? it would also prevent Context reload with a different war if some of the entities changes and the tomcat has to be reloaded. |
@mrniko I'm experiencing the same issue using just tomcat session management. What I fount out is that if I use JndiRedissonSessionManager (e.g. single redis client) I'm getting ClassNotFoundException. Here is the configuration in context.xml of Tomcat 8.5:
and in server.xml:
here is redisson.yaml:
Now if I switch to use following in context.xml: It doesn't give me ClassNotFoundException - I guess Tomcat is creating a manager per each application and use web application classloader. The issue seems to be when the buildClient() method in RedissionSessionManager is been build. Inside this method I can see that it is using Thread.currentThread().getContextClassLoader() : `Config c = new Config(config);
but when the buildClient() is been invoked when using JNDI redis client is not a part of the web app classloader - and that's why the issue. e.g. to use the Thread.currentThread().getContextClassLoader() directly when trying to deserialize the object but later I found out that the actual retrieval of the bytes from Redis is done in another thread - so Thread.currentThread().getContextClassLoader() will not be set correctly, and it is given the same issue - I guess if it is possible to modify the API to retrieve the byte array only without deserialization (additional API method) and inside I'm using latest redisson 2.15.0 version and redisson-all.jar and redisson-tomcat-8.jar are located only in tomcat lib directory, the yaml file is located in tomcat conf directory. No redisson jar files are packaged with the web application (WAR) - I guess a proper test should be to put custom codec class in yaml file that only exist in web application that is been deployed - this way you should be able to see that the custom coded in yaml file will not be able to be instantiated unless it is using a classloader that is the web app classloader - each web application that is deployed should be able to use their own version of the codec - e.g. the same class name in yaml file but different implementation of the codec with the same name packaged in different web apps |
Which class is not found in your case? RedissonClient? Did you add
That's correct. |
Here is my test application: Use follow url to write attribute: Use follow url to read attribute: |
In my case our own class that is inside WAR - the serializable class that is used to be set into session.setAttribute (in your WAR application the SessionValueCopy)
Yes, I did - there is no issue with finding the JNDI and in my server.xml: I have tried your RedisTestCopy application and I can reproduce the exception that I'm getting. By the way I'm using Tomcat 8.5.35 - not Tomcat 7 In your WAR RedisTestCopy application you are missing SessionValueCopy class (probably you have forgot to add it so I have created a new class in order to test our your WAR and put it into RedisTestCopy\WEB-INF\classes\comcopy\redistestcopy\mycopy) Here is the source code for my SessionValueCopy class: import java.io.Serializable; public class SessionValueCopy implements Serializable {
} Here is the exception that I'm getting: Message Unexpected exception while processing command Description The server encountered an unexpected condition that prevented it from fulfilling the request. Exception org.redisson.client.RedisException: Unexpected exception while processing command java.io.IOException: java.lang.ClassNotFoundException: comcopy.redistestcopy.mycopy.SessionValueCopy java.lang.ClassNotFoundException: comcopy.redistestcopy.mycopy.SessionValueCopy Note that if I change my context.xml file to have this instead : and remove following from server.xml:
Then there is no issue. Here is the exception in tomcat console: |
here is the RedisTestCopy web app that I have tested out: and here is my context.xml: |
@mrniko |
probably to have the following : to create a new method that will take classloader like this: redissonManager.getMap(Thread.currentThread().getContextClassLoader(), id); then RMap (in this case RedissonMap) will have the correct context class loader as then the RedissonMap should pass the classloader into the loader thread somehow so that the codec will have the correct classloader set - and the classloader should be passed again as method argument and not set into the codec as different web application have different classloader and could have different implementation of the specified codec in yaml file and/or serializable session attribute classes Note also that putting classloader into redisson classes should only be done if the class will be unloaded when the web app is going to be unloaded when using shared redisson client - as if you keep web app context classloader in shared classes when the web application gets unloaded will prevent proper garbage collection (classloader memory leak) when the web app shutdown or get reloaded/redeployed. |
use Tomcat 8.5.35 - and configure using JNDI resource and when you use the read : It will throw you the exception |
use the zip file RedisTestCopy.zip that I have attached and put this web application into Tomcat webapps - use the provided context.xml file as well as for the server.xml add this inside: |
I applied change you suggested and now it works. Please try demo attached. |
Could you attach the redisson-tomcat-8.jar file as I'm using Tomcat 8.5 ? |
Here it is: |
@mrniko it is working - thank you |
I'm not sure how you should resolve the issue where you have the RedissonClient bound in the JNDI and web application are using this as a cache (same shared JNDI resource accessed via multiple web applications having different classloaders) - there should be some API that you could pass the classloader so that deserialization works |
I think last implementation fits good for this. There is no way to pass classloader during write/read session data. |
@mrniko I have re implemented the fix so that if JNDI bound redis client is used from web application as cache we don't get the ClassNotFoundException (as it will probably happen with your implementation as you are not passing the correct web app classloader when we use redisson as a cache instead of tomcat session manager, and what original issue creator experienced) - it is working for tomcat session manager as well - I have tested out with our own application and the demo application that you provided. What I did is in org.redisson.command.CommandAsyncService in the method async I have used this code:
` this way when the read is invoked from web app the Thread.currentThread().getContextClassLoader() will be the web app classloader (as up to the call of the async method the thread that is used is the web app thread) which we are going to pass into the codec by creating a new codec class (once the codec is done with it's purpose it will be garbage collected and the reference to the web app classloader will be released so we do not have classloader leak issue - also because we have a new codec for each execution the context classloader will be the correct one in multi concurrent env - the codec is not shared between different async invocations that could cause issues when multiple threads access the same codec - as the classloader is a field in the codec class). This implementation will fix the eager creation of the Codec class when trying to bind into JNDI resource and using the tomcat server classloader and not the web app classloader. Note that I have not used your changes for this issue as I don't know what you changed there. As a side note, as I saw you are creating the Codec object eagerly, the issue with this is that if the codec class reside only in the web application (e.g. the web application is providing the class codec and not the redisson library) then the ClassNotFoundException will be thrown and redisson won't be bind into the JNDI tree - a better approach is to have CodecBuilder object that have only the class name as string and when needed to create the Codec out of the CodecBuilder when you do have the proper classloader available |
@jchobantonov |
@mrniko could you commit your changes into master when possible so I could be able to rebuild jar files out of the master repository. Thank you |
@mrniko as for the comment: make sure the application classloader could be garbage collected if/when application is stopped/redeployed (as web app could be redeployed without bind/unbind JNDI resource) to not cause classloader memory leak - best way is to use WeakReference or SoftReference to the classloader |
Will do. Thanks for reminding! |
@mrniko Hi, and sorry for reporting so many issues. Here is another issue with the ClassNotFoundException. I have used the provided jar files (tomcat-7-2.zip and redisson-tomcat-8-2.15.1-SNAPSHOT.jar.zip) that you attached to this issue and they did work with JNDI bound redis client using following configuration: `
` but throwing ClassNotFoundException when I switch to use this: `
` I had to do this because using default REDIS and DEFAULT settings our usage of Spring WebFlow is going to report that there is no conversation): `
` Now the issue is as follow : ..somewhere down the flow where we do not have access to obj but to the session: ...now obj2 is different than obj (and some frameworks will assume that the objects will be equal - even those objects could be used in some sort of cache where object hashcode and identity will be used. So that's explains why I had to change the configuration to use :
` My suggestion to readMode="REDIS" is when during the request phase someone tried to obtain an object from the session using session.getAttribute() to have internal cache so that if an object was retrieved from session.getAttribute() the next time we ask for the same attribute using session.getAttribute to return the same object as before (e.g. to not use deserialization and create another object) so that following is true: session.getAttribute("test") == session.getAttribute("test") This could be done using similar approach as to UpdateValve - e.g install a new valve when readMode="Redis" that when the request begins will clear internal RedissonSession cache - and after that then RedissonSession.getAttribute() is called first to check the internal cache and if there return the object otherwise to deserialize as before and just before returning the object to put it into request cache - the new valve at the end will clear this cache - so in this way you could know the boundaries of the request. As one enhancement to updateMode="DEFAULT" or creation of another mode could be not only to update Redis during session.setAttribute but also at the end of the request if the session.getAttribute was used and this attribute was mutable (e.g. not int, String etc but a Serializable object) so that if the application change this object at the end a synchronization to this object with redis will happen even if we do not invoke session.setAttribute for such object. Now to the issue with the configuration readMode="MEMORY" and updateMode="AFTER_REQUEST" and ClassNotFoundException - here is the stacktrace: java.io.IOException: java.lang.ClassNotFoundException: com.XXX ` Note that I put XXX in places to obfuscate either binary dump that happens or our class names. @OverRide
` I think the actual deserialization should happen inside onMessage method by passing the classloader that was given during startInternal() method - and the state should not be Map<String, Object> but it should be Map<String, byte[]> - so that the won't be an issue getting the AttributesPutAllMessage or AttributeUpdateMessage deserialized but use CustomObjectInputStream to decode the byte[] in onMessage and web application classloader And one more thing - during the testing I saw a bunch of messages in Tomcat console after the request which kept going on - looked like infinite loop occur because of the sending of the messages using RTopic - not sure if there is a retry mechanism there if the message listener is not invoked, or probably the message was not removed from the topic because of the ClassNotFoundException ... - not sure but those log message kept going on even after the request was completed. |
@mrniko I did verified my suggestion to use byte[] instead of the actual Object in AttributesPutAllMessage and AttributeUpdateMessage as well as getting the application classloader like this: ` final ClassLoader applicationClassLoader = Thread.currentThread().getContextClassLoader() != null ? Thread.currentThread().getContextClassLoader() : this.getClass().getClassLoader(); ` in RedissonSessionManager and it is working fine with this configuration now: `
` There is no logs in Tomcat that shows exceptions and it is not constantly looping with those exceptions Note that I do still have the changes in those files related to my PR that I have send you, so you could ignore the fastPut changes there related to session timeout Please see the attached sources and let me know what you think My concern with using MEMORY and AFTER_REQUEST is if there are some disconnect between the tomcat and redis server we could miss some updates to the session so I would prefer if I could use REDIS and DEFAULT for my case (extended version of those things) |
Thank you for shared code! I applied your changes to current version. Please check it |
@mrniko could you provide redisson-all jar as well? I have tried to build the latest from master but I'm getting compilation error: ` [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.0:compile (default-compile) on project redisson: Compilation failure: Compilation failure: Take a look at this commit that I did in my PR that fixes those compilation errors: |
Fixed |
@mrniko still compilation issue: ` [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.0:compile (default-compile) on project redisson: Compilation failure |
@mrniko alter you fix it could you provide me with your copy of redisson-all jar as well - because you only send me the redisson-tomcat-8 jar and I don't know what branch you are using to compile all this |
Sorry, now it's fixed |
Here is compiled version |
@mrniko thank you for providing redisson-all jar - I have tested out using this provided jar file and the latest redisson-tomcat-8 jar files attached here. When I have this configuration:
it is now working fine and it does not throw ClassNotFoundException, but if I change it to :
it is not working. With REDIS and DEFAULT it throws following exception:
Did you forget to add the fix for ClassNotFoundException issue with redisson-all jar ? |
@mrniko I have implemented a feature to actually cache the object from getAttibute in thread local variable (requestSessionAttributeCache) so that each time the application invokes session.getAttribute() it will get the same object - Note that in case of readMode != REDIS this is the case as we are using super.getAttribute() of Tomcat which internally uses HashMap, but in case of readMode = REDIS we are deserializing the object out of Redis server each time the application invokes session.getAttribute() - using this feature will make readMode = REDIS and readMode = MEMORY behave the same way - e.g. the object that will first be returned will be the same object if we do another session.getAttribute() this in the scope of the request processing (the only caveat here is that this is not done for String, Number, Boolean and Character as those are immutable objects). So during request processing if the app do this using readMode = REDIS and readMode = MEMORY it will behave the same:
in both cases REDIS and MEMORY now. (Note that this a is a simple case where we could use the obj instead of obj1 to get the modified name but you could imagine if we have servlet Filters layered to use a common object via the session attribute then if the Filter1 is creating the obj, setting it into the session and after that modifies it, then Filter2 is invoked that will get the object out of session attribute - it will not get the same object - it will always get a clone without this feature.) Previously in mode REDIS this will not be true as session.getAttribute("test") will create new object out of serialized data from Redis server which deserialization could be expensive operation to do each time we call session.getAttribute() for the same object during request processing - Note that the difference between readMode=REDIS and readMode=MEMORY here is that in readMode=REDIS the same object will be returned only in the scope of the same request processing. Next request processing will get the value out of Redis server again. The feature here also add each object that is returned from super.getAttribute() and it is not String, Number, Boolean or Character - e.g. assuming mutable object into the thread local variable as well so to keep track and update this object at the end of the request processing in order to solve following issue (and as I already mentioned Spring WebFlow is using this pattern and I guess many more web frameworks as well):
Key points:
alter setAttribute, getAttribute, removeAttribute to make usage of requestSessionAttributeCache (search attached sources to see where it is used) In RedissonSessionManager add new RequestSessionAttributeCacheValve valve in startInternal:
And in RequestSessionAttributeCacheValve it will clear thread local variable requestSessionAttributeCache once the request processing completes and it will save any thread local session attributes that are mutable and were accesses by the application - either via setAttribute() or if getAttribute() returns "mutable" object which might be altered by the web application (this is not guarantee that the object will be modified or even mutable but this way we are going to solve the issue defined above if the web application is able to alter the object) Here is the source of the 3 files (RedissonSession, RedissonSessionManager, RequestSessionAttributeCacheValve) in redisson-tomcat-8: |
@mrniko I forgot to mention that the new feature made our application to run using REDIS/DEFAULT (note usage of Spring WebFlow) and I have tested out with MEMORY/AFTER_REQUEST as well - which works as well. Here is a side note:
this will guarantee that att1 will have MYVALUE1 and att2 will have MYVALUE2 in redis - this starts to matter if you see the session as atomic thing - e.g. if att1 write succeed with setting value to MYVALUE1 but write of att2 do not - redis won't be having only att1 set to MYVALUE1 which could be invalid case - e.g. either the 2 attributes are set or none are set This enhancement could be implemented using a new read/write mode let say REQUEST_TRANSACTION or something where all attributes are written at once in REDIS instead one by one as web application don't have opportunity to write 2 or more attributes in the same call - the only option here is to have value object that represent the transaction and this way when you write the value object into session attribute it will be atomic. |
This fix is not applied yet. |
Could you extract this to another issue? |
…tance used in tomcat and application. #1668
@jchobantonov Thank you! |
Fixed! @jchobantonov thanks for your help! |
…tance used in tomcat and application. #1668
Here's my Cliffs Notes takeaway on how to resolve this issue in your application now that the code fixes are in place.
|
I have a webapp using Redisson to handle Hibernate cache, application cache and session persistence.
However, each time my webapp client attempt to READ back from Redis, Redisson give back an java.lang.ClassNotFoundException on every object saved in Redis.
If I remove all lib from the lib folder and disable the session manager, the webapp client work perfectly.
I suspect a class loader conflict, as Redisson's classes are loaded inside the Server Class loader, while the webapp classes inside the Webapp class loader. The sessions objects are working as all is handled at Tomcat level, but the client is jumping between both class loader.
Anyone was able to get both working altogether, or is it require to use 2 Redis Java client implementation, one for the Session and the other for the Webapps?
Expected behavior
Tomcat sessions are saved.
Object are saved and restored from Redis in the Webapps
Actual behavior
Tomcat Session are saved and restored.
Webapps objects are saved but cause a "java.lang.ClassNotFoundException" upon restore.
Steps to reproduce or test case
Create a plain tomcat instance.
Put redisson-all and redisson-tomcat jars in lib folder
Create a war with a 2 serializable entities and a servlet that read and save said entity in the session And the client directly (caching).
Redis version
4.0.9
Redisson version
3.8.2
Redisson configuration
{
"singleServerConfig":{
"idleConnectionTimeout":10000,
"pingTimeout":1000,
"connectTimeout":10000,
"timeout":3000,
"retryAttempts":3,
"retryInterval":1500,
"password":null,
"subscriptionsPerConnection":5,
"clientName":"tomcat",
"address": "redis://127.0.0.1:6379",
"subscriptionConnectionMinimumIdleSize":1,
"subscriptionConnectionPoolSize":50,
"connectionMinimumIdleSize":32,
"connectionPoolSize":64,
"database":1
},
"threads":0,
"nettyThreads":0,
"codec":{
"class":"org.redisson.codec.SnappyCodec"
},
"transportMode":"NIO"
}
RedissonTomcatTest.zip
The text was updated successfully, but these errors were encountered: