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

ClassCastException after plugin gets reloaded #19

Closed
guusdk opened this issue Jan 29, 2021 · 10 comments
Closed

ClassCastException after plugin gets reloaded #19

guusdk opened this issue Jan 29, 2021 · 10 comments

Comments

@guusdk
Copy link
Member

guusdk commented Jan 29, 2021

When the plugin gets reloaded (or reinstalled, upgraded), then ClassCastExceptions are very likely to occur (if the plugin has seen usage).

Exceptions relate to the class that's used to store instances of in a cache, as shown below

java.lang.ClassCastException: class org.igniterealtime.openfire.plugins.pushnotification.PushInterceptor$SentNotification cannot be cast to class org.igniterealtime.openfire.plugins.pushnotification.PushInterceptor$SentNotification (org.igniterealtime.openfire.plugins.pushnotification.PushInterceptor$SentNotification is in unnamed module of loader org.jivesoftware.openfire.container.PluginClassLoader @446907d7; org.igniterealtime.openfire.plugins.pushnotification.PushInterceptor$SentNotification is in unnamed module of loader org.jivesoftware.openfire.container.PluginClassLoader @531905e3)
at java.util.Collection.removeIf(Collection.java:576) ~[?:?]
at org.igniterealtime.openfire.plugins.pushnotification.PushInterceptor.wasPushAttemptedFor(PushInterceptor.java:263) ~[?:?]
at org.igniterealtime.openfire.plugins.pushnotification.PushInterceptor.tryPushNotification(PushInterceptor.java:144) ~[?:?]
at org.igniterealtime.openfire.plugins.pushnotification.PushInterceptor.messageStored(PushInterceptor.java:247) ~[?:?]
at org.jivesoftware.openfire.OfflineMessageStrategy.store(OfflineMessageStrategy.java:196) [xmppserver-4.6.1.jar:4.6.1]
at org.jivesoftware.openfire.OfflineMessageStrategy.storeOffline(OfflineMessageStrategy.java:140) [xmppserver-4.6.1.jar:4.6.1]
at org.jivesoftware.openfire.MessageRouter.routingFailed(MessageRouter.java:268) [xmppserver-4.6.1.jar:4.6.1]
at org.jivesoftware.openfire.spi.RoutingTableImpl.routePacket(RoutingTableImpl.java:290) [xmppserver-4.6.1.jar:4.6.1]
at org.jivesoftware.openfire.MessageRouter.route(MessageRouter.java:134) [xmppserver-4.6.1.jar:4.6.1]
at org.jivesoftware.openfire.spi.PacketRouterImpl.route(PacketRouterImpl.java:79) [xmppserver-4.6.1.jar:4.6.1]
at org.jivesoftware.openfire.net.StanzaHandler.processMessage(StanzaHandler.java:422) [xmppserver-4.6.1.jar:4.6.1]
at org.jivesoftware.openfire.net.ClientStanzaHandler.processMessage(ClientStanzaHandler.java:109) [xmppserver-4.6.1.jar:4.6.1]
at org.jivesoftware.openfire.net.StanzaHandler.process(StanzaHandler.java:246) [xmppserver-4.6.1.jar:4.6.1]
at org.jivesoftware.openfire.net.StanzaHandler.process(StanzaHandler.java:209) [xmppserver-4.6.1.jar:4.6.1]
at org.jivesoftware.openfire.nio.ConnectionHandler.messageReceived(ConnectionHandler.java:183) [xmppserver-4.6.1.jar:4.6.1]
at org.apache.mina.core.filterchain.DefaultIoFilterChain$TailFilter.messageReceived(DefaultIoFilterChain.java:1015) [mina-core-2.1.3.jar:?]
at org.apache.mina.core.filterchain.DefaultIoFilterChain.callNextMessageReceived(DefaultIoFilterChain.java:650) [mina-core-2.1.3.jar:?]
at org.apache.mina.core.filterchain.DefaultIoFilterChain.access$1300(DefaultIoFilterChain.java:49) [mina-core-2.1.3.jar:?]
at org.apache.mina.core.filterchain.DefaultIoFilterChain$EntryImpl$1.messageReceived(DefaultIoFilterChain.java:1128) [mina-core-2.1.3.jar:?]
at org.apache.mina.core.filterchain.IoFilterAdapter.messageReceived(IoFilterAdapter.java:122) [mina-core-2.1.3.jar:?]
at org.apache.mina.core.filterchain.DefaultIoFilterChain.callNextMessageReceived(DefaultIoFilterChain.java:650) [mina-core-2.1.3.jar:?]
at org.apache.mina.core.filterchain.DefaultIoFilterChain.access$1300(DefaultIoFilterChain.java:49) [mina-core-2.1.3.jar:?]
at org.apache.mina.core.filterchain.DefaultIoFilterChain$EntryImpl$1.messageReceived(DefaultIoFilterChain.java:1128) [mina-core-2.1.3.jar:?]
at org.apache.mina.filter.codec.ProtocolCodecFilter$ProtocolDecoderOutputImpl.flush(ProtocolCodecFilter.java:413) [mina-core-2.1.3.jar:?]
at org.apache.mina.filter.codec.ProtocolCodecFilter.messageReceived(ProtocolCodecFilter.java:257) [mina-core-2.1.3.jar:?]
at org.apache.mina.core.filterchain.DefaultIoFilterChain.callNextMessageReceived(DefaultIoFilterChain.java:650) [mina-core-2.1.3.jar:?]
at org.apache.mina.core.filterchain.DefaultIoFilterChain.access$1300(DefaultIoFilterChain.java:49) [mina-core-2.1.3.jar:?]
at org.apache.mina.core.filterchain.DefaultIoFilterChain$EntryImpl$1.messageReceived(DefaultIoFilterChain.java:1128) [mina-core-2.1.3.jar:?]
at org.apache.mina.core.filterchain.IoFilterEvent.fire(IoFilterEvent.java:106) [mina-core-2.1.3.jar:?]
at org.apache.mina.core.session.IoEvent.run(IoEvent.java:89) [mina-core-2.1.3.jar:?]
at org.apache.mina.filter.executor.OrderedThreadPoolExecutor$Worker.runTask(OrderedThreadPoolExecutor.java:766) [mina-core-2.1.3.jar:?]
at org.apache.mina.filter.executor.OrderedThreadPoolExecutor$Worker.runTasks(OrderedThreadPoolExecutor.java:758) [mina-core-2.1.3.jar:?]
at org.apache.mina.filter.executor.OrderedThreadPoolExecutor$Worker.run(OrderedThreadPoolExecutor.java:697) [mina-core-2.1.3.jar:?]
at java.lang.Thread.run(Thread.java:832) [?:?]
@guusdk
Copy link
Member Author

