lunes, 10 de junio de 2013

PHP. Cambio de codificación a UTF8

Migrando un sitio a UTF-8, he tenido que convertir todos los ficheros desde iso-8859-1. Para ello he usado un programa que lo hace automáticamente para un directorio especificado.
Lo curioso del tema, es que una vez hecha la migración, me he encontrado con este warning de PHP que aparentemente no parecía guardar ninguna relación:
PHP Warning:  Cannot modify header information - headers already sent
cuando intentaba hacer un session_start()

Lógicamente, toda la información que se encuentra acerca de este problema va en la línea de que se haga el session_start al principio del script y que no se devuelva ningún carácter al navegador antes.

El problema ha venido de la conversión a UTF8, el programa ha metido una marca en el fichero que se envía antes, y causa dicho error. Se trata de una Marca de Orden de bytes (conocido como BOM character), véase http://es.wikipedia.org/wiki/Marca_de_orden_de_bytes_(BOM)

La solución pasa por revisar si el programa que hemos usado permite no meter dicho BOM. Una vez quitados dichos caracteres, el problema ha desaparecido.

Por si a alguien le resulta de interés, para convertir las páginas en windows he usado el UTFCast (en el que puede configurarse que no incluya dicho BOM).
Y para quitar el caracter BOM de ficheros ya convertidos, he usado el "File BOM Detector".
Es importante señalar que editores como el notepad de windows, meten dicho carácter siempre que se guarda un fichero como utf8.

miércoles, 5 de junio de 2013

Llamadas Ajax en paralelo con PHP

Trabajando con llamadas Ajax de jQuery y con el AjaxSubmit de jquery.forms, me he encontrado con que aunque las llamadas son asíncronas, cuando se realizan muchas simultáneamente, se comporta como si se estuvieran encolando en lugar de realizarse en paralelo. Y el tiempo de respuesta de cada llamada va siendo el sumatorio de cada una de las llamadas (para procesos lentos esto es crítico).

El error ha sido pensar que había algún tipo de problema con jQuery o relacionado con javascript, porque ha resultado ser algo evidente, pero relacionado con PHP y el uso de las sesiones.
Al usar sesiones, si PHP trata de ejecutar en paralelo varios scripts que hacen uso de la misma sesión, cabe el riesgo de que se sobrescriban, por lo que cuando está usando una sesión, la bloquea y el resto de scripts deben esperar a que se libere para empezar su trabajo.

Una vez identificado el problema es tan sencillo de solucionar como liberar la sesión una vez que nuestro script no va escribir en ella con la función session_write_close().
Lo recomendable por tanto es intentar juntar lo máximo posible en nuestro script el uso de sesiones y abrirla, operar sobre ellas y desbloquearlas lo más rápido posible. O incluso desbloquearlas al principio si no vamos a escribir en ellas.

viernes, 31 de mayo de 2013

Mantener actualizados los índices del Sphinx

   Una vez instalado Sphinx, y configurados los índices, me he encontrado con un problema habitual. ¿Cómo mantener actualizada la información de dichos índices en "tiempo real" (o lo más parecido posible)?.
   Hay entornos en los que una demora de minutos u horas no tiene mayor importancia. Pero en otros, supongamos una aplicación de archivo, es imprescindible que una vez creada una entrada, ésta salga en las búsquedas.
   La primera aproximación "lógica", sería hacer que mi aplicación (PHP) usara los métodos que proporciona el API de Sphinx para ir actualizando los contenidos.
   Ya tenemos nuestro primer problema de consistencia. A largo plazo es imposible que todo sea coherente. Supongamos que mi aplicación tiene ya un volumen importante y no puedo garantizar que siempre se notifique. Y aunque se hiciera, no puedo garantizar que dicha notificación ha cumplido correctamente con su cometido. Imaginemos una aplicación en la que hay varios sistemas interactuando entre si (incluso varios servicios o demonios independientes). No puedo obligar a todos,(y confiar en que así sea) a que mantengan los índices actualizados.

   Está claro que dicha actualización debe ser transparente y asumida por un único actor. Si conseguimos trasladar todo el trabajo a la base de datos, podemos despreocuparnos, y seguir trabajando como hasta ahora.
   Resulta casi obvio, llegados a este punto, que tenemos que hacer uso de los Triggers para UPDATE, INSERT y DELETE en las tablas que contienen la información que debe estar actualizada en los índices.

   Para un entorno como el que estoy proponiendo, lo normal será querer lanzar una página php que se encargue de actualizar los índices cada vez que salte uno de éstos eventos.
Para poder lanzar dicha página, tenemos que meternos en los UDF, o funciones definidas por el usuario. MySQL permite que programemos en C o C++ funciones, y que luego hagamos uso de ellas en las querys.
   Por suerte, ya hay unas funciones hechas para lanzar aplicaciones del sistema (cuidado con los potenciales peligros que puede conllevar esto). Se trata de la función sys_exec. Está incluída en la librería lib_mysqludf_sys
Se puede descargar y compilar, pero si estamos en windows y buscamos algo más sencillo, también puede descargarse la dll aquí. Debemos copiar dicha librería en la carpeta lib/plugin/ de nuestro servidor mysql (si tenemos la configuración por defecto).
A continuación debemos ejecutar el siguiente script mysql
 
DROP FUNCTION IF EXISTS sys_exec;
CREATE FUNCTION sys_exec RETURNS STRING SONAME 'lib_mysqludf_sys.dll';


