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

Can Redisson be used for Tomcat session and application cache? Getting java.lang.ClassNotFoundException on read #1668

Closed
pbusquemdf opened this issue Oct 10, 2018 · 45 comments
Milestone

Comments

@pbusquemdf
Copy link

pbusquemdf commented Oct 10, 2018

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

@pbusquemdf
Copy link
Author

catalina.2018-10-10.log

@mrniko mrniko added this to the 2.13.3 milestone Oct 29, 2018
@mrniko
Copy link
Member

mrniko commented Oct 29, 2018

Could you share config of RedissonSessionManager object?

@pbusquemdf
Copy link
Author

This is what I used in the test case,

{
"singleServerConfig":{
"idleConnectionTimeout":10000,
"pingTimeout":1000,
"connectTimeout":10000,
"timeout":3000,
"retryAttempts":3,
"retryInterval":1500,
"password":null,
"subscriptionsPerConnection":5,
"clientName":"toto",
"address": "redis://127.0.0.1:6379",
"subscriptionConnectionMinimumIdleSize":1,
"subscriptionConnectionPoolSize":50,
"connectionMinimumIdleSize":32,
"connectionPoolSize":64,
"database":0
},
"threads":0,
"nettyThreads":0,
"codec":{
"class":"org.redisson.codec.FstCodec"
},
"transportMode":"NIO"
}

@mrniko mrniko modified the milestones: 2.13.3, 2.14.1 Oct 31, 2018
@mrniko
Copy link
Member

mrniko commented Nov 5, 2018

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.

@mrniko mrniko removed this from the 2.14.1 milestone Nov 5, 2018
@mrniko mrniko added the question label Nov 5, 2018
@pbusquemdf
Copy link
Author

So I take it it is not possible otherwise?
In most situation, where you have a WAR to deploy, you cannot extract the entities from the WAR and manually install them into the shared classpath folder.

it would also prevent Context reload with a different war if some of the entities changes and the tomcat has to be reloaded.

@jchobantonov
Copy link

@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:
`

<Manager className="org.redisson.tomcat.JndiRedissonSessionManager" jndiName="session/redisson" readMode="REDIS" updateMode="DEFAULT"/>`

and in server.xml:

<Resource auth="Container" configPath="${catalina.home}/conf/redisson.yaml" factory="org.redisson.JndiRedissonFactory" name="session/redisson"/>

here is redisson.yaml:

singleServerConfig: idleConnectionTimeout: 10000 pingTimeout: 1000 connectTimeout: 10000 timeout: 3000 retryAttempts: 3 retryInterval: 1500 password: null subscriptionsPerConnection: 5 clientName: null address: "redis://127.0.0.1:6379" subscriptionConnectionMinimumIdleSize: 1 subscriptionConnectionPoolSize: 50 connectionMinimumIdleSize: 32 connectionPoolSize: 64 database: 0 dnsMonitoringInterval: 5000 threads: 0 nettyThreads: 0 codec: !<org.redisson.codec.SerializationCodec> {} transportMode: "NIO"

Now if I switch to use following in context.xml:
<Manager className="org.redisson.tomcat.RedissonSessionManager" configPath="${catalina.home}/conf/redisson.yaml" readMode="REDIS" updateMode="DEFAULT"/>

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);
Codec codec = c.getCodec().getClass().getConstructor(ClassLoader.class)
.newInstance(Thread.currentThread().getContextClassLoader());
config.setCodec(codec);
} catch (Exception e) {
throw new IllegalStateException("Unable to initialize codec with ClassLoader parameter", e);
}

        return Redisson.create(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.
I have tried to modify org.redisson.codec.SerializationCodec and use this:
ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader(); if (threadClassLoader != null) { inputStream = new CustomObjectInputStream(threadClassLoader, in); } else if (classLoader != null) { inputStream = new CustomObjectInputStream(classLoader, in); } else { inputStream = new ObjectInputStream(in); }

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
org.redisson.tomcat.RedissonSession getAttribute method to do deserialization of the byte array using proper web application classloader or somehow to pass the correct web app classloader during retrieval of objects from redis (using deserialization)

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

@mrniko
Copy link
Member

mrniko commented Jan 9, 2019

Which class is not found in your case? RedissonClient?

Did you add ResourceLink element in context.xml too? JNDI bean registered globally couldn't be found without it.

Tomcat is creating a manager per each application and use web application classloader

That's correct.

@mrniko
Copy link
Member

mrniko commented Jan 9, 2019

Here is my test application:

tomcat-7-1.zip

Use follow url to write attribute:
http://localhost:8091/RedisTestCopy/write?key=vsdf&value=1

Use follow url to read attribute:
http://localhost:8091/RedisTestCopy/read?key=vsdf

@jchobantonov
Copy link

@mrniko

Which class is not found in your case? RedissonClient?

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)

