Jan Luehe's Blog
How a Jruby-Based Web Application Became Unusable Following its Redeployment. Lessons Learned
How a Jruby-Based Web Application Became Unusable Following its Redeployment. Lessons Learned
- Recently, Ashish Sahni, a GlassFish developer, reported a mysterious problem: He had deployed a Jruby-based web application to GlassFish, and when he tried to redeploy it, he noticed errors of the form
- Jruby should have unregistered its cryptographic provider during undeployment, by calling
java.security.Security.removeProvider("BC"). This could have been done during the invocation oforg.jruby.webapp.AbstractRailsServlet.destroy(). In addition, Jruby should not have registered its provider with such a high priority (usingjava.security.Security.insertProviderAt()). Instead, it should have registered its provider usingjava.security.Security.addProvider(), which inserts the provider at the next available position, which normally is at the end of the list, and should have requested its provider'sMessageDigestalgorithm implementation by passing its provider's name to thejava.security.MessageDigest.getInstance()method. - The web container should have been more resilient against the case where a web application registers a cryptographic provider, but
then forgets to unregister it during undeployment. In other words, the web container should have cleaned up after the Jruby-based web application (or any other web application for that matter) during its undeployment, by unregistering any of its cryptographic providers that were left behind. This can be accomplished by the following code:
import java.security.*; Provider[] providers = Security.getProviders(); if (providers != null) { for (Provider provider : providers) { if (provider.getClass().getClassLoader() == this) { Security.removeProvider(provider.getName()); } } }which has been added to
org.apache.catalina.loader.WebappClassLoader.stop()and committed to GlassFish v2 Build 44.
java.lang.ThreadDeath in the server.log (in fact, the subsequent deployment of any web application would cause this error to be logged), and his web application had become unusable.
How could the deployment and undeployment of a Jruby-based web application affect the subsequent deployment of unrelated web applications? And worse, how could it render the Jruby-based web application unusable?
To investigate this issue, I looked at the stacktrace of the ThreadDeath error that was logged during redeployment:
java.lang.ThreadDeath
at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1325)
...
at java.lang.Class.newInstance(Class.java:303)
at java.security.Provider$Service.newInstance(Provider.java:1130)
at sun.security.jca.GetInstance.getInstance(GetInstance.java:220)
at sun.security.jca.GetInstance.getInstance(GetInstance.java:147)
at java.security.Security.getImpl(Security.java:658)
at java.security.MessageDigest.getInstance(MessageDigest.java:122)
at java.io.ObjectStreamClass.computeDefaultSUID(ObjectStreamClass.java:1731)
...
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:302)
at com.sun.enterprise.instance.SerializedDescriptorHelper.storeSerializedDescriptor(SerializedDescriptorHelper.java:239)
at com.sun.enterprise.instance.SerializedDescriptorHelper.store(SerializedDescriptorHelper.java:143)
at com.sun.enterprise.instance.SerializedDescriptorHelper.store(SerializedDescriptorHelper.java:123)
at com.sun.enterprise.instance.BaseManager.saveAppDescriptor(BaseManager.java:684)
...
at com.sun.enterprise.management.deploy.DeployThread.deploy(DeployThread.java:174)
at com.sun.enterprise.management.deploy.DeployThread.run(DeployThread.java:210)
I noticed the ThreadDeath error was thrown when the deployment code was attempting to serialize the web application's descriptor files, and in doing so, requested a MessageDigest instance for computing a serialization UID.
The serialization (and deserialization) of application descriptors has been a performance enhancement developed by the deployment team. The code catches and logs any Throwable (along with its stacktrace) that may emerge from the attempt to serialize an application descriptor, and continues. Although the next reload of an application will take longer without the serialized descriptor, an exception during serialization does not cause any functional problems, i.e., the application will still deploy fine.
The above stacktrace made me suspicious, and I instrumented the insertProviderAt(), addProvider(), and removeProvider() methods of java.security.Security to dump the stack of the calling thread. My suspicion was confirmed: It turned out that the Jruby code that was bundled with the web application registered its own provider (named BC) for cryptographic (in particular, MessageDigest) services, but never unregistered it during undeployment!
The following stack trace shows how the provider was registered by Jruby:
at java.lang.Thread.dumpStack(Thread.java:1158)
at java.security.Security.insertProviderAt(Security.java:329)
at org.jruby.RubyDigest.createDigest(RubyDigest.java:50)
at org.jruby.libraries.DigestLibrary.load(DigestLibrary.java:40)
at org.jruby.runtime.load.LoadService.smartLoad(LoadService.java:277)
at org.jruby.runtime.load.LoadService.require(LoadService.java:299)
at org.jruby.RubyDigest.createDigestSHA2(RubyDigest.java:104)
at org.jruby.libraries.DigestLibrary$SHA2.load(DigestLibrary.java:63)
at org.jruby.runtime.load.LoadService.smartLoad(LoadService.java:277)
at org.jruby.runtime.load.LoadService.require(LoadService.java:299)
at org.jruby.RubyKernel.require(RubyKernel.java:670)
at org.jruby.RubyKernelInvokerSrequire1.call(Unknown Source)
at org.jruby.runtime.callback.InvocationCallback.execute(InvocationCallback.java:49)
at org.jruby.internal.runtime.methods.FullFunctionCallbackMethod.internalCall(FullFunctionCallbackMethod.java:76)
at org.jruby.internal.runtime.methods.DynamicMethod.call(DynamicMethod.java:69)
at org.jruby.RubyObject.callMethod(RubyObject.java:487)
at org.jruby.RubyObject.callMethod(RubyObject.java:395)
at org.jruby.evaluator.EvaluationState.evalInternal(EvaluationState.java:770)
at org.jruby.evaluator.EvaluationState.evalInternal(EvaluationState.java:298)
at org.jruby.evaluator.EvaluationState.eval(EvaluationState.java:163)
at org.jruby.evaluator.EvaluationState.evalInternal(EvaluationState.java:1317)
at org.jruby.evaluator.EvaluationState.eval(EvaluationState.java:163)
at org.jruby.RubyObject.eval(RubyObject.java:563)
....
but this provider was never unregistered.
You may wonder why the serialization of application descriptors during subsequent deployments would pick up a MessageDigest algorithm implementation from the cryptographic provider that was registered by Jruby, when the Jruby-based web application had already been undeployed, and why any attempt to acquire such a MessageDigest implementation would result in a ThreadDeath error.
Here's an explanation: Jruby inserted its cryptographic provider at position 2 (using java.security.Security.insertProviderAt()), thereby preempting the default cryptographic provider for MessageDigest (and other algorithms) named sun.security.provider.Sun, which ships with Sun's JRE and which, by default, is registered in 2nd position, i.e., takes precedence over any providers registered with a higher position number (the lower the position number, the higher the priority during
cryptographic algorithm implementation lookups for which no provider name was specified). (The Java runtime's list of cryptographic service providers and their precedence order is initialized from the runtime's lib/security/java.security file resource. Any
changes to this list, which require appropriate security permissions, have VM-wide visibility.)
Now, this would not have been an issue had Jruby unregistered its provider during the undeployment of its bundling web application.
But it never did, causing the application descriptor serialization during the subsequent redeployment to acquire a MessageDigest algorithm implementation whose underlying classloader (an instance of org.apache.catalina.loader.WebappClassLoader) had been deactivated when the Jruby-based web application was undeployed. And was does a deactivated org.apache.catalina.loader.WebappClassLoader instance do when it is being asked to load a resource? It throws a ThreadDeath error!
This also explains why the Jruby-based web application was unusable following its redeployment: The Jruby code ended up getting the
same BC provider and its MessageDigest implementation that had been registered and loaded during the initial
deployment, and whose classloader had been deactivated during undeployment, so any attempt to perform any digest operations by Jruby started failing in the redeployed version of the bundling web application.
Lessons Learned:
Posted at 07:03PM Apr 27, 2007 by Jan Luehe in Sun | Comments[3]