viernes, 21 de febrero de 2014

Problema al maximizar un Player

Para los players en HTML, me he encontrado con un problema a la hora de usar el fullscreen. Este problema se me ha dado tanto en Internet Explorer como en Chrome, mientras que en Firefox no tenía ningún problema. En mi caso ha sido usando el FlowPlayer, pero puede apicarse a otros players.

El efecto del problema es que al pulsar sobre el maximizar se puede observar cómo el navegador trata de maximizarlo, en Explorer dentro del espacio que hay bajo los botones (es como funciona el fullscreen a día de hoy en Explorer), y en Chrome se puede ver la pantalla completa en negro, pero el video se sigue reproduciendo al mismo tamaño que antes de maximizar.
Es como si el video se quedara del mismo tamaño que el contenedor en el que está metido.

Pues se trata de algo obvio en realidad, el css que contiene los estilos del player, tenía un !important en el height y width del player. Esto hace que aunque el navegador pase a modo maximizar se respete dicho ancho y alto establecido en la hoja de estilos. Lo lógico es evitar siempre que sea posible el uso de !important, aunque seguro que siempre hay un buen motivo detrás de cada uno de ellos...





NEAR en MySQL

MySQL ofrece a través del MATCH AGAINST una serie de funcionalidades "avanzadas" de búsqueda, que pueden aprovecharse de los índices fulltext. Concretamente con el Boolean mode, podemos buscar cuando están o no unas determinadas palabras, otorgar pesos, truncamiento...
Pero podemos necesitar la funcionalidad NEAR que ofrecen los motores de búsquedas, como el Sphinx.

NEAR sirve para buscar registros en los que unas determinadas palabras aparezcan a determinada distancia la una de la otra, por ejemplo, para el registro:
Esta mañana el presidente del gobierno ha desayunado con el ministro de agricultura
La query con la condición presidente NEAR/6 ministro devolvería el registro indicado, mientras que NEAR/3 o NEAR/4 no lo encontrarían.
Básicamente es un método para asegurarnos de que las palabras que buscamos están cerca, y por lo tanto, que sea más probable que se refieran al mismo contexto.

Si tratamos de aplicar expresiones regulares directamente sobre los registros, y tenemos un gran volumen de datos (muchos registros y mucho texto en cada uno de ellos), veremos que la ejecución es excesivamente lenta.

La idea consiste en aprovechar los índices fulltext para filtrar rápidamente el número de posibles "candidatos", y luego aplicar la expresión regular para confirmar que las palabras se encuentran a la distancia deseada. Cuantas más palabras metamos en el proceso, la expresión regular se volverá más compleja, pero a su vez el número de registros candidatos será cada vez menor.

Además podemos usar algo del tipo NEAR/2-5, de forma que las palabras tengan que estar separadas por 2,3,4 o 5 palabras intermedias. NEAR/0-5, NEAR/5-5,...

Supongamos que tenemos una tabla NOTICIAS con (entre otros) un campo TEXTO que contiene el texto de nuestras noticias (se supone que TEXTO tiene el ínidice fulltext creado), veamos cómo quedaría la query:

SELECT * FROM NOTICIAS WHERE MATCH (TEXTO) AGAINST ('+presidente +ministro' in boolean mode) AND TEXTO REGEXP 'presidente([[:space:][:punct:]]+[^[:space:]]+){0,6}[[:space:][:punct:]]+ministro'

El + delante de las palabras en el AGAINS en boolean mode nos sirve para asegurarnos de que dichas palabras están en el registro que vamos a analizar, y será lo que nos permita tener un buen filtrado para que la query sea lo más rápida posible. No voy a detenerme mucho en explicar la expresión regular, ya que hay mucha documentación al respecto en la web.

Queda como ejercicio al lector jugar con la expresión regular para hacer el NEAR en los dos sentidos o añadir o quitar determinados caracteres... Lo importante es intentar mantenerla lo más simplificada posible, ya que la complejidad aumenta muy rápidamente.

viernes, 10 de enero de 2014

Límite en tamaño máximo de subida en PHP (upload_max_filesize)

Para hacer subidas de ficheros grandes a un servidor web Apche con PHP, es ncesario cambiar dos variables de configuración del php.ini:
- upload_max_filesize: Que establece el tamaño máximo que puede tener un fichero que se va a subir-
- max_post_size: Establece el tamaño máximo total que puede subirse en el POST. Aquí se debe incluir cualquier otra información que se suba en ese momento.
Para estas variables se puede usar la M para indicar megabytes y la G para gigabytes.
Si se establece el upload_max_filesize a 100M y el max_post_size a 210M, se podrán subir simultáneamente 2 ficheros de hasta 100M.

Pero he encontrado un problema en Windows (Server 2008 R2), con Apache 2.2.8 y PHP 5.2.4, no sé si afectará en otras versiones más modernas (pero imagino que sí).
Si se establecen unos valores a partir de 2048M o 2G para estas variables, TODAS las peticiones POST llegan al servidor vacías. Da la sensación de que se está interpretando mal el valor introducido (overflow) y se está usando un 0 como tamaño máximo del post.
Usando un máximo de 2000M para ambas varibales no tenemos dicho problema, pero estamos limitados a ficheros de casi 2GB.
Por otro lado hay que tener en cuenta que muchos navegadores no permiten sobrepasar el límite de los 2GB, por lo que en realidad no tenemos mucho más margen (si estamos trabajando con navegadores, ya que para servicios web sí podría interesarnos ir a otros tamaños).

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.