Did you add ResourceLink element in context.xml too? JNDI bean registered globally couldn't be found without it.

Yes, I did - there is no issue with finding the JNDI
here are my context.xml settings as well:
<ResourceLink name="session/redisson" global="session/redisson" type="org.redisson.api.RedissonClient" />
<Manager className="org.redisson.tomcat.JndiRedissonSessionManager" readMode="REDIS" updateMode="DEFAULT" jndiName="session/redisson" />

and in my server.xml:
<Resource name="session/redisson" auth="Container" factory="org.redisson.JndiRedissonFactory" configPath="${catalina.home}/conf/redisson.yaml" />

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:
`
package comcopy.redistestcopy.mycopy;

import java.io.Serializable;

public class SessionValueCopy implements Serializable {

private String value;

public SessionValueCopy() {
}

public SessionValueCopy(String value) {
    this.value = value;
}

public String getValue() {
    return value;
}

public void setValue(String value) {
    this.value = value;
}

public String toString() {
    return value;
}

}
`
also I saw that you do have tomcat-servlet-api-9.0.10.jar into RedisTestCopy\WEB-INF\lib directory, you should not have this jar file there - although even if the jar file is in the lib directory the issue is exposed as well.

Here is the exception that I'm getting:
`
HTTP Status 500 – Internal Server Error
Type Exception Report

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
org.redisson.command.CommandAsyncService.convertException(CommandAsyncService.java:402)
org.redisson.command.CommandAsyncService.get(CommandAsyncService.java:201)
org.redisson.RedissonObject.get(RedissonObject.java:76)
org.redisson.RedissonMap.get(RedissonMap.java:281)
org.redisson.tomcat.RedissonSession.getAttribute(RedissonSession.java:77)
org.apache.catalina.session.StandardSessionFacade.getAttribute(StandardSessionFacade.java:103)
comcopy.redistestcopy.mycopy.TestServletCopy.doGet(TestServletCopy.java:49)
javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
Root Cause

java.io.IOException: java.lang.ClassNotFoundException: comcopy.redistestcopy.mycopy.SessionValueCopy
org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:54)
org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:330)
org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:130)
org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:110)
io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502)
io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366)
io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:656)
io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:591)
io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:508)
io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:470)
io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:909)
io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
java.lang.Thread.run(Thread.java:811)
Root Cause

java.lang.ClassNotFoundException: comcopy.redistestcopy.mycopy.SessionValueCopy
java.lang.Class.forNameImpl(Native Method)
java.lang.Class.forName(Class.java:365)
java.io.ClassCache$FutureValue.get(ClassCache.java:177)
java.io.ClassCache.get(ClassCache.java:148)
java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:831)
org.redisson.codec.CustomObjectInputStream.resolveClass(CustomObjectInputStream.java:46)
java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2012)
java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1895)
java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2189)
java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1717)
java.io.ObjectInputStream.readObjectImpl(ObjectInputStream.java:537)
java.io.ObjectInputStream.readObject(ObjectInputStream.java:472)
org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:50)
org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:330)
org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:130)
org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:110)
io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502)
io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366)
io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:656)
io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:591)
io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:508)
io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:470)
io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:909)
io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
java.lang.Thread.run(Thread.java:811)
Note The full stack trace of the root cause is available in the server logs.`

Note that if I change my context.xml file to have this instead :
<Manager className="org.redisson.tomcat.RedissonSessionManager" configPath="${catalina.home}/conf/redisson.yaml" readMode="REDIS" updateMode="DEFAULT"/>

and remove following from server.xml:

<Resource name="session/redisson" auth="Container" factory="org.redisson.JndiRedissonFactory" configPath="${catalina.home}/conf/redisson.yaml" />

Then there is no issue.

