Sobre mim

Trabalho com programação há cerca de 5 anos, sempre interessado nas linguagens WEB (PHP, Ruby, Java, JS, etc). Atualmente desenvolvo em PHP com o CodeIgniter, utilizando metodologias ágeis, na Fortes Informática.

Páginas

5 June 2009 - 17:28Múltiplas conexões no CodeIgniter: Cuidado!

Não estou escrevendo aqui para explicar como se utilizar múltiplas conexões no CodeIgniter, para isso, acredito que a documentação esteja suficientemente clara (http://codeigniter.com/user_guide/database/index.html).
Vim falar dos perigos de utilizar conexão a mais de um banco de dados no CI:

Recentemente, comecei a ter problemas em um portal de grande acesso. O servidor começava a ficar cada vez mais lento, até crashar com um erro do MySQL 1040 (too many connections [Muitas conexões]). Abri o “Process List” do MySQL para investigar (Você pode acessar via phpmyadmin, outras IDEs ou direto pela query “show processlist”).

Não foi difícil perceber o problema: Existiam dezenas ou centenas de conexões com tempos cada vez maiores e com o status “Sleep”.

Sleep significa que existe uma conexão com o banco, porém o usuário (no caso minha aplicação PHP) não está mandando nem recebendo nenhum dado, simplesmente está parado esperando que algo aconteça. Se você não utiliza conexão persistente (O que era meu caso), isso pode acontecer por uma série de razões:
- Alguma de suas páginas está demorando muito para ser carregada (Ou algo no código está gerando um loop infinito).
- Algum Web Service externo está muito lento ou não está online e não foi setado um timeout adequado.
- Existem muitas conexões ao Banco, enviando ou recebendo dados, o que gera uma fila de espera para as novas conexões que ficam aguardando.

De cara, a solução mais fácil (O que infelizmente não foi meu caso), é que eu estivesse utilizando conexão persistente nas configurações do meu banco. Para checar isso, basta abrir system/application/config/database.php e verificar se a configuração “pconnect” está setada para TRUE.

Nesse ponto alguns podem estar se perguntando: Opa, mas pera aí.. Conexão persistente não é aquela que é encerrada apenas explicitamente? Pois não sendo persistente, existe essa nota, direto da documentação do PHP:
Nota: A conexão com o servidor será fechada assim que a execução do script terminar, a menos que tenha sido fechada anteriormente usando-se explicitamente mysql_close().

Pois bem, as vezes essa nota não se faz verdadeira, e com certeza foi pensando nisso que desenvolveram o seguinte código no final do arquivo system/codeigniter/CodeIgniter.php:

if (class_exists('CI_DB') AND isset($CI->db))
{
	$CI->db->close();
}

“Garantindo” assim, que no final da execução de qualquer página, a conexão com o banco seja explicitamente encerrada.

Êpa!! agora complicou mais ainda né? Minha conexão não está persistente, e ainda assim o CI fecha a conexão ao término da execução do script. Como pode existirem conexões não encerradas?

Queria saber em que arquivo essas conexões não fechadas estavam sendo abertas. Então, modifiquei a função db_connect dentro de system/database/drivers/mysql/mysql_driver.php para o seguinte:

function db_connect()
{
    if ($this->port != '')
    {
        $this->hostname .= ':'.$this->port;
    }
 
    $conexao = @mysql_connect($this->hostname, $this->username, $this->password, TRUE);
 
    $msg = "[".mysql_thread_id($conexao)."] CONEXÃO INICIADA NO ENDEREÇO: ".$_SERVER['PHP_SELF']." PARA {$this->hostname}
    ";
 
    $fp = fopen("./system/application/public/conexao.txt", "a");
    fwrite($fp, $msg);
    fclose($fp);
 
    return $conexao;
}

Com essa mosificação, comecei a gravar um log em TXT com todas as conexões abertas pelo CodeIgniter, identificando o ID de conexão com o banco, o arquivo em que essa conexão foi iniciada e para qual host a conexão apontava. Monitorando novamente o “Proccess List” e agora o log, comparei o ID das conexões que estavam com status “Sleep” a muito tempo e cheguei ao Controller onde todas as conexões não fechadas estavam sendo abertas.

Esse Controller não tinha nenhuma consulta demorada e muito menos um loop infinito. A única particularidade dele é que abria conexão a mais de um banco:

$DB2 = $this->load->database('banco2', TRUE);

O segundo parâmetro, TRUE, identifica que o objeto de conexão será retornado, ao invés de substituído no $this->db. É aí que está o problema:
O CI cria uma nova conexão, mas não a encerra, pois a conexão encerrada ao fim do script, é a carregada dentro do $this->db.

Se você leu a função mostrada a pouco, deve ter reparado num quarto parâmetro no mysql_connect:

$conexao = @mysql_connect($this->hostname, $this->username, $this->password, TRUE);

Segundo a documentação do PHP:
Se uma segunda chamada é feita a mysql_connect() com os mesmos argumentos, não é estabelecida uma nova conexão, mas ao invés, o identificador da conexão que já esta aberta é retornado. O parâmetro new_link modifica este funcionamento e faz mysql_connect() sempre abrir uma nova conexão, mesmo que mysql_connect() seja chamado antes com os mesmos parâmetros.

Em geral, classes de database iniciam e fecham a conexão dentro da função query(). Sendo assim, não é necessário deixar esse parâmetro como TRUE, pois nunca teremos mais de uma conexão simultânea, por mais que utilizemos mais de um database.

Resultado:
O PHP realmente não garante que todas as conexões sejam fechadas ao término do script;
O CodeIgniter fecha manualmente a conexão. Porém, ao gerar uma nova conexão e retorná-la como objeto, essa não será fechada ao término da execução;

Solução:
Ideal mesmo seria o CodeIgniter gravar em um array, na sua instância, o resource de todas as conexões abertas durante o script. Para então fechar uma por uma ao término da execução. Mas enquanto ele não faz isso:

Não retorne objetos de conexão: Encerre a conexão atual, destrua o objeto $this-db e retire o parâmetro “TRUE” do método load->database()).
Sendo assim, carregue um database, faça as queries necessárias e recarregue o database default, caso ainda vá executar alguma query nele.

