Fase 3: Nodo Multitarea
Última Actualización:
05 de Abril de 2004 - Lunes
Aunque el objetivo final es implementar un esquema
distribuido, para acelerar el nacimiento de la red se ha
empezado por utilizar un esquema centralizado empleando
las facilidades proporcionadas por los servidores IRCd
de Undernet.
En estos momentos los nodos de control son:
- OLIMPO: Este es el nodo principal de la red
- ORACULO: Nodo secundario y de pruebas
Las operaciones que cubren esos nodos son:
- Control de clonos:
La red sólo permite dos conexiones
por IP
- Control de OP:
Permite dar y quitar OP en los canales
- Control de las Bases de Datos Distribuidas:
Puerta de entrada para las BDD de clonos, nicks, opers, IPs virtuales, etc.
- Gestión dinámica de módulos:
Carga y descarga de módulos como *.so y objetos Python, sin interrumpir el servicio.
- Control de canales:
Soporte para la gestión de canales.
TO DO
- El Hash es hasta el primer espacio, pero la comprobación al buscar es la cadena completa.
- Cuando se cambia el nick o los modos, no es necesario tocar las tres tablas de nicks-usuarios, nicks numéricos e IPs.
- Llamadas recursivas entre borra_usuario_compress y borra_usuario_nick.
- Simplificar el acceso a las tablas HASH indicando un único parámetro, no la pléyade Tabla, Tamaño y CANONICE, lo que es más confuso y más proclive a errores.
- Definir un pool de estructuras lista para evitar hacer el free()/malloc() de estas estructuras de tamaño fijo.
- Segmentación de los registros usando la rutina programada hace tiempo para ello.
- Posibilidad de enviar notificaciones en las conexiones de usuarios individuales y/o cibercafés.
- Todo tipo de estadísticas sobre la red: usuarios totales, usuarios por nodo, dominios, nivel de uso de las I-Lines de los cibercafés, etc., todo clasificado por día y hora.
- Log del uso de ESNET X-MODE.
- Identificación de nodos "maliciosos" en la red, especialmente en lo relativo a sus bases de datos.
- G-Lines.
- Las I-lines deberían poder validarse tanto por IP como por inversa.
- La operación más lenta de Olimpo es la grabación de las I-lines que se van conectando. Debería trabajar con un búffer de grabación.
- Pasar todas las operaciones de recorrido de base de datos a iteradores.
- Olimpo debe matar a los usuarios cuyo nick esté registrado en la base de datos, pero que no tengan el flag "+r".
- Identifica los máximos y mínimos locales de usuarios: Son los instantes que superan a todos sus vecinos durante un entorno (por ejemplo, 15 minutos antes y 15 minutos después).
- Ojo con lo que ocurre si se recupera un archivo histórico con un Olimpo con un tamaño de paso diferente de la grabación original.
- El contador de conexiones se hincha considerablemente si:
- Hay splits en la red, ya que al volver a entrar los nodos, se inyectan todos sus usuarios de nuevo y de golpe.
- Cuando Olimpo se reinicia, ya que recibe una ráfaga con todas las conexiones actuales de la red.
Hay que tener en cuenta hechos como éstos si se quiere evaluar adecuadamente los resultados estadísticos que ofrece Olimpo.
- En la visualización histórica, debería imprimirse tanto el número de conexiones como el número de usuarios de pico en cada intervalo.
- La hora cargada, en los datos históricos, debería salir en negrita, además de listar realmente la hora cargada, no la ventana deslizante de 60 minutos.
- Olimpo no implementa todas las funcionalidades de la BDD. En particular, no inicializa sus contadores ante borrados, por ejemplo.
- ¿Cómo debe hacer frente Olimpo a la llegada de alta de registros de los cuales es responsable como nodo de control?.
- Olimpo debería hacer log de todos los eventos de interés, como la propagación de registros o de SETTIME por terceras partes.
- La migración a DB tiene como principal problema la posibilidad de que se queden bloqueos en la base de datos. Entre otras cosas, eso obliga a controlar las señales que nos llegan, así como tener algún mecanismo proactivo de monitorización y resolución de bloqueos.
- Actualización de la base de datos por transacciones, para evitar su corrupción a toda costa.
- Tanto en las altas como en los borrados de registros tenemos problemas de races, entre que actualizamos nuestra base de datos y enviamos el registro a la red, y el nodo de la red almacena el registro y lo propaga al resto de nodos.
- Control de consistencia de las bases de datos remotas.
- Cuando se realiza el alta de una iline no se verifica si ya existe. No pasa nada, pero se propagaría un nuevo registro por la red.
- Dado que un usuario no se podría conectar, una iline de 0 clones no es retirada nunca por SuX, ya que no llega a ver que ha caducado porque no se conecta ningún usuario con ella. Para éste y otros casos, hay que usar el script de eliminación de I-Lines antiguas, introducido en la versión 31 de Olimpo.
- El número de conexiones simultaneas para considerar que no hay límite debe ser de 999, porque ese es el valor máximo que permite el formulario web. Además, los IRCd modernos no permiten más de ¿255? conexiones simultaneas desde la misma IP.
- Olimpo debería almacenar tanto el DNS inverso como la IP de cada conexión.
- Cuando se hace JUPE sobre un servidor, debe pasarse como P10 y no como J10. Lo malo es que sin el J10 no aparece el mensaje de "net join".
- Poder hacer JUPE sobre un nodo aunque éste no esté conectado en este momento.
- Olimpo debería detectar colisiones de nick entre módulos cargables.
- Cuando se hace un "profile", no se liberan módulos, no se cierran las bases de datos, etc. No debería ser un problema, porque el hijo muere de forma casi inmediata y no manipula las bases de datos, pero...
- Implementar dependencias entre módulos, con autocarga y autodescarga de los mismos.
- Las rutinas de impresión de estadísticas de usuarios por nodo y la rutina de gestión de SQUIT, son muy ineficientes con el cambio a numerics extendidos.
- El fichero de cabeceras "mod_berkeleydb.h" debería regenerarse automáticamente a partir de "berkeleydb.h".
- La apertura y cierre de ficheros debería realizarse dentro de una transacción.
- Cuando Olimpo hace un kill por clones, se ve la IP real, desprotegida.
- Cuando se pone un JUPE a un nodo, debería simularse la gestión de "burst", para que no nos aparezca marcado con un asterisco en las versiones recientes del IRC. El asterisco señala un nodo que está haciendo un net join, que en este caso no se completa nunca.
- Los JUPE deben ser persistentes, hasta que se retiran. No se puede depender de reinicios de Olimpo o de squit.
- En el API de módulos debe haber una función que nos indique cuándo se ha completado el net join de Olimpo con la red.
- En el API de módulos debe haber una función que nos permita especificar que solo queremos recibir determinado tipo de evento si ocurre en un net join o si no ocurre en un net join.
- Usar el Python como una librería dinámica, no estática.
- Los módulos no pueden distinguir cuando un mensaje que reciben lo ha enviado un usuario o un servidor. Tal vez habría que hacer que los servidores apareciesen con un handle negativo, o algo por el estilo.
- Las herramientas están programadas para que no intenten obtener un bloqueo si está cerrado, para evitar "deadlocks" y para no interferir con el funcionamiento del programa. Debo hacer, no obstante, que las herramientas impriman algún mensaje si se encuentran con este caso.
- Si tengo un módulo Python cuyo nombre coincide con un módulo SO, se supone que puedo elegir qué modulo cargo poniendo la extensión correcta. Pero si intento cargar el módulo Python, me va a dar un error, porque Python intentará cargar un módulo nativo, usando el SO, y el SO cargado no cumple el API Python.
- Ahora que Olimpo es capaz de soportar llamadas recursivas entre e intra módulos, debería ser posible que un módulo invocase a otro.
- Un módulo debería poder cargar y descargar otros módulos. Esto es especialmente interesante para los módulos Python.
- Verificar si un DBDEL inicializa correctamente el contador de registro interno.
- Cuando liberamos mucha memoria, debería devolverse al sistema, no reternerla. El problema es que el "dlmalloc" gestiona los bloques libres en plan FIFO. Esto es bueno para las cachés y la paginación, pero supone que las páginas no están ocupadas densamente. Si en vez de ello reutilizase la memoria en orden de dirección, sí que se podría ir liberando la última página de memoria, de vez en cuando.
- Para ocupar menos memoria, compilar la librería Python como dinámica, para compartirla con todos los programas que usen Python.
- Olimpo no debería reutilizar los "numerics" hasta que agotase la máscara, igual que hace con el IRC. Ello evitaría reutilizar los "numerics" en un plazo corto, lo que evitaría algunos desync en situaciones de lag.
- Olimpo debe poder usar más de 64 nicks simultaneos para sus módulos cargables.
- La función de API "notifica_nick_registrado()", debería dar tanto el handle como el nick en sí, como optimización, en vez de dar solo el handle.
- Definir una nueva función en el API que me dé el tiempo actual sin necesidad de que haga una llamada al sistema operativo. Se puede actualizar cada vez que se reciba algo por la red, por ejemplo. La motivación de esto es no estar haciendo "time()" en varios módulos, para los mismos datos.
- La función que se invoca cuando se descarga un módulo debería tener un parámetro que le diga si se está descargando el módulo o, por el contrario, se está cerrando todo Olimpo. Ello permite optimizar cosas como, por ejemplo, destruir threads pendientes en vez de esperar a que terminen por sus propios medios, si sabemos que se está cerrando todo el sistema y no importa dejar basura detrás.
- Cuando nos llega un mensaje para un nick que YA no existe (por ejemplo, acabamos de descargar su módulo, debemos enviar al origen un mensaje de error tipo "target left IRC".
- Potencialmente, el calcular qué nodos se van de la red cuando hay un split es O(n2).
- Deberíamos convertir las funciones "Olimpo.hace_log*()" en "Thread Safe".
- El volcado de "tracebacks" de Python debería ser "Thread Safe".
- Solucionar el problema de la eliminación automática de transacciones provenientes
de ejecuciones previas. Más información en el thread
[IRC-DEV] Cadenas de dominó curiosas.
LIMITACIONES
- Olimpo sólo puede usar un "numeric" inferior a 64, y sólo utiliza los "numerics" cortos, lo que le limita a 4096 nicks simultaneos, para todos sus nicks propios y sus módulos dinámicos.
Historia
- 28/Dic/01 Versión 57
- Amplío la caché de disco de la BerkeleyDB de los actuales 4 MB a 8 MB.
- Amplío el tamaño del buffer en memoria de logs de la BerkeleyDB, de los 32 Kbytes actuales a 384KB. Mi idea es limitar el "overflow" del buffer en presencia de transacciones muy largas o costosas, o de transacciones sin garantía de "durabilidad".
Estas transacciones ligeras se utilizan, por ejemplo, para cosas como actualizar la fecha de la última conexión de un usuario. No importa que perdamos alguna actualización de vez en cuando, si a cambio ganamos rendimiento. También se utilizan para la fecha de último uso de "I-lines". Esto es especialmente interesante cuando nos llega un "burst" de un nuevo nodo, por ejemplo.
Un buffer mayor tiene la ventaja adicional de que su volcado a disco es más eficiente.
- Guarda el record de usuarios y su fecha en la base de datos, para poder rotar los logs de acceso sin perder el valor.
- Si no había fichero de histórico de conexiones, tampoco leía el record de la base de datos. Solucionado.
- Cuando actualizamos en la base de datos el record de conexiones, lo hacemos con transacciones "ACI", en vez de "ACID". Esto mejora mucho el rendimiento cuando estamos sobrepasando el record constantemente, algo que ocurre durante varios minutos cuando se supera el record, ya que cuando se supera el record, se suele superar varias decenas veces a lo largo de una hora, por ejemplo.
No nos importa que la transacción sea "ACI", porque estamos guardando el record también en el histórico de conexiones y, además, la base de datos se actualizará definitivamente y con certeza cuando se realice un "Group Commiting" al finalizar la siguiente transacción que sea "ACID", o bien que se llene el buffer de logs de transacciones.
- Cuando se cierra una base de datos, BerkeleyDB compromete a disco los logs de transacciones. Ello tiene el efecto de que, a pesar de los cambios previos, las transacciones de nuevos records de usuarios siguen siendo "ACID", con lo que ello implica para el rendimiento.
Hago cambios en el sistema para tratar la base de datos en cuestión de forma persistente, al igual que ya se hace con la base de datos de clones ("I-lines"). Este cambio incluso simplifica código.
- 02/Ene/02 Versión 58
- Migración a Python 2.1. Eso incluye repasarse TODOS los módulos Python a la caza de cambios. También incluye el repasar posibles cambios en las estructuras en la interfaz de interconexión entre C y Python.
- Migración a Python 2.2, recién salido del horno :-).
- 04/Ene/02 Versión 59
- Debido a un bug en Python 2.2, cambio temporalmento las llamadas a "PyMapping_DelItemString" a "PyDict_DelItemString". Volveré a dejar la llamada como estaba cuando se parchee el problema en el intérprete Python.
- Cuando cambiamos el número de versión de Olimpo, el "make" procede a una recompilación casi completa de todo el código. Modifico las dependencias entre módulos y los ficheros "*.h" para que se recompile lo mínimo.
- Parcheo mi Python 2.2 para implementar "PyMapping_DelItemString", y mando el parche a sus desarrolladores. Vuelvo a dejar el código de Olimpo como estaba.
- Ampliamos el API OLIMPO con la función "nombre_modulo()", para que un módulo obtenga su propio nombre.
- Documento la función "debug()" del API OLIMPO, existente desde hace tiempo, y que nos indica si el framework de Olimpo en el que se carga el módulo está compilado en modo desarrollo o en modo producción.
- Añadidas al API "NOTIFY" las funciones "notifica_IRQ()" y "notifica_IRQ_handler()". Estas funciones se utilizan para la intercomunicación entre threads de un módulo y entre módulos distintos (previo acuerdo de un API personalizado caso a caso).
- La compilación debe realizarse, bajo Solaris, con el símbolo "_REENTRANT" definido, si vamos a utilizar threads.
- Dado que ahora hay una llamada invocable desde un thread separado, debemos proteger con un mutex:
- La carga de módulos
- La descarga de módulos
- El envío de IRQs
No es necesario proteger el listado de módulos porque no realiza cambios en las estructuras.
No es necesario proteger la instalación de un nuevo handler de IRQs porque ese proceso se realiza desde el thread principal, y su hipotética activación (del nuevo handler) también se realiza desde el thread principal, así que nunca nos lo encontraremos en estado inconsistente.
No es necesario proteger la invocación de las IRQs pendientes por el mismo punto que el anterior, y porque su flag está marcado como "volatile". Si el thread principal pasa sobre un módulo sin IRQ pendiente y, mientras se revisan el resto de módulos, llega un IRQ para elmódulo ya visto, el thread principal lo procesará en su siguiente repaso de IRQs pendientes.
- Como optimización, aparte de señalización de IRQ pendiente para cada módulo, hay una señalización global de IRQ pendiente el "algún módulo". De esta forma solo repasamos los módulos a la caza de IRQs pendientes si el flag global, que es "volatile", así lo sugiere.
- 04/Ene/02 Versión 60
- Cuando cargamos un módulo dinámico Python se puede ejecutar código previo a la invocación de la rutina "inicio()". Debemos asegurarnos de que todo lo que puedan utilizar esté ya correctamente inicializado.
En especial, durante la carga de un módulo y antes de la llamada a su rutina "inicio()", hay que marcarlo como "modulo_actual". En caso contrario, "Olimpo.nombre_modulo()", si se llama de forma implícita al cargar el módulo y antes de "inicio()", nos devolverá el nombre del último módulo ejecutado, no del módulo actual.
- No debo hacer operaciones que impliquen escrituras en los "assert()", ya que si compilamos el código con ellos deshabilitados, perdemos la operación.
- Me encuentro con un problema curioso, que identifico tras investigar durante unas horas volcados de llamadas al sistema y código fuente: cuando se integra Python en otra aplicación, como hacemos nosotros, nos encontramos el problema de que llamamos a una función Python y, cuando ésta regresa, mantiene el lock global Python, lo que bloquea otros threads.
Una vez identificado el problema, veo que está bastante documentado en Internet:
Defino macros "ENTER_PYTHON", "ENTER_PYTHON2", "EXIT_PYTHON" y "EXIT_PYTHON2", que distribuyo por el código, para lograr un threading mucho más fino y eficiente.
Los cambios que hay que hacer en el código son bastante extensos.
- Modifico los macros "ENTER_PYTHON", "ENTER_PYTHON2", "EXIT_PYTHON" y "EXIT_PYTHON2", para que permitan y gestionen adecuadamente invocaciones anidadas.
- 08/Ene/02 Versión 61
- 14/Ene/02 Versión 62
- 21/Feb/02 Versión 63
- 20/May/02 Versión 64
- Se amplía el API "SERVMSG" con la función "envia_raw2()", para poder enviar comandos directos a la red, cuyo remitente será el propio Olimpo.
Es importante recordar que dichos cambios NO serán seguidos por Olimpo. Osea, que Olimpo no los ve.
- Modifico Olimpo para que considere case insensible también los comandos "ILINEADD" e "ILINEDEL". Esto es un poco complicado, ya que tenemos entradas en la base de datos incorrectas. Nada que no se pueda resolver usando un poco de código, pero no deja de ser un coñazo...
El problema ha sido el despliegue de dos parches incompatibles en Olimpo y en formulario de alta de I-lines.
- En vez de enviar el "SETTIME" en los "net join" cuando se anuncia la presencia de un nuevo nodo en la red, se hace cuando dicho nodo completa el BURST. De esta forma no introducimos un error derivado del tiempo de transferencia del BURST.
- Con la normalización de los clones metí la pata y sale un mensaje erroneo cuando te avisa de su caducidad inminente. Solucionado.
- Solucionado un problema cuando se intentan ver los modos de un usuario cuyo "host" mide menos de dos caracteres.
- Conjuntamente con los cambios en la Base de Datos Distribuida impolementados en DB93, modifico el comando DBQ de Olimpo para que permita preguntar a la tabla "*", lo que permite verificar la integridad de todas las tablas de toda la red con un único comando.
- Soluciono, ¡por fin!, un viejo problema de Olimpo, que he conseguido diagnosticar hoy en un cuidadoso análisis de la situación. El problema se hace patente cuando se "jupea" otro nodo, y entre Olimpo y su HUB hay algo de lag o una cola de salida más o menos importante. El problema es el siguiente:
Una vez aclarado el problema, existen varias posibilidades:
- Tener una tabla con los nodos presentes en la red, y rechazar cualquier usuario nuevo de un nodo inexistente. Eso obliga a mantener y gestionar una tabla de 4096 entradas.
- Tener una tabla con los nodos "jupeados", y rechazar cualquier usuario nuevo de un nodo inexistente. Esto obliga a gestionar una tabla de tamaño dinámico, relativamente pequeña.
- Cuando se "jupea" un nodo, usar el punto anterior, pero enviando al HUB algún tipo de PING o similar. Una vez respondido el PING o similar sabemos que el HUB ya ha recibido y procesado el "jupe", por lo que ya no hace falta guardarse nada.
- El cambio anterior proporciona soporte para poder "jupear" un nodo no conectado a la red aunque, de momento, no abordamos esa posibilidad.
- Ignoramos la entrada de nuevos usuarios y nodos inyectados por nodos en situación de "jupe".
- En resumen, un nodo "jupeado" (y los nodos que cuelgan de él) son marcados de forma que su entrada de usuarios o de nodos se ignoran hasta que se reciba un "SERVER" inyectando el nodo.
- 19/Jun/02 Versión 65
- Ampliamos el API "SERVMSG" para poder escuchar cualquier comando enviado a Olimpo.
- Acomodo Olimpo para hacer frente a un par de cambios en el IRCD, como el que ahora se envíen los prefijos para "PASS" y para el primer "SERVER".
- Ampliamos el API "TOOLS" para poder convertir entre "handles" y "numerics", de nicks y de servidores.
- Solucionado un problema introducido en la ampliación del API.
- Ampliamos el API "NOTIFY" para que un módulo reciba notificaciones de entradas y salidas de usuarios de la red.
- Solucionado un problema si un módulo pide una lista de nicks registrados conectados, pero no tiene definido el "callback" asociado.
- Solucionado un problema con las notificaciones de salidas de usuarios cuando se produce un split.
- Ampliamos el API "NOTIFY" para que un módulo reciba la notificación final de que se ha completado un split.
- En uno de los últimos cambios introduje un "chanchullo" en el código, que no era muy claro pero que me simplificaba la gestión de un caso especial. Lamentablemente dicho "chanchullo" supone contar erroneamente el número de usuarios conectados cuando hay split.
- Tengo que hacerme un pequeño módulo que elimine el registro "RECORD_USUARIOS" de la tabla "db.varios", ya que su valor actual es incorrecto. Pierdo el histórico, pero es un mal menor.
- Permito que operaciones de lectura, escritura y borrado de registros de las bases de datos no necesiten estar incluidas en transacciones, necesariamente, si no lo deseamos así.
- 20/Jun/02 Versión 66
- 27/Jun/02 Versión 67
- Solucionamos un par de bugs que pueden provocar el fallo de varios "assert()" si se produce un split con el HUB de Olimpo.
- Solucionamos algunos problemas en el "JOIN 0", que puede provocar un core dump debido a que se está afectando los canales, pero no empieza por los caracteres normales de canales.
- Solucionamos un problema menor de un bot de control que pueda enviar mensajes a Olimpo y que tenga un numeric largo.
- Bajo ciertas circunstancias excepcionales, un KILL de un nick que acaba de desaparecer puede llegar a comunicarse a los módulos, de forma espuria.
- Solución de un bug introducido en una versión reciente: Cuando hay un split y se van usuarios con clones fuera y dentro del split, se eliminan más clones de los que realmente se van, de las tablas internas.
- Cuando hay un split, primero llegan las salidas de nodos, luego la salida de usuarios y finalmente la notificación del fin del split. Lamentablemente, cuando se notifican la salida de usuarios, sus nodos asociados ya han sido dados de baja, por lo que un módulo no puede pedir esa información.
Mantengo el orden, pero la baja efectiva de los nodos se realiza DESPUÉS de enviar las notificaciones de nicks. Y antes de eso se envían las notificaciones de salida de los nodos, aunque no se eliminan de las estructuras de datos internas hasta el final.
- Solucionamos un problema en la conversión de handle a numeric, y viceversa.
- Una vez solucionado el problema anterior, hay que cambiar la asignación interna de handles para los nicks introducidos por módulos Olimpo.
- 09/Sep/02 Versión 68
- 12/Dic/02 Versión 69
- 23/Abr/03 Versión 70
- 30/Abr/03 Versión 71
- 31/Jul/03 Versión 72
- Solucionado un problema cuando Olimpo usaba más de 32 nicks.
- Aprovechamos la extensión del API de Python 2.2.*, con METH_NOARGS y METH_O.
De esta forma se simplifican las rutinas y se gana rendimiento.
El primer caso es para los métodos y funciones sin parámetros. El segundo caso
es para los métodos y funciones cuyo parámetro es un (único) objeto.
- Solucionado un problema detectado tras la introducción del bot
"CHANLOG": Los mensajes enviados a canales no deben
contrastarse contra los módulos cargados, porque el destino es un canal, no un
"numeric" que podamos o debamos utilizar.
- Para evitar problemas con el "multithreading", las llamadas a funciones como
"ctime()" se hacen a la versión reentrante. Debemos cambiar tanto Olimpo como
los módulos escritos en C.
- Cambio "ctime()".
- Cambio "localtime()".
- Cambio "gmtime()".
- Cambio "printf()", "fprintf()".
- Parece que los problemas de multithreading son, en realidad, bugs en mi
versión de Solaris, tal y como documento en la página de la
agenda, entrada 1.66.2.20 del 22/Jul/03. El bug no afecta
solo a las funciones "strftime" sino en general a todas las que transforman un
"tiempo" en una cadena ASCII. Esto es así porque el problema parece residir en la gestión
del "timezone", que no es "thread safe" a pesar de lo que diga la
documentación.
Así, debo "sincronizar" entre threads las llamadas a "ctime_r()".
- Con estos cambios, nos aseguramos de inicializar el intérprete de Python muy
al principio de la ejecución de Olimpo. En particular, antes de imprimir nada,
especialmente de logs.
- Debo sincronizar también las llamadas a "localtime_r()".
- 26/Nov/03 Versión 73
- Migración a Python 2.3, recién salido del horno :-).
- Tengo que cambiar unas cuantas cosas, ya que esta versión de Python
permite compilar el intérprete como librería compartida, y quiero
aprovecharme de ese hecho.
- También debo reinstalar para el nuevo Python las librerías no estándares
usadas por los módulos Python de Olimpo. Para ver las que necesito,
uso "grep import *.py".
- Debo cambiar las llamadas "Olimpo.notify.notifica_timer()" de muchos
módulos, para que en vez de pasar un tiempo en coma flotante, lo pasen como entero.
- Actualización a BerkeleyDB 4.2.
- 26/Nov/03 Versión 74
©2001-2004
jcea@jcea.es
Más información sobre los OpenBadges
Donación BitCoin: 19niBN42ac2pqDQFx6GJZxry2JQSFvwAfS