Here is the exception in tomcat console:
[redisson-netty-1-1] ERROR org.redisson.client.handler.CommandDecoder - Unable to decode data. reply: $102 ?? ♣sr -comcopy.redistestcopy.mycopy.SessionValueCopy7j♀??r☻ ☺L ♣valuet ↕Ljava/lang/String;xpt ♦test , channel: [id: 0x119c88c0, L:/127.0.0.1:56665 - R:127.0.0.1/127.0.0.1:6379], command: CommandData [promise=RedissonPromise [promise=ImmediateEventExecutor$ImmediatePromise@db45c04b(incomplete)], command=(HGET), params=[redisson:tomcat_session:78BF2657246EF9086A306832326AB542, ?? java.io.IOException: java.lang.ClassNotFoundException: comcopy.redistestcopy.mycopy.SessionValueCopy at org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:54) at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:330) at org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:130) at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:110) at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502) at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366) at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965) at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163) at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:656) at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:591) at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:508) at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:470) at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:909) at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) at java.lang.Thread.run(Thread.java:811) Caused by: java.lang.ClassNotFoundException: comcopy.redistestcopy.mycopy.SessionValueCopy at java.lang.Class.forNameImpl(Native Method) at java.lang.Class.forName(Class.java:365) at java.io.ClassCache$FutureValue.get(ClassCache.java:177) at java.io.ClassCache.get(ClassCache.java:148) at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:831) at org.redisson.codec.CustomObjectInputStream.resolveClass(CustomObjectInputStream.java:46) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2012) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1895) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2189) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1717) at java.io.ObjectInputStream.readObjectImpl(ObjectInputStream.java:537) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:472) at org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:50) ... 33 more [redisson-netty-1-1] WARN io.netty.channel.DefaultChannelPipeline - An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception. io.netty.handler.codec.DecoderException: java.io.IOException: java.lang.ClassNotFoundException: comcopy.redistestcopy.mycopy.SessionValueCopy at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:421) at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965) at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163) at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:656) at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:591) at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:508) at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:470) at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:909) at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) at java.lang.Thread.run(Thread.java:811) Caused by: java.io.IOException: java.lang.ClassNotFoundException: comcopy.redistestcopy.mycopy.SessionValueCopy at org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:54) at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:330) at org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:130) at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:110) at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502) at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366) ... 28 more Caused by: java.lang.ClassNotFoundException: comcopy.redistestcopy.mycopy.SessionValueCopy at java.lang.Class.forNameImpl(Native Method) at java.lang.Class.forName(Class.java:365) at java.io.ClassCache$FutureValue.get(ClassCache.java:177) at java.io.ClassCache.get(ClassCache.java:148) at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:831) at org.redisson.codec.CustomObjectInputStream.resolveClass(CustomObjectInputStream.java:46) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2012) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1895) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2189) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1717) at java.io.ObjectInputStream.readObjectImpl(ObjectInputStream.java:537) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:472) at org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:50) ... 33 more

@jchobantonov
Copy link

jchobantonov commented Jan 9, 2019

here is the RedisTestCopy web app that I have tested out:
RedisTestCopy.zip

and here is my context.xml:
context.zip

@jchobantonov
Copy link

here is the exception screenshot:
image

@jchobantonov
Copy link

@mrniko
The issue is that when we define resource into server.xml this resource will be instantiated using org.redisson.JndiRedissonFactory outside of the web application scope - getObjectInstance method will be invoked in order to bind the object into the JNDI and at this point buildClient method is invoked that won't have the web app classpath set into Thread.currentThread().getContextClassLoader())- the same way how JDBC resources will be instantiated and bound into JNDI - outside web app as those resources are server specific and not web app specific and needs to be there (created and bound into JNDI) before web app starts so that after web app starts it will have those resources available

@jchobantonov
Copy link

jchobantonov commented Jan 9, 2019

probably to have the following :
In RedissonSession instead of using :
redissonManager.getMap(id);

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
RedissonSession.setId should be invoked from inside the web app

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.

@jchobantonov
Copy link

use Tomcat 8.5.35 - and configure using JNDI resource
then use to write:
http://localhost:8091/RedisTestCopy/write?key=vsdf&value=1

and when you use the read :
http://localhost:8091/RedisTestCopy/read?key=vsdf

It will throw you the exception

@jchobantonov
Copy link

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:
<Resource name="session/redisson" auth="Container" factory="org.redisson.JndiRedissonFactory" configPath="${catalina.home}/conf/redisson.yaml" />
here is the redisson.yaml as well:

redisson.yaml.zip

@mrniko
Copy link
Member

mrniko commented Jan 9, 2019

I applied change you suggested and now it works. Please try demo attached.

tomcat-7-2.zip

@mrniko mrniko added this to the 2.15.1 milestone Jan 9, 2019
@jchobantonov
Copy link

Could you attach the redisson-tomcat-8.jar file as I'm using Tomcat 8.5 ?

@mrniko
Copy link
Member

mrniko commented Jan 9, 2019

Here it is:

redisson-tomcat-8-2.15.1-SNAPSHOT.jar.zip

@jchobantonov
Copy link

@mrniko it is working - thank you

@jchobantonov
Copy link

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

@mrniko
Copy link
Member

mrniko commented Jan 10, 2019

@jchobantonov