guusdk commented Jan 29, 2021

The problem is caused by storing instances of a class provided by this plugin in a cache.

Whenever such an instance gets stored in a cache, its class is being references by that cache. When the plugin gets reloaded/updated, the PluginClassLoader that loaded the class gets replaced. When the newly loaded plugin tries to interact with the cache (that still has a reference to 'old' data), instances from the same class, but loaded by both the old as well as new PluginClassLoader, are being used, causing class cast exceptions.

Note that I believe this problem to apply to all Openfire plugins that store instances of classes that they provide in a cache. I don't think that this problem is specific to Hazelcast either: it also applies to caches in a non-clustered setup.

A workaround for this problem is: restart Openfire whenever the plugin gets updated/reloaded.

@guusdk
Copy link
Member Author

guusdk commented Jan 29, 2021

The only reliable way to prevent this issue that I can think of is to prevent using classes provided by anything but the Openfire classloader. In other words, do not add instances of classes provided by the plugin into a cache.

@guusdk
Copy link
Member Author

guusdk commented Jan 29, 2021

@GregDThomas what's your take on this?

guusdk added a commit to guusdk/openfire-pushnotification-plugin that referenced this issue Jan 29, 2021
…updating the plugin.

This commit replaces the usage of classes provided by the plugin itself during cache usage. ClassCastExceptions
can occur when a cache has a reference to a class that was loaded by a PluginClassLoader from a previous incarnation
of this plugin.

See igniterealtime#19 for more details.
@GregDThomas
Copy link

Whenever such an instance gets stored in a cache, its class is being references by that cache.

That behaviour does seem odd, but certainly explains this problem (and possibly others). I've got around similar problems by caching a String - the JSON representation of the object I wanted to cache, using Jackson (https://www.baeldung.com/jackson-object-mapper-tutorial if you're not already familiar with it). Looking at your fix, you've got two caches, one for each field. That's probably fine, but clearly becomes more of a problem if the number of fields grows.

I'd be very tempted to start with a JSON cache now, tbh.

@GregDThomas
Copy link

(on reflection, I'd be very tempted to write a couple of default methods to the Cache class,

default void putAsJsonString(final K key, final V value) {
    put(objectMapper.writeValueAsString(key), objectMapper.writeValueAsString(value));
}

default V getFromJsonString(final K key, final Class<V> clazz) {
    return get(objectMapper.readValue(get(key), clazz);
}

@guusdk
Copy link
Member Author

guusdk commented Jan 29, 2021

Why JSON, and not, for example, XML - which is a lot closer to XMPP, and possibly more type-safe? It might even we worth considering using Java native / object serialization, for complete type safety. All classes for which instances are stored in a cache must be Serializable anyway. We might loose 'human readability' (which might be restored by having a smart utility method, but is limited to the admin console anyway), but it'll be a lot more efficient.

I was also thinking that it might be a good idea to somehow log a warning, when a Cache is instantiated or used with instances of classes that are loaded by a PluginClassLoader. That might help identify potential affected code.

@GregDThomas
Copy link

Why JSON? At the time I was doing it, I was working in JSON a lot (carried over XMPP!) But yes - XML or standard serialization would also do it. Though a textual format may help with the display on the admin screen. And yes, the warning sounds like a good idea.

@guusdk
Copy link
Member Author

guusdk commented Feb 3, 2021

Although I think you're right in that we can improve on this significantly, I'm going to merge my fix that simply prevents using any plugin-provided class for now. It's cumbersome and adds code complexity, but works. We should replace this when we've got the infrastructure in place that will 'automagically' does some kind of serialization (always? only for plugins?), but for now, I'd like to release something that no longer has the problem.

@guusdk
Copy link
Member Author

guusdk commented Nov 18, 2021

I believe that I have provided a suitable implementation in igniterealtime/Openfire#1926

@GregDThomas, your suggested default implementation (quoted below) didn't work, as the objectmapped keys and values are Strings (obviously). That does not match with the generics definition of the Cache (K and V). Instead, I've opted for a model that uses delegation to do much of the same.

default void putAsJsonString(final K key, final V value) {
put(objectMapper.writeValueAsString(key), objectMapper.writeValueAsString(value));
}

default V getFromJsonString(final K key, final Class clazz) {
return get(objectMapper.readValue(get(key), clazz);
}

@GregDThomas
Copy link

That's fine; the above code was written off the top of my head, the implementation itself looks good.

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

No branches or pull requests

2 participants