2007年4月16日星期一

OTL建立连接时可能会遇到的一个bug

  最近因为工作原因,从OCICPP改为用OTL做Oracle开发。初时挺诧异的,怎么只有.h没有.cpp?看过才知道原来一个头文件就全做完了。它是对OCI的一个封装,可以用stream的方式去操作数据库。用起来还是比较简便,但是感觉封装得多了点。虽然OCI那些函数也挺复杂的,但至少觉得一切都在自己掌握之中。OTL这么一封装了之后,有一些实现细节就不得而知了,这就导致了今天发现的一个bug。

  根据OTL关于otl_connect的说明,其构造函数之一是可以直接用来连接数据库的。虽然正儿八经做这个事情的是rlogon(db_string),但是对构造函数otl_connect(connect_str, ...)的说明是其等于otl_connect(void)再加上一个rlogon(connect_str)。所以,一般就这样写了:otl_connect* pdb = new otl_connect("..."); 然后把pdb存在自己的数据库连接池中。如果连接失败,那么会抛出一个otl_exception异常。
  可是,今天却遇到意外。连接字符串中,tnsname写对了,但用户名/密码没对。于是测试的时候发现,多次连接之后,数据库那边内存爆掉了,ORACLE.EXE的线程数也多到疯掉。别的客户端全连接不上了,报ORA-00020错误。
  process数满,这倒也在情理之中。可是看了看session,发现会话其实很少。猜测是什么东西没有释放掉,不是connect就是cursor。查了一下代码,发现没有什么大问题,connect都是连接池管理,不会无限多下去的。otl_stream也都是在栈中声明,花括号之后就自动析构了,cursor也不应该是问题。郁闷中,发现正常的连接反而不会有泄漏发生,会泄漏的都是连接错误的时候。但是连tnsname都不对反而就不会了,估计是因为OCI那边根本就无法建立一个连接,也就耗不了资源。这下定位到了,就是这个创建连接的代码上有问题。

  这段创建连接的代码是这样写的。注意其中捕捉异常的部分:

otl_connect* pdb = NULL;
try
{
  pdb = new otl_connect("...");
}
catch(otl_exception& e)
{
  ……
  if (NULL != pdb)
    delete pdb;
  pdb = NULL;
}

  可以看到,如果连接不成功时没有生成对象,那么应该返回空指针,这样delete指令也不会发生。如果有对象生成,那么在异常处理代码中应该已经把这个对象给销毁了,而对象占用的资源应该也已经释放了。但事实就是不同,由于OTL在这个构造函数中封装了实现细节,而显然这个实现并不完美,于是有了泄漏。

  采用如下的代码,便不存在这个问题了:

otl_connect* pdb = NULL;
try
{
  pdb = new otl_connect();
  pdb->rlogon("...");
}
catch(otl_exception& e)
{
  ……
  if (NULL != pdb)
    delete pdb;
  pdb = NULL;
}

  根据OTL的说明,这两种建立连接的方法应该没有区别。但事实上OTL提供的范例代码中都是采用后一种方法。

  追进OTL的头文件中去看,应该可以弄明白原委。不过为了完成任务,没有那么多时间了,这个任务只好留到下次再说。大致估计了一下,应该是因为在new otl_connect(connect_str)的时候就抛了异常,于是代码跳转到了catch处,pdb根本没得到对象指针的赋值,这样新生成的对象就丢了。建议用OTL的各位,在栈中是无所谓啦,但如果要在堆中初始化一个连接,千万不要图省事想用构造函数直接一步到位哦!

后记:
  其实,如果构造函数中抛了异常,而对象中存在着自己管理的资源,那么很可能会发生资源泄漏,这对于C++程序员而言几乎可以作为一条准则了。在我后来看到《Effective C++》之后,就知道本文属于其中案例之一。我认为,OTL就不应该支持在构造函数中进行连接这种方式。你给程序员两条路可走,那每条路都肯定会有人走。如果其中一条有坑,那这条路就没必要对外开放了。

没有评论:

发表评论