I think last implementation fits good for this. There is no way to pass classloader during write/read session data.

@jchobantonov
Copy link

@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:
`

    Codec codecToUse = codec;
    ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader();
    if (threadClassLoader != null) {
    	try {
    		codecToUse = codec.getClass().getConstructor(ClassLoader.class).newInstance(threadClassLoader);
        } catch (Exception ignored) {
        }
    }
    details.init(connectionFuture, attemptPromise,
            readOnlyMode, source, codecToUse, command, params, mainPromise, attempt);

`

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.
Here is the modified CommandAsyncService that works for me:

CommandAsyncService.zip

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

@mrniko
Copy link
Member

mrniko commented Jan 11, 2019

@jchobantonov
Sounds reasonable, thanks for suggestion! I removed all codec instantiations and applied your change but cached it in LRUCacheMap<ClassLoader, Codec> map. Please give it a try:

tomcat-7-2.zip

redisson-tomcat-8-2.15.1-SNAPSHOT.jar.zip

@mrniko mrniko removed the question label Jan 11, 2019
@jchobantonov
Copy link

@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

@jchobantonov
Copy link

@mrniko as for the comment:
LRUCacheMap<ClassLoader, Codec>

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

@mrniko
Copy link
Member

mrniko commented Jan 15, 2019

@jchobantonov

Will do. Thanks for reminding!

@jchobantonov
Copy link

@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:

`

<Manager className="org.redisson.tomcat.JndiRedissonSessionManager"
     readMode="REDIS"
	 updateMode="DEFAULT"
     jndiName="session/redisson" />

`

but throwing ClassNotFoundException when I switch to use this:

`

<Manager className="org.redisson.tomcat.JndiRedissonSessionManager"
     readMode="MEMORY"
	 updateMode="AFTER_REQUEST"
     jndiName="session/redisson" />

`

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):

`
//example in Spring WebFlow org.springframework.webflow.conversation.impl.SessionBindingConversationManager:

private ConversationContainer getConversationContainer() {
	SharedAttributeMap sessionMap = ExternalContextHolder.getExternalContext().getSessionMap();
	synchronized (sessionMap.getMutex()) {
		ConversationContainer container = (ConversationContainer) sessionMap.get(sessionKey);
		if (container == null) {
			container = createConversationContainer();
			sessionMap.put(sessionKey, container);
		}
		return container;
	}
}

public Conversation beginConversation(ConversationParameters conversationParameters) throws ConversationException {
	return getConversationContainer().createConversation(conversationParameters, conversationLockFactory);
}

`

Now the issue is as follow :
when we check session using sessionMap.get(sessionKey); the first time it will report that there is no such object so we will create one and after that update the session using sessionMap.put(sessionKey, container); - the issue is that this newly created object is been modified after that - so next time we do sessionMap.get(sessionKey); we will obtain deserialized version of the original object but not been populated - so when we do session.setAttribute("name", object) the object could be altered after the setAttribute that's why I had to change my configuration to use updateMode="AFTER_REQUEST" so that at the end Redis is updated with the latest changes to the object that is bound to the session. Now the readMode="MEMORY" is needed because during deserialization when we return the object in sessionMap.get(sessionKey); we are always returning the deserialized version of the object - e.g. a new object so a code like this won't work:
`
MyObject obj = MyObject(1);
session.setAttribute("test", obj);
...
obj.setId(2);

..somewhere down the flow where we do not have access to obj but to the session:
MyObject obj2 = session.getAttribute("test");

...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 :
`

<Manager className="org.redisson.tomcat.JndiRedissonSessionManager"
     readMode="MEMORY"
	 updateMode="AFTER_REQUEST"
     jndiName="session/redisson" />

`

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:
`
[redisson-netty-1-2] ERROR org.redisson.client.handler.CommandPubSubDecoder - Unable to decode data. channel: [id: 0x965d8c10, L:/127.0.0.1:52060 - R:127.0.0.1/127.0.0.1:6379] message: *3
$37
redisson:tomcat_session_updates:/XXX
$5542
XXX +org.redisson.tomcat.AttributesPutAllMessage XXX