Exemplo:

$this->db->close();
$this->db = '';
$this->load->database('banco2');
 
$x = $this->db->query("alguma query em outro banco");
 
$this->db->close();
$this->db = '';
$this->load->database('default');
 
$y = $this->db->query("alguma query no banco default");

.

Ahhh, duas observações:
Criei uma solicitação no Bug Tracker do CodeIgniter sugerindo tal mudança.
Quanto mais mexo no core do CodeIgniter, mais me apaixono por esse framework ;)

Até a próxima!

3 Comments | Tags: CodeIgniter, Database, PHP

11 March 2009 - 12:33Integrando PHPUnit ao CodeIgniter

Irei mostrar como consegui rodar os testes unitários no framework CodeIgniter com o PHPUnit, através de um plugin chamado CIUnit.

Não irei abordar o porque de utilizar técnicas de Desenvolvimento Orientado a Testes, Extreme Programming, etc, e nem detalhar o funcionamento do PHPUnit, que é muito similar ao JUnit. Caso seja seu primeiro contato com essas tecnologias, indico a leitura desse post.

Em primeiro lugar, precisamos instalar o PHPUnit, para isso basta executar na linha de comando “pear install phpunit/PHPUnit”, ou instalar manualmente. Como disse, não vou detalhar a instalação do PHPUnit, pois creio que fuja do tema. Caso queira, siga os passos de instalação mostrados aqui.

Feito isso, vamos a instalação do CIUnit:

1. faca o download da ultima versão do CIUnit, neste link, e abra o arquivo. Iremos encontrar a seguinte estrutura dentro do ZIP baixado:
fooStack/
tests/
índex.html
CodeIgniter.php

2. Copie a pasta fooStack para dentro do seu diretorio SeuProjeto/system/application/config/

3. Copie a pasta tests para dentro do seu diretório SeuProjeto/system/application/

4. Substitua o arquivo CodeIgniter.php no diretório SeuProjeto/system/codeigniter/ pelo contido no ZIP

5. Abra o arquivo SeuProjeto/system/application/config/database.php e substitua a linha que começa com:

$active_group =

por:

$env_used = 'default'; //Onde “default” deve ser o nome da sua base de produção
if(defined('CIUnit_Version')){
  $env_used .= '_test';
}
$active_group = $env_used;

Essa modificação faz com que todos os seus testes sejam executados utilizando as configurações do database de tests ao invés do de produção. Perceba que todos os dados são deletados quando executamos um teste. Sendo assim, muito cuidado pra não esquecer esse passo, ou você poderá perder dados de produção.

6. No mesmo arquivo do passo 5, crie as configurações do seu database de testes, usando o mesmo nome colocado a cima (default_test, no nosso casso). Para isso você pode simplesmente copiar as configurações do seu database de produção e modificar apenas o nome do database. Para isso, adicione essas linhas ao fim do seu arquivo:

// Configurações do dabase de testes
$db['default_test'] = $db['default'];
$db['default_test']['database'] = "projeto_test";

7. Crie o novo database com o sufixo _test (projeto_test, por exemplo) contendo a mesma estrutura do seu database de produção, essa base, como foi dito, será usada pra popular seus testes quando estes acessarem um banco de dados.

Pronto! agora para executar os testes, abra a linha de comando e digite:

  • Para executar todos os testes: phpunit AllTests.php
  • Para executar um grupo de testes (Todos os models, por exemplo): phpunit ModelAllTests.php
  • Para executar um teste especifico: phpunit nomeDoTeste.php

Lembre-se que você precisa colocar o path na pasta onde o seu teste se encontra (Para testar todos os models, por exemplo, teríamos que colocar o path em: SeuProjeto/system/application/tests/models/).

Não se esqueça de adicionar o path do seu phpunit aos paths do Windows, para isso você pode executar na linha de comando:

SET PATH=%PATH%;C:\php\pear (Onde C:\php\pear deve ser o diretório onde encontra-se o seu phpunit.bat).

No Comments | Tags: CodeIgniter, Metodologias Ágeis, PHP