Una vez que tenemos el UDF necesario para lanzar el PHP, podemos hacer una prueba:
DELIMITER @@
CREATE TRIGGER Test_Trigger
AFTER INSERT ON mitabla
FOR EACH ROW
BEGIN
 DECLARE cmd CHAR(255);
 DECLARE result int(10);
 SET cmd=CONCAT('echo ', NEW.id, ' ', NEW.texto, ' > c:\\out.txt');
 SET result = sys_exec(cmd);
END;
@@
DELIMITER ;

Veremos que al hacer un insert en mitabla, se genera un txt en c: (windows) con el contenido de los campos id y texto de dicha tabla.

Con esta misma función podríamos lanzar el PHP con algo parecido a esto
sys_exec(CONCAT('php /var/www/test/actualizar.php ', NEW.id));
(Póngase la ruta correspondiente del ejecutable php y del script)
 

Pero aquí viene un gran problema. El trigger que lanza MSQL es síncrono, y debe ser así para poder garantizar que la operación es atómica, esto es, que nadie se me va a colar a mitad de inserción. Es importante entender esto, porque si ahora desde actualizar.php recojo el id, y trato de hacer un select sobre la tabla, no encontraré el registro (porque el insert aún no ha acabado, el registro está bloqueado, y estoy tratando de acceder desde otra instancia del cliente Mysql).

Una vez planteado el reto, estas son las soluciones:

  • La mejor opción, es usar una herramienta como gearman (hay muchas otras). Esta solución está propuesta en http://sphinxsearch.com/forum/view.html?id=9823
    Gearman básicamente es una herramienta que se encarga de realizar tareas. Se le piden las cosas y él se encarga de encolarlas e ir lanzándolas (permite además hacerlo de forma distribuida). Si alguna tarea falla se encargará de volver a intentarlo...
    Si nuestro trigger encola la tarea, podrá acabar la operación de INSERT y cuando el proceso php llegue lanzado por Gearman encontrará el registro y podrá leerlo.
    No voy a hacer incapié en ella, a pesar de ser la mejor porque ya hay mucha documentación para desarrollarlo.
  • Si buscamos algo más sencillo y no queremos entrar en instalar herramientas de este tipo, lo ideal sería pasar al PHP como argumentos todos los campos del registro y poder actualizar los índices del Sphinx sin tener que hacer un SELECT previo.
    Esto plantea algunos problemas:
    • Es incómodo pasar todos los campos (y cada uno puede ser de un tipo diferente).
    • Tenemos el INSERT esperando a que finalice la ejecución del PHP, por lo que notaremos un decremento de rendimiento de la Base de datos (habrá que evaluar el impacto en cada escenario) .
  • Pero habría una tercera solución intermedia, ni tan buena como la primera ni tan mala como la segunda, y puede ser suficiente para muchos.
    La idea consiste en que el trigger genere una entrada en una tabla "actualizaciones_pendientes". Y que después un evento de Mysql se encargue de leer las actualizaciones pendientes y vaya lanzando scripts PHP con el ID, para que vayan actualizando los índices. Veamos cómo hacerlo:

Creamos la tabla actualizaciones_pendientes:
CREATE TABLE actualizaciones_pendientes(
   id int not null auto_increment PRIMARY KEY,
   id_actualizar int not null,
   tipo_actualizacion ENUM('INSERT', 'UPDATE', 'DELETE') not null
);

Generamos el trigger, con algo parecido a esto:
DELIMITER @@
CREATE TRIGGER Insert_mitabla_Trigger
AFTER INSERT ON mitabla
FOR EACH ROW
BEGIN
 INSERT INTO actualizaciones_pendientes(id_actualizar, tipo_actualizacion) VALUES (NEW.id, 'INSERT');
END;
@@
DELIMITER ;
Y generaríamos triggers equivalentes para los UPDATES y DELETES.

Por último generaremos eventos de Mysql (es equivalente a un cron), para que vayan lanzado los PHP pendientes. Podemos hacer que el evento salte cada más o menos tiempo en función de nuestras necesidades. Lo ideal sería escoger el tiempo más alto posible que cumpla nuestras necesidades, ya que los procesos PHP que va a lanzar pueden ser algo pesados.
Para darlos de alta recomiendo visitar la documentación oficial
Pero para probar podemos ejecutar directamente (luego habrá que configurar el MySQL para que la global esté fija).
set global event_scheduler = ON;
y al hacer
show processlist;
Veremos el proceso event_scheduler lanzado.

Nos creamos un evento de este estilo (sintaxis aquí):

CREATE EVENT Actualizar_Pendiente_Event
   ON SCHEDULE EVERY 10 SECOND
   COMMENT 'Lee de actualizaciones_pendientes y lanza los PHP correspondientes'
   DO DELETE FROM actualizaciones_pendientes;

Y lanzamos un trigger al eliminar dichas entradas:
CREATE TRIGGER Eliminar_Pendientes_Trigger
BEFORE DELETE ON actualizaciones_pendientes
FOR EACH ROW
BEGIN
 DECLARE cmd CHAR(255);
 DECLARE result int(10);
 SET cmd= CONCAT('php /var/www/test/actualizar.php ', OLD.id_actualizar, ' ', OLD.tipo_actualizacion);
 SET result = sys_exec(cmd);
END;

Ya tenemos el proceso completo. De esta forma podemos trabajar directamente en nuestra aplicación, como si no tuvieramos Sphinx y este se mantendrá actualizado. Además hemos llevado el proceso "lento" a unos triggers que se lanzan en otras instancias y en background.
  • Se hace una modificación en un registro que debe actualizarse en el Sphinx
  • Salta un trigger que hace una inserción en una tabla de pendientes
  • Ciclicamente se recorre dicha tabla eliminando sus registros
  • Al eliminar dichos registros se lanza un PHP que actualiza los índices.