java.io.IOException: java.lang.ClassNotFoundException: com.XXX
at org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:54)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:330)
at org.redisson.client.handler.CommandDecoder.decodeList(CommandDecoder.java:384)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:345)
at org.redisson.client.handler.CommandPubSubDecoder.decodeCommand(CommandPubSubDecoder.java:77)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:110)
at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502)
at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:656)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:591)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:508)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:470)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:909)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(Thread.java:811)
Caused by: java.lang.ClassNotFoundException: com.XXX
at java.lang.Class.forNameImpl(Native Method)
at java.lang.Class.forName(Class.java:365)
at java.io.ClassCache$FutureValue.get(ClassCache.java:177)
at java.io.ClassCache.get(ClassCache.java:148)
at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:831)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2012)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1895)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2189)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1717)
at java.io.ObjectInputStream.readObjectImpl(ObjectInputStream.java:537)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:472)
at java.util.HashMap.readObject(HashMap.java:1420)
at sun.reflect.GeneratedMethodAccessor281.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:55)
at java.lang.reflect.Method.invoke(Method.java:508)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1230)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2325)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2216)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1717)
at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2434)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2358)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2216)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1717)
at java.io.ObjectInputStream.readObjectImpl(ObjectInputStream.java:537)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:472)
at org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:50)
... 35 more
[redisson-netty-1-2] WARN io.netty.channel.DefaultChannelPipeline - An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
io.netty.handler.codec.DecoderException: java.io.IOException: java.lang.ClassNotFoundException: com.XXX
at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:421)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:656)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:591)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:508)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:470)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:909)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(Thread.java:811)
Caused by: java.io.IOException: java.lang.ClassNotFoundException: com.XXX
at org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:54)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:330)
at org.redisson.client.handler.CommandDecoder.decodeList(CommandDecoder.java:384)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:345)
at org.redisson.client.handler.CommandPubSubDecoder.decodeCommand(CommandPubSubDecoder.java:77)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:110)
at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502)
at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366)
... 28 more
Caused by: java.lang.ClassNotFoundException: com.XXX
at java.lang.Class.forNameImpl(Native Method)
at java.lang.Class.forName(Class.java:365)
at java.io.ClassCache$FutureValue.get(ClassCache.java:177)
at java.io.ClassCache.get(ClassCache.java:148)
at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:831)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2012)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1895)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2189)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1717)
at java.io.ObjectInputStream.readObjectImpl(ObjectInputStream.java:537)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:472)
at java.util.HashMap.readObject(HashMap.java:1420)
at sun.reflect.GeneratedMethodAccessor281.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:55)
at java.lang.reflect.Method.invoke(Method.java:508)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1230)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2325)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2216)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1717)
at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2434)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2358)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2216)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1717)
at java.io.ObjectInputStream.readObjectImpl(ObjectInputStream.java:537)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:472)
at org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:50)
... 35 more

`

Note that I put XXX in places to obfuscate either binary dump that happens or our class names.
The issues are with AttributesPutAllMessage and AttributeUpdateMessage classes that actually holds web application specific classes in their state - during serialization that's fine - but during deserialization this will have classloader issues as in RedissonSessionManager we do have this:
`

@OverRide
protected void startInternal() throws LifecycleException {
super.startInternal();
redisson = buildClient();

    if (updateMode == UpdateMode.AFTER_REQUEST) {
        getEngine().getPipeline().addValve(new UpdateValve(this));
    }
    
    if (readMode == ReadMode.MEMORY) {
        RTopic updatesTopic = getTopic();
        updatesTopic.addListener(AttributeMessage.class, new MessageListener<AttributeMessage>() {
            
            @Override
            public void onMessage(CharSequence channel, AttributeMessage msg) {
                try {
                    // TODO make it thread-safe
                    RedissonSession session = (RedissonSession) RedissonSessionManager.super.findSession(msg.getSessionId());
                    if (session != null && !msg.getNodeId().equals(nodeId)) {
                        if (msg instanceof AttributeRemoveMessage) {
                            session.superRemoveAttributeInternal(((AttributeRemoveMessage)msg).getName(), true);
                        }

                        if (msg instanceof AttributesClearMessage) {
                            RedissonSessionManager.super.remove(session, false);
                        }
                        
                        if (msg instanceof AttributesPutAllMessage) {
                            AttributesPutAllMessage m = (AttributesPutAllMessage) msg;
                            for (Entry<String, Object> entry : m.getAttrs().entrySet()) {
                                session.superSetAttribute(entry.getKey(), entry.getValue(), true);
                            }
                        }
                        
                        if (msg instanceof AttributeUpdateMessage) {
                            AttributeUpdateMessage m = (AttributeUpdateMessage)msg;
                            session.superSetAttribute(m.getName(), m.getValue(), true);
                        }
                    }
                } catch (IOException e) {
                    log.error("Can't handle topic message", e);
                }
            }
        });
    }
    
    setState(LifecycleState.STARTING);
}

`

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.

@jchobantonov
Copy link

@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:

`

<Manager className="org.redisson.tomcat.JndiRedissonSessionManager"
     readMode="MEMORY"
	 updateMode="AFTER_REQUEST"
     jndiName="session/redisson" />

