28.2.13

El método flush() de GZIPOutputStream, no garantiza el flush de los datos

La clase java.util.zip.GZIPOutputStream permite comprimir un conjunto de bytes en el formato gzip. Esta clase hereda de java.io.FilterOutputStream, por lo que su uso es muy sencillo: envolvemos un java.io.OutputStream cualquiera con la clase mencionada, y vamos llamando a las distintas variantes de write(). Al terminar el proceso (llamando a close()), habremos escrito en el stream envuelto, los datos comprimidos.

Pero hay un detalle que no está reflejado en la documentación del JDK, y es que GZIPOutputStream rompe el contrato implícito de sus padres (al menos, en Java 6). Si llamamos al método flush() de GZIPOutputStream, no siempre forzaremos el volcado de datos. Este comportamiento no es en realidad consecuencia de una reimplementación de flush() en GZIPOutputStream. Si observamos la documentación, comprobaremos que flush() es directamente heredado de FilterOutputStream, y lo que hace es llamar al método flush() del stream subyacente.

El problema está en realidad en que la clase padre de GZIPOutputStream, DeflaterOutputStream, sobreescribe los métodos write(). Esta clase padre es la que realiza la tarea de compresión de datos, ya que GZIPOutputStream simplemente añade una cabecera y una cola. Las llamadas a las distintas variantes de write(), no siempre se traducen en llamadas a los métodos write() del stream subyacente. DeflaterOutputStream utiliza un buffer propio donde guarda los bytes resultantes de la compresión, y sólo los pasa al stream subyacente en algunos casos. Esto es debido al algoritmo de compresión, que se basa en la búsqueda de símbolos y secuencias repetidas.

Sólo una llamada a los métodos close() o finish() nos garantiza que el stream subyacente ha recibido la totalidad de los bytes. Pero al hacerlo, cerramos el stream a nuevos datos. El método finish() es exclusivo de DeflaterOutputStream y fuerza la compresión, escribiendo en el stream subyacente. Una vez hecho esto, cualquier nueva llamada a write() lanzará una excepción. El método close(), llama al propio finish() y al close() del stream subyacente.

Supongo que algunos pensaréis: «Bueno, no pasa nada. Después de todo, al terminar de usar un stream, hay que cerrarlo». Sí, pero este comportamiento no cumple el principio de sustitución de Liskov. Esto puede causar problemas (y de hecho los causa, como veremos en el siguiente post) cuando pasamos nuestro objeto GZIPOutputStream a una librería o a un framework, y no tenemos control de lo que se hace con él.

Un síntoma de este problema es encontrarnos con la desagradable sorpresa de que tras llamar varias veces a los métodos write(), al stream subyacente sólo llegan los 10 bytes de la cabecera gzip, por mucho flush() que hagamos. Si esto ocurre, tened la seguridad de que nadie está haciendo un close() del objeto GZIPOutputStream.

20.2.13

Es un dato del problema

He decidido empezar este blog con una pequeña anécdota, ya que muchas de las soluciones que veáis por aquí, lo que hacen en realidad es sortear un problema de diseño en el software, o forzar una herramienta para que pueda ser utilizada en un caso de uso nada recomendable. O puede que cosas peores, que os hagan llevar las manos a la cabeza y decir «es que aquí hay un problema de base; habría que rehacerlo todo». Bueno, bienvenidos al mundo real. En ocasiones tenemos que tratar con código heredado que nadie quiere tirar, con imposiciones técnicas por decisiones no técnicas, o simplemente con una fecha tan ajustada que sólo da tiempo a parchear algo de mala manera. Es lo que yo llamo «un dato del problema».

En realidad, el término no es creación mía sino que viene del jefe de proyecto que tuve en mi primer trabajo. Corría el año 1998, y debíamos realizar varias aplicaciones web para un nuevo operador de telefonía. A saber, una intranet para su personal, una extranet para sus distribuidores, y la web corporativa. Las aplicaciones debían correr sobre Java, pero además había una condición por parte del cliente, que nos resultó extraña: El desarrollo debía realizarse con una herramienta llamada NetDynamics, recién adquirida por Sun Microsystems. Cuando preguntamos por qué, teniendo en cuenta la filosofía de Java «write once, run anywhere», y que debería dar igual qué IDE o plataforma usáramos, nuestro jefe de proyecto nos dijo «porque es un dato del problema». Ante nuestras caras de interrogación, nos hizo recordar cómo eran los problemas de matemáticas o física que nos ponían en el colegio. A veces, había partes del enunciado que no sabías muy bien para qué te servían, pero que tenías que meterlas como sea, porque eran un dato del problema. Y si no las usabas, seguro que el profesor te lo calificaba como incorrecto. Esto era lo mismo.

Nuestra experiencia no fue agradable. Pese a que el planteamiento era bueno (no existían en aquel entonces las JSPs, y con esta plataforma podíamos escribir páginas HTML con etiquetas especiales que renderizaban el valor de parámetros de la request o la sesión), la herramienta estaba inmadura, y encontramos algún bug grave. Como además éramos unos novatos imberbes, tardábamos muchísimo en identificar los bugs de NetDynamics como tales, pensando que era culpa nuestra por no saber usar la herramienta. El desarrollo se retrasó una barbaridad, trabajamos día y noche, se crisparon los ánimos... En fin, supongo que algunos habréis pasado por situaciones similares.

A lo largo de mi carrera profesional, me he encontrado con situaciones parecidas (aunque nunca con desenlaces tan desastrosos). Decisiones inamovibles que condicionan el desarrollo del proyecto, sin posibilidad de valorar otras opciones. Lo que desde entonces he llamado, «un dato del problema».