Our application supports both SQL and Oracle users. To do this, there are certain things that we need present at runtime – like an Oracle Provider. But, in our case, we require a certain version or higher due to the API calls we need to make.
During testing for our upcoming release, we found a bug where if someone tried to create a connection to an Oracle database, and didn’t have the Oracle Client tools, the app would crash – at some random time. Sometimes it was right after they tried the test from our app. Sometimes it wasn’t until they closed the app. But all points showed exactly where and why it was happening – sort of.
Firing up my trusty WinDBG and connecting to our app, I saw that when it crashed, we got the following CLR exception:
(9e4.bc0): CLR exception - code e0434f4d (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00c0f524 ebx=e0434f4d ecx=00000000 edx=7c8285ec esi=00c0f5b0 edi=0016dd50
eip=77e4bee7 esp=00c0f520 ebp=00c0f574 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
KERNEL32!RaiseException+0x53:
77e4bee7 5e pop esi
0:002> !printexception
Exception object: 0bb1e114
Exception type: System.TypeInitializationException
Message: The type initializer for 'Oracle.DataAccess.Client.OracleConnection' threw an exception.
InnerException: Oracle.DataAccess.Client.OracleException, use !PrintException 0bb169cc to see more
StackTrace (generated):
StackTraceString:
HResult: 80131534
0:002> !clrstack
OS Thread Id: 0xbc0 (2)
ESP EIP
00c0f628 77e4bee7 [GCFrame: 00c0f628]
00c0fc10 77e4bee7 [PrestubMethodFrame: 00c0fc10] Oracle.DataAccess.Client.OracleConnection.Dispose(Boolean)
00c0fc20 7a572eb5 System.ComponentModel.Component.Finalize()
The interesting part of the above is that last line. The exception just before the crash is happening on the Dispose method which is being called by the Finalizer. Recall that in .NET 2.0 and higher, unhandled exceptions on the finalizer thread kill the thread, taking down the runtime with it, since there is no longer a thread to handle garbage collection finalization.
Our app was basically just doing a return new OracleConnection();
so that meant that the exception was in the Oracle API itself. And sure enough, here’s what happens in the constructor of OracleConnection (according to Reflector):
public OracleConnection()
{
if (!OracleInit.bSetDllDirectoryInvoked)
{
OracleInit.Initialize();
}
if (!OraTrace.m_RegistryRead)
{
OraTrace.GetRegistryTraceInfo();
}
if (OraTrace.m_TraceLevel != 0)
{
OraTrace.Trace(1, new string[] { " (ENTRY) OracleConnection::OracleConnection(1)\n" });
}
this.Initialize();
if (OraTrace.m_TraceLevel != 0)
{
OraTrace.Trace(1, new string[] { " (EXIT) OracleConnection::OracleConnection(1)\n" });
}
}
Peeking at the exceptions that get thrown when we try to test the connection, I can see that OpsInit.CheckVersionCompatability
throws a TypeInitializerException, which gets caught by the Initialize method, which then turns around and throws an OracleException. Which then caused the constructor to throw an exception, and the object not to get created.
But there’s something subtle there. To my app, the OracleConnection object was never created. In fact, the return value is null. But, what do we see on the Heap?
0:002> !dumpheap -type Oracle
Address MT Size
0bb153e0 084b7e10 112
0bb169cc 084b8318 76
0bb16b14 084b87c4 24
0bb16b2c 084b89a4 32
total 4 objects
Statistics:
MT Count TotalSize Class Name
084b87c4 1 24 Oracle.DataAccess.Client.OracleErrorCollection
084b89a4 1 32 Oracle.DataAccess.Client.OracleError
084b8318 1 76 Oracle.DataAccess.Client.OracleException
084b7e10 1 112 Oracle.DataAccess.Client.OracleConnection
Total 4 objects
There, on the heap, is an Oracle.DataAccess.Client.OracleConnection
. Which means that at some point, the Garbage Collector will clean it up, and in doing so, call the Dispose()
method. But the object hasn’t cleaned itself up properly, so it throws an exception in Dispose.
The problem at this point is 3-fold
- If you are going to throw an exception in your constructor, you must ensure that you’ve cleaned yourself up properly
- An unhandled exception on the Finalizer thread takes down the runtime, and because the object isn’t following number 1, this will happen
- Because the exception is in the constructor, we never get a handle to the object, and thus can’t do anything
The problem is further compounded by OracleConnection being sealed, so we can’t just subclass it. It seems like at this point we are just stuck. Or are we?
We know from Reflector that the exception happens on the call to OracleInit.Initialize()
. So perhaps we could just call that, and if it throws an exception, we know that the Oracle Client isn’t set up properly, and thus we don’t try to construct an OracleConnection object. But OracleInit is an internal class. So how do we get around that?
Reflection.
public static bool Oracle102OrHigherIsInstalled()
{
try
{
Assembly oracleAssembly = Assembly.LoadWithPartialName("Oracle.DataAccess");
if (oracleAssembly == null) { return false; }
Type oracleInit = oracleAssembly.GetType("Oracle.DataAccess.Client.OracleInit");
if (oracleInit == null) { return false; }
MethodInfo initialize = oracleInit.GetMethod("Initialize", BindingFlags.Public | BindingFlags.Static);
if (initialize == null) { return false; }
initial
ize.Invoke(null, null);
}
catch (Exception ex)
{
return false;
}
return true;
}
Internal or not, we can use reflection to get to that class. And since Initialize is a static method, we don’t even have to deal with getting an instance. We simply follow the same path that the OracleConnection constructor does, and if it fails, we know to not even try creating an instance of it.
The takeaway really is to make sure you are cleaning yourself up, and to absolutely ensure that Dispose never throws an unhandled exception. But even when you are dealing with third parties who don’t have that, you can still use tools like WinDBG, Reflector and .NET Reflection to find a workaround.
1 thought on “Exceptions in Constructors: How Reflection Helped Workaround an Oracle Bug”
Comments are closed.