`

There is no logs in Tomcat that shows exceptions and it is not constantly looping with those exceptions
all my changes are done to redisson-tomcat - here is my redisson-tomcat-8 modified classes:
tomcat.zip

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
Also about the readMode="REDIS" and updateMode="DEFAULT" - could we do something about that so that when during the request the application tries to get something from the session each time during the request retrieval we are returning the same object over and over again ? instead of deserializing the object each time? this is at least during the request processing time - once the request finishes we could get the attribute from redis again for the next request - what do you think ? also as for the DEFAULT updateMode - not sure if you want to actually put a track if we return non immutable object from getAttribute to extend the meaning of DEFAULT to update those objects as well as part of ending the request (not only via explicit setAttribute method)

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)

@mrniko
Copy link
Member

mrniko commented Jan 16, 2019

@jchobantonov

here is my redisson-tomcat-8 modified classes

Thank you for shared code!

I applied your changes to current version. Please check it

redisson-tomcat-8.zip

@jchobantonov
Copy link

@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:
[ERROR] /C:/dev/redisson-master/redisson/src/main/java/org/redisson/cache/LocalCacheView.java:[57,38] <anonymous org.redisson.cache.LocalCacheView$LocalKeySet$1> is not abstract and does not override abstract method remove() in java.util.Iterator
[ERROR] /C:/dev/redisson-master/redisson/src/main/java/org/redisson/cache/LocalCacheView.java:[103,38] <anonymous org.redisson.cache.LocalCacheView$LocalValues$1> is not abstract and does not override abstract method remove() in java.util.Iterator
[ERROR] /C:/dev/redisson-master/redisson/src/main/java/org/redisson/cache/LocalCacheView.java:[143,51] <anonymous org.redisson.cache.LocalCacheView$LocalEntrySet$1> is not abstract and does not override abstract method remove() in java.util.Iterator
[ERROR] /C:/dev/redisson-master/redisson/src/main/java/org/redisson/cache/LocalCacheView.java:[173,29] method remove in interface java.util.Map<K,V> cannot be applied to given types;
[ERROR] required: java.lang.Object
`

Take a look at this commit that I did in my PR that fixes those compilation errors:
d37796a

@mrniko
Copy link
Member

mrniko commented Jan 16, 2019

@jchobantonov

Fixed

@jchobantonov
Copy link

@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
[ERROR] /C:/dev/redisson-master/redisson/src/main/java/org/redisson/cache/LocalCacheView.java:[188,29] method remove in interface java.util.Map<K,V> cannot be applied to given types;
[ERROR] required: java.lang.Object
[ERROR] found: org.redisson.cache.CacheKey,org.redisson.RedissonLocalCachedMap.CacheValue
[ERROR] reason: actual and formal argument lists differ in length
`
look at my commit that fix it:
d37796a

@jchobantonov
Copy link

@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

@mrniko
Copy link
Member

mrniko commented Jan 16, 2019

@jchobantonov

Sorry, now it's fixed

@mrniko mrniko closed this as completed Jan 16, 2019
@mrniko mrniko reopened this Jan 16, 2019
@mrniko
Copy link
Member

mrniko commented Jan 16, 2019

Here is compiled version

redisson-all-2.15.1-SNAPSHOT.jar.zip

@jchobantonov
Copy link

@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:

    <Manager className="org.redisson.tomcat.JndiRedissonSessionManager"
        readMode="MEMORY"
	updateMode="AFTER_REQUEST"
        jndiName="session/redisson" />

it is now working fine and it does not throw ClassNotFoundException, but if I change it to :

    <Manager className="org.redisson.tomcat.JndiRedissonSessionManager"
        readMode="REDIS"
	updateMode="DEFAULT"
        jndiName="session/redisson" />

it is not working. With REDIS and DEFAULT it throws following exception:

[redisson-netty-1-7] ERROR org.redisson.client.handler.CommandDecoder - Unable to decode data. reply: $4469
XXX
, channel: [id: 0xb509296b, L:/127.0.0.1:59758 - R:127.0.0.1/127.0.0.1:6379], command: CommandData [promise=RedissonPromise [promise=ImmediateEventExecutor$ImmediatePromise@f1b9206a(incomplete)], command=(HGET), params=[redisson:tomcat_session:672EE4A72F43A876E899A8C4A0997222, security], codec=org.redisson.codec.CompositeCodec@3bfa792d]
java.io.IOException: java.lang.ClassNotFoundException: com.XXX
	at org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:54)
	at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:330)
	at org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:130)
	at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:110)
	at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502)
	at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366)
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:656)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:591)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:508)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:470)
	at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:909)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.lang.Thread.run(Thread.java:811)
Caused by: java.lang.ClassNotFoundException: com.XXX
	at java.lang.Class.forNameImpl(Native Method)
	at java.lang.Class.forName(Class.java:365)
	at java.io.ClassCache$FutureValue.get(ClassCache.java:177)
	at java.io.ClassCache.get(ClassCache.java:148)
	at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:831)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2012)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1895)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2189)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1717)
	at java.io.ObjectInputStream.readObjectImpl(ObjectInputStream.java:537)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:472)
	at org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:50)
	... 33 more
[redisson-netty-1-7] WARN io.netty.channel.DefaultChannelPipeline - An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
io.netty.handler.codec.DecoderException: java.io.IOException: java.lang.ClassNotFoundException: com.XXX
	at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:421)
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:656)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:591)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:508)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:470)
	at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:909)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.lang.Thread.run(Thread.java:811)
Caused by: java.io.IOException: java.lang.ClassNotFoundException: com.XXX
	at org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:54)
	at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:330)
	at org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:130)
	at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:110)
	at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502)
	at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366)
	... 28 more
Caused by: java.lang.ClassNotFoundException: com.XXX
	at java.lang.Class.forNameImpl(Native Method)
	at java.lang.Class.forName(Class.java:365)
	at java.io.ClassCache$FutureValue.get(ClassCache.java:177)
	at java.io.ClassCache.get(ClassCache.java:148)
	at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:831)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2012)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1895)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2189)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1717)
	at java.io.ObjectInputStream.readObjectImpl(ObjectInputStream.java:537)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:472)
	at org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:50)
	... 33 more
16 Jan 2019 13:33:27,859 ERROR [http-nio-8080-exec-2] (Login_jsp.java:539) - 
org.redisson.client.RedisException: Unexpected exception while processing command
	at org.redisson.command.CommandAsyncService.convertException(CommandAsyncService.java:407)
	at org.redisson.command.CommandAsyncService.get(CommandAsyncService.java:204)
	at org.redisson.RedissonObject.get(RedissonObject.java:75)
	at org.redisson.RedissonMap.get(RedissonMap.java:281)
	at org.redisson.tomcat.RedissonSession.getAttribute(RedissonSession.java:79)
	at org.apache.catalina.session.StandardSessionFacade.getAttribute(StandardSessionFacade.java:103)
	at org.apache.jsp.Login_jsp._jspService(Login_jsp.java:524)
	at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
	at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:476)
	at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:386)
	at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:330)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:610)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
	at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:650)
	at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:685)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:800)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:806)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1498)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1160)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:811)
Caused by: java.io.IOException: java.lang.ClassNotFoundException: com.XXX
	at org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:54)
	at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:330)
	at org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:130)
	at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:110)
	at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502)
	at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366)
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:656)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:591)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:508)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:470)
	at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:909)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	... 1 more
Caused by: java.lang.ClassNotFoundException: com.XXX
	at java.lang.Class.forNameImpl(Native Method)
	at java.lang.Class.forName(Class.java:365)
	at java.io.ClassCache$FutureValue.get(ClassCache.java:177)
	at java.io.ClassCache.get(ClassCache.java:148)
	at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:831)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2012)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1895)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2189)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1717)
	at java.io.ObjectInputStream.readObjectImpl(ObjectInputStream.java:537)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:472)
	at org.redisson.codec.SerializationCodec$1.decode(SerializationCodec.java:50)
	... 33 more

Did you forget to add the fix for ClassNotFoundException issue with redisson-all jar ?

@jchobantonov
Copy link

@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:

    MyObject obj = (MyObject) session.getAttribute("test");
    obj.setName("test");

    MyObject obj1 = (MyObject) session.getAttribute("test");
     
   assert(obj1.getName().equals("test")); // this should be true 

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):

    private MyObject getMyObject() {
         MyObject obj = (MyObject) session.getAttribute("obj");
         if (obj == null) {
              obj = new MyObject();
              session.setAttibute("obj", obj); // without the thread local cache which will be saved at the end ot the request this will capture a snapshot of the obj that is the newly created object - no name is set here and this is what Redis server will store
         }
    }

    .....
    //somewhere during request processing
    MyObject obj = getMyObject();
    obj.setName("test");  // here the object is modified - note that with regular session implementation of Tomcat this works without issue as it is using memory HashMap - in redission one need to put readMode="MEMORY" and updateMode="AFTER_REQUEST" in order to emulate this behavior making REDIS and DEFAULT setting useless in this scenario. Using REDIS and DEFAUTL and default options should work for all web application that will otherwise work with the regular Tomcat session implementation
    .....
    

Key points:
In RedissonSession define:

    private static ThreadLocal<Map<String, Object>> requestSessionAttributeCache = new ThreadLocal<Map<String, Object>>();

    private Object getRequestSessionAttributeCacheValue(String name) {
    	Map<String, Object> localMap = requestSessionAttributeCache.get();
    	if (localMap != null) {
    		return localMap.get(name);
    	}
    	return null;
    }
    
    private void setRequestSessionAttributeCacheValue(String name, Object value) {
    	Map<String, Object> localMap = requestSessionAttributeCache.get();
    	if (localMap != null) {
    		localMap.remove(name);
    	}
    	if (value != null) {
    		if (!(value instanceof String) && !(value instanceof Number) && !(value instanceof Boolean) && !(value instanceof Character)) { // do not cache common immutable classes
    			if (localMap == null) {
    				localMap = new HashMap<String, Object>();
    				requestSessionAttributeCache.set(localMap);
    			}
    			localMap.put(name, value);
    		}
    	}
    }

    public void saveRequestSessionAttributeCache() {
    	Map<String, Object> localMap = requestSessionAttributeCache.get();
    	if (localMap != null && !localMap.isEmpty()) { // check if we gave to the application mutable objects or the application set mutable objects
    		//only store the attributes that were provided and/or stored to the application as mutable to prevent serialization of potentially big objects that were not touched by the current http request.
    		if (updateMode != UpdateMode.AFTER_REQUEST) {//check if updateMode is not AFTER_REQUEST to not store attributes into Redis twice, as after request will update all attributes at the end of the request.
    			save(localMap);
    		}
    	}
    }

    // use static method here so that if the session gets invalidated and we can't get the id out of it in RequestSessionAttributeCacheValve we should still be able to properly clean the thread
    public static void clearRequestSessionAttributeCache() {
    	requestSessionAttributeCache.remove();
    }

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:

getEngine().getPipeline().addValve(new RequestSessionAttributeCacheValve());

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:

redisson-tomcat-src.zip

@jchobantonov
Copy link

@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:
In my opinion the request processing regarding session attributes should be like transaction

  1. as soon as request asks for the session attribute we get all attibutes out of Redis server (disadvantage is that application might not want to access all stored attributes during request processing making deserialization of all values potentially useless - or the actual deserialization could happen only when the getAttribute is invoked and the byte[] will already be retrieved in the prior call only the deserialization will happen if the application wants to use this object).
  2. if application changes object returned by getAttribute or use setAttribute / remove to write attribute store this in local request processing (use local cache and not write into Redis)
  3. once the request completes write to Redis server all changed object either via setAttribute, removeAttribute, or getAttribute via reference (as the object might have been changed by the application once retrieved)
    This way if let say application is having 2 attributes att1 and att2 and uses following logic:
   session.setAttribute("att1", "MYVALUE1");
   session.setAttribute("att2", "MYVALUE2");

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.

@mrniko
Copy link
Member

mrniko commented Jan 17, 2019

Did you forget to add the fix for ClassNotFoundException issue with redisson-all jar ?

This fix is not applied yet.

@mrniko
Copy link
Member

mrniko commented Jan 17, 2019

#1668 (comment)

Could you extract this to another issue?

@jchobantonov
Copy link

@mrniko Here #1868

@mrniko mrniko added the bug label Jan 17, 2019
mrniko pushed a commit that referenced this issue Jan 17, 2019
@mrniko
Copy link
Member

mrniko commented Jan 17, 2019

@jchobantonov Thank you!

@mrniko
Copy link
Member

mrniko commented Jan 17, 2019

Fixed! @jchobantonov thanks for your help!

@SeanRMunoz
Copy link

Here's my Cliffs Notes takeaway on how to resolve this issue in your application now that the code fixes are in place.
Thanks to everyone who had a hand in resolving this issue! 👍

// REPLACE this line...
Codec codecFails = new SerializationCodec(); 

// WITH this line...
Codec codecWorks = new SerializationCodec( Thread.currentThread().getContextClassLoader() ); 

// THEN configure as normal, and this fixes 'RedisException' caused by 'ClassNotFoundException'
config.setCodec( codecWorks )
  .useClusterServers()
  .setReadMode( ReadMode.SLAVE )
  .setSubscriptionMode( SubscriptionMode.SLAVE )
  .setClientName( redisClientName )
  .addNodeAddress( redisNodeAddress );
RedissonClient redissonClient = Redisson.create( config );

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

No branches or pull requests

4 participants