Si alguna vez has desarrollado una aplicación web en un sistema Linux y utilizando el conocido framework de PHP, cakePHP, es posible que hayas observado que, de repente, se hayan comenzado a acumular miles y miles de sesiones de usuario debido a que aquellas que actualmente están caducadas no están siendo debidamente eliminadas. Si esto perdura suficiente tiempo, ademas puede provocar que el servidor colapse. Esto es debido, en parte, a la incompatibilidad de cómo se tratan las sesiones en PHP y CakePHP.

Pero para comprender mejor el problema será mejor empezar desde el principio:

PHP cuenta con un recolector de basura (más conocido por su nombre en inglés “garbage collector” o simplemente “gc”) por defecto. Es el encargado de eliminar las sesiones expiradas, pero que es llamado de una forma poco convencional.

Siempre que un usuario comience una sesión existe una probabilidad de que el recolector de basuras se active y limpie todas aquellas sesiones caducadas en ese momento. La variable que controla esta probabilidad recibe el nombre de “session.gc_probability” y se encuentra en el php.ini, de forma que si, por ejemplo, le tenemos asignado el valor 2, siempre que un usuario comience a navegar por nuestra web, existirá una probabilidad del 2% de que nuestro recolector se active.
Sin embargo, en los sistemas Linux, el recolector de basura esta configurado de una manera diferente. El sistema por defecto sigue estando presente aunque, para evitar que este lleve a cabo su cometido, la variable session.gc_probability viene por defecto asignada a 0 (probabilidad 0 de que el recolector de basura sea lanzado). En su lugar se utiliza un cron job, una pequeña tarea que se ejecuta cada cierto tiempo, como el que podemos observar a continuación:

# /etc/cron.d/php5: crontab fragment for php5
# This purges session files older than X, where X is defined in seconds
# as the largest value of session.gc_maxlifetime from all your php.ini
# files, or 24 minutes if not defined. See /usr/lib/php5/maxlifetime
# Look for and purge old sessions every 30 minutes
09,39 * * * * root [ -x /usr/lib/php5/maxlifetime ] && [ -d /var/lib/php5 ] && find /var/lib/php5/ -depth -mindepth 1 -maxdepth 1 -type f -cmin +$(/usr/lib/php5/maxlifetime) -delete

Esta tarea, a grandes rasgos, es ejecutada cada media hora (a los 9 minutos y a los 39) y, una vez en ejecución, comprueba la existencia de la variable “maxliketime”, donde está establecido el tiempo que tienen que durar las sesiones, y obtiene su valor. Por último, accede a la dirección donde deberían estar las sesiones almacenadas por defecto y elimina todas aquellas que excedan el tiempo indicado.

El problema que presenta este método es que, a diferencia del establecido por defecto, no tiene en cuenta la posibilidad de que se haya modificado la dirección donde se almacenan las sesiones de la aplicación web (la dirección se encuentra escrita en el mismo código, no en una variable). En caso de que se haya producido dicha modificación, este nuevo método no podrá encontrar dichas sesiones y borrar las pertinentes. Además, como la probabilidad de llamar al recolector de basura con el método original es 0, no habrá ningún mecanismo que sea capaz de llamar al recolector de basura a no ser que tú mismo hayas establecido una regla por tu cuenta y esta llame al recolector cuando se cumplan las condiciones necesarias.

En caso de no estar seguros de donde se están guardando las sesiones siempre podemos consultarlo con la función “session_save_path()” que devuelve la dirección utilizada por PHP, o establece una nueva dirección si se le pasa la dirección deseada por parámetro.

La solución más sencilla a este problema es modificar el archivo php.ini para asignar a “session.gc_probability” una probabilidad mayor que 0 para que se vuelva a llamar al recolector de basuras utilizando el método por defecto. Si por alguna razón no es posible o no se quiere modificar dicho fichero también es posible volver a cambiar la ruta donde se guardan las sesiones a la establecida originalmente para que el nuevo método pueda acceder a las sesiones y lance el recolector de basura cuando sea pertinente. Para ello podemos utilizar el método del que hablamos anteriormente: “session_save_path(tu_nueva_direccion)”.

Todo esto soluciona el problema en el caso en el que queramos que PHP se haga cargo de la gestión de las sesiones pero ¿y si queremos que sea cakePHP la que las gestione y aun así sufrimos el mismo problema?

Si nos encontramos en este supuesto PHP hace una pequeña trampa que puede despistarnos: sin importar que indiquemos que cakePHP sea la que se haga cargo de las sesiones tendremos que ser cuidadosos porque, si en alguna parte del código se establece que PHP sea la que se haga cargo, esta siempre tendrá prioridad sobre cualquier otra. Así que aunque creamos que cakePHP es la que esta gestionando el borrado de sesiones es posible que estemos siendo engañados y PHP sea la que se este encargando de ello. Si estáis sufriendo el problema de que las sesiones no están siendo eliminadas correctamente merece la pena que comprobéis cuidadosamente quien está realmente a cargo de las sesiones.

Finalmente, para evitar nuevos problemas conviene recordar que la unidad de tiempo utilizadas en las variables “session.gc_maxlifetime” y “Session.timeout” (de PHP y cakePHP respectivamente) es diferente, esto es, mientras que en PHP el numero que le pasemos estará en segundos, cakePHP lo tomara como si fueran minutos Por lo que podemos encontrarnos que las sesiones tarden mucho mas o mucho menos de lo que esperábamos en un primer momento. Otro pequeño detalle que puede costarnos un día de rompernos la cabeza intentando descubrir por qué PHP ha vuelto a rebelarse contra nosotros.