4 de Agosto, 2011

El intrigante caso del fichero que no ocupaba espacio

Llevo meses trabajando en una implementación de OpenStack Object Storage, la cual incluye interfaces bastante atractivas como FTP o SFTP (este último lo hemos liberado en Memset). La verdad es que tiene complicación, porque trabajamos con un almacén de objetos, con lo que ofrecer un interfaz que ofrece una vista de un sistema de ficheros no es trivial en absoluto (pero esto es historia para otra anotación).

El caso es que estaba haciendo pruebas de carga a los dos servicios, que consistían en subir un fichero desde n clientes concurrentes. Los clientes que estaba implementando me ofrecían un API bastante sencillo vía un objeto fichero:

with open("fichero", "r") as fd:
    # hacemos lo que corresponda con fd, ej:
    print fd.read()

Así que solo necesitaba un fichero para las pruebas, lo cual resultaba adecuado al principio, pero era un incordio si quería trabajar con distintos tamaños. ¿Creamos ficheros temporales? Puede funcionar, pero... ¿qué pasa cuando empezamos a jugar con ficheros que son varios cientos de MB?

No es práctico, lo ideal sería un fichero que tuviera el tamaño que necesitamos, pero que no hubiera que crearlo. De hecho, lo ideal sería... que no ocupara espacio tampoco :).

from hashlib import md5

class fakefile(object):
	"""A read-only file generator."""
	def __init__(self, size=1024):
		self.size = size
		self.pt = 0
		self.closed = False
		self.md5 = md5()

	def close(self):
		self.pt = 0
		self.closed = True

	def read(self, size=9*1024):
		if self.closed:
			raise ValueError()

		if self.pt + size >= self.size:
			data = (self.size-self.pt) * '0'
			self.pt = self.size
		else:
			self.pt += size
			data = size * '0'

		self.md5.update(data)
		return data

	def checksum(self):
		return self.md5.hexdigest()

Este es el objeto mínimo para explicar el invento: proporcionamos un interfaz compatible con el de un objeto fichero.

En mi caso tuve que implementar algún método más, después de haber leído un poco de código de la librería cliente que estaba empleando, pero en general es la misma idea: simulamos un fichero, proporcionando los bytes que nos piden hasta el tamaño estipulado (solo utilizamos memoria según el tamaño del buffer de lectura, que en mi caso eran 9KB), y además calculamos el MD5 de nuestro fichero por si queremos comprobar que la operación que hicimos con él fue correcta.

Por ejemplo:

>>> from fakefile import fakefile
>>> fd = fakefile(16)
>>> fd.read()
'0000000000000000'
>>> fd.checksum()
'1e4a1b03d1b6cd8a174a826f76e009f4'

Así que ese es el MD5 de un fichero que contiene 16 veces el caracter 0, lo que nos permitirá comprobar que lo que guardamos en nuestro almacenamiento remoto es correcto.

Además no es muy complicado hacer que nuestro read proporcione bytes aleatorios, si eso es importante, e implementar más métodos si es necesario... sencillo, para ser un fichero que no ocupaba espacio ;).

Actualización: me doy cuenta que anoche me salté un paso intermedio, que es emplear StringIO:

>>> from StringIO import StringIO
>>> fd = StringIO(16*'0')
>>> fd.read()
'0000000000000000'

Es lo primero que hice, y funciona muy bien hasta que lanzas una prueba con 20 clientes concurrentes con 100MB de fichero cada uno: te quedas sin memoria. En realidad mi solución es equivalente a StringIO, pero consumiendo menos recursos.

Anotación por Juan J. Martínez, clasificada en: python, programming.

Hay 1 comentario

Gravatar

Oye, pues me ha gustado mucho la idea.. ;-)

por Antonio Pérez, en 2011-08-05 09:51:07

Los comentarios están cerrados: los comentarios se cierran automáticamente una vez pasados 30 días. Si quieres comentar algo acerca de la anotación, puedes hacerlo por e-mail.

Algunas anotaciones relacionadas: