27 mar 2011

No uses variables globales

Este post explica por qué mejor no usar variables globales en entornos Web y está dirigido a personas que aún no lo ven tan claro. Para este ejemplo emplearé Apache, mod_python/mod_wsgi, Django y Memcached pero cualquier otro entorno Web también sirve.

Apache


Apache tiene 2 modos de funcionamiento:
1. Prefork: Mediante procesos
2. Worker: Mediante hebras y procesos
Más frecuentemente se suele usar el modo 1 porque es más seguro ya que no todas las librerías usadas están escritos teniendo en cuenta que pueden ser empleadas por varias hebras a la vez. Se dice que no son thread-safe.

En las variantes de Unix (Linux, FreeBSD, NetBSD, Mac OS X, Unix, Solaris, etc.), el modo 1 se basa en una llamada al sistema operativo fork que divide un proceso en dos, uno considerado "padre" y otro "hijo". Ambos procesos tienen una copia idéntica de todos los datos. Pero una vez divididos, los cambios en memoria realizados por un proceso ya no afectan al otro.

En caso de Apache, existe un proceso maestro responsable de crear los hijos, controlar el número de procesos disponibles, etc. Los procesos hijos son los que atienden a las peticiones. En caso de los intérpretes de lenguajes o en general cualquier módulo, se carga después de realizar el fork.

mod_python y mod_wsgi


mod_python y mod_wsgi permiten el uso de más de un intérprete si Apache está sirviendo varias aplicaciones Web y pueda haber interferencias entre ellas. Si, por ejemplo, usamos dos intérpretes y tenemos diez procesos de Apache, podemos tener hasta 2 x 10 = 20 intérpretes de Python activos a la vez. Al mostrar los procesos con ps o top no aparecen como Python sino como Apache porque se ha invocado el intérprete desde una llamada a una librería. Por tanto, se ejecuta en el espacio de memoria y con los permisos del proceso Apache.

Por otra parte, todo lo que un proceso almacena en variables globales, por ejemplo datos de una base de datos, datos pre-calculados o páginas web completas, no puede ser aprovechado por otro proceso. Supongamos que haya que cargar un conjunto de datos de tamaño considerable y lo almacenamos en variables globales. En caso de Python, por ejemplo, podría ser un diccionario asignado a una variable con ámbito de módulo durante el inicio de la aplicación o cuando el usuario realice una determinada petición. Cada proceso tendría entonces que seguir los mismos pasos duplicando así los datos en memoria y realizando las mismas consultas a la base de datos. Esto no es demasiado óptimo y hará que cada proceso de Apache ocupe mucha memoria.

¿Qué podemos hacer al respecto?

Memcached


La solución es usar una caché compartida entre los procesos e incluso entre varias máquinas. Siempre que queramos aprovechar un dato elaborado, tal como los resultados de una consulta de base de datos, una página completa, un cálculo estadístico, etc. lo almacenamos en la caché compartida para que cualquiera de los procesos Apache pueda aprovecharlo.

Memcached es una caché de este tipo. Básicamente, se trata de un software que permite almacenar y recuperar conjuntos de datos desde cualquier ubicación de nuestra red.

La lógica podría ser siempre la misma:
1. Intentar obtener el valor requerido desde la caché
2. En caso de no existir, lo calculamos y lo almacenamos en caché.
3. Hacer lo oportuno con el valor.

Un fragmento Python podría ser:

value = cache.get(key)
if value == None:
# calc value
value = do_calc_value_here()
cache.set(key, value)
# do something with value
render_template(template, value)


Todos los sistemas de caché emplean una clave para almacenar y poder recuperar con posterioridad el valor. A la hora de establecer la clave es importante indicar si el valor cacheado puede compartirse entre todos los usuarios o no. Por otra parte, es importante que se actualice la caché cuando algún usuario provoque un cambio, es decir, que la caché no ofrezca valores inconsistentes.

Django & Memcached


Tomemos como ejemplo Django. Podemos almacenar un valor recuperado de la base de datos siempre que la petición de otra persona no lo actualice ni que exista otra aplicación que actualice los mismos datos directamente sobre la base de datos. Para no tener que lidiar con diferentes APIs de los sistemas de caché, Django ofrece una abstracción:

from django.core.cache import cache

cache.set(clave, valor, tiempo) # almacenar un valor en cache
valor = cache.get(clave) # recuperar un valor de cache

Para facilitar el uso con página completas, puede emplearse el decorador cache_page:

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def my_view(request):
...

Esto almacena la página generada durante 900 segundos y emplea la versión cacheada si está disponible. La función cache_page hace toda la mágica.

Supongamos, sin embargo que la página dependa de la persona que realiza la consulta:

from django.core.cache import cache

def my_view(request):
key = "my_view" + request.user.username
page = cache.get(key)
if not page:
page = render....
cache.set(key, page, 60 * 15)
return page

El valor a almacenar en caché ha de ser "persistible". El cliente de memcached para python (OJO: existen varios clientes en la actualidad: python-memcachedmemcached, python-libmemcached y pylibmc usa pickle, siempre que no se trate de una cadena de caracteres. Esto significa que no podemos cachear objetos tipo conexión a base de datos, objeto sesión, etc.

Es importante dimensionar el tamaño de RAM asignado a memcached, así como el número de posibles conexiones. Los valores por defecto son 64 MBytes (parámetro -m) y 1024 (parámetro -m) respectivamente. Mire la ayuda de memcached para ver cómo modificar estos parámetros (memcached -h).

Django ofrece incluso la posibilidad de cachear trozos de una plantilla:

{% load cache %}
{% cache 500 topmenu %}
.. topmenu ..
{% endcache %}

En caso que el menú dependa del usuario podría usarse el siguiente fragmento:

{% load cache %}
{% cache 500 topmenu request.user.username %}
.. topmenu ..
{% endcache %}

Es decir, todos los parámetros a partir del segundo de la etiqueta de plantilla "cache" son usados para formar la clave de caché.

Finalmente, pueden almacenarse también las sesiones en memcached. Para ello indicamos lo siguiente en settings.py:

SESSION_ENGINE = "django.contrib.sessions.backends.cache"

Si deseamos que las sesiones sobrevivan el reinicio de memcached a costa de un muy pequeña reducción de rendimiento debemos usar siguiente línea:

SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"

Los detalles sobre el uso de caché en Django los podemos encontrar en la Web de Django.

Conclusión


En vez de usar variables globales podemos emplear una caché compartida para ahorrar recursos y aumentar la escalabilidad. El uso de la API de caché de Django es extremadamente simple.

18 mar 2011

Alfresco Export Tool


I recently found the problem that Alfresco doesn't provide a shell script to export repository contents anymore. So I rewrote one of them found on Alfresco forums. One of the problems that I encountered is that it tries to start the VTI server module (if installed), colliding with the running Alfresco instance. Setting port to 0 disabled it.



See my post to Alfresco forums.

16 feb 2011

oodiff reloaded

Although there exists alread oodiff, it's a bit limited. What's about any format or image changes not reflected in the diff?

I had a oodiff before, but some day it stopped working. Now I renamed it to ooodiff.
Thanks to some notes from here I corrected it. This version should work well with subversion. Here's the code (PEP8/Pyflakes compatible):


#!/usr/bin/python
# Requires python-uno
# Code originally gotten from:
# http://people.warp.es/~xtor/blog/?p=166
# See also: (google for "openoffice uno compare documents")
# http://win32com.goermezer.de/content/view/193/274/
# http://nxsy.org/blog/447.html
# http://www-verimag.imag.fr/~moy/opendocument/


PORT = 2526
OOFFICE = 'ooffice'

# General imports
import filecmp
import os
import shutil
import sys
import time

# pyuno imports
import uno
from com.sun.star.connection import NoConnectException


class OOUnoConnectionError(Exception):
pass


def getRevision(filename, revision, save_as):
"""Export a revision of a document."""
os.system('svn export -r%s "%s" "%s"' % (revision, filename, save_as))
return save_as


def compare(filename, oldrev='', newrev='', force=False):
"""Compare two revisions of a document."""
if not oldrev:
oldrev = 'BASE'
newfilename = '%s%s%s%s' % (filename, '.' + oldrev,
'.' + (newrev or 'workingcopy'), '.diff.tmp')
if not newrev:
shutil.copyfile(filename, newfilename)
else:
getRevision(filename, newrev, newfilename)
oldfilename = '%s%s%s' % (filename, '.' + oldrev, '.tmp')
getRevision(filename, oldrev, oldfilename)
if not force and filecmp.cmp(newfilename, oldfilename):
print ("The files seem to be the same. "
"There are no changes. Use --force run the diff in OO.")
else:
oo = OO()
doc = oo.open(newfilename)
oo.compareCurrentDocument(doc, oldfilename)


class OO:
"""A small class to abstract OpenOffice application."""
def __init__(self, filename=''):
# start OO
print "Starting OO"
os.system("soffice -nodefault "
"'-accept=socket,host=localhost,port=%s;urp;'" % PORT)

# Get the uno component context from the PyUNO runtime
localctx = uno.getComponentContext()
# Create the UnoUrlResolver on the Python side.
resolver = localctx.ServiceManager.createInstanceWithContext(
"com.sun.star.bridge.UnoUrlResolver", localctx)

cnxstr = ("uno:socket,host=localhost,port=%s;urp;"
"StarOffice.ComponentContext")
cnxstr = cnxstr % PORT
# try to connect to OO
ctx = None
retries = 5
while retries:
retries -= 1
try:
ctx = resolver.resolve(cnxstr)
except NoConnectException:
time.sleep(1)
if not ctx:
raise OOUnoConnectionError("Can't connect to OpenOffice")

# Get the ServiceManager object
smgr = ctx.ServiceManager

# Create the Desktop instance
desktop = smgr.createInstance("com.sun.star.frame.Desktop")
# save objects
self.desktop = desktop
self.smgr = smgr
self.ctx = ctx
if filename:
self.open(filename)

def open(self, filename, **kwargs):
"""Open a file."""
print "Opening: %s" % filename
properties = []
for key, value in kwargs.items():
properties.append(self.getProperty(key, value))
properties = tuple(properties)
doc = self.desktop.loadComponentFromURL(
self.convertToURL(filename), "_blank", 0, properties)
return doc

def compareCurrentDocument(self, doc, filename):
"""Compare the current document to the specified one."""
print "Comparing to: %s" % filename

# Sometimes, after opening OO or loading a doc
# we've still not access to the dispatcher or current frame
# so we wait here a bit.
# Get the dispatcher
dispatcher = self.smgr.createInstance("com.sun.star.frame."
"DispatchHelper")

# Show tracked changes and compare documents
frame = doc.getCurrentController().getFrame()
property = self.getProperty('URL', self.convertToURL(filename))
dispatcher.executeDispatch(frame, ".uno:CompareDocuments", "", 0,
(property,))
property = self.getProperty("ShowTrackedChanges", True)
dispatcher.executeDispatch(frame, ".uno:ShowTrackedChanges", "", 0,
(property,))

def getProperty(name, value):
"""Read a OO property."""
prop = uno.createUnoStruct("com.sun.star.beans.PropertyValue")
prop.Name, prop.Value = name, value
return prop
getProperty = staticmethod(getProperty)

def convertToURL(filename):
"""Convert a local filename to URL required by OO."""
return uno.systemPathToFileUrl(os.path.abspath(filename))
convertToURL = staticmethod(convertToURL)


if __name__ == '__main__':
oldrev = newrev = ''
force = '--force' in sys.argv
filename = ''
for arg in sys.argv[1:]:
if arg.startswith('-r'):
if not ':' in arg:
arg += ':'
oldrev, newrev = arg[2:].split(':')
elif not arg.startswith('-'):
filename = arg
if filename:
compare(filename, oldrev, newrev, force)
else:
print """
Usage: oodiff [--force] [-r[:]]
: the file to comparte
--force: force comparison, although files seem the same
-r : same as subversion. If not specified, -rPREV assumed.

This command:
- may not work if OpenOffice.org is already running (because
it has to start with a listening port for UNO to work).

- will create some temporary files (*.tmp). You have delete them
manually when you are finished.
"""

21 sept 2010

Tomcat 6: Session replication for failover

Summary: there is a bug in tomcat 6.0.20 which inhibits tomcat to send multicasts between instances, failing to form the cluster and hence not replicate sessions.

For the current project, I have to cluster Alfresco 3.2r Enterprise. We have a mini 2-node cluster. Although hibernate L2 cache replication works correctly (you have to rename ehcache-custom.xml.sample.cluster, which was not totally clear after reading the documentation), I went for session replication, which the docs state as supported. Now, only the session replication was missing. (There seems to be a bug which makes session replication fail, but I had no time to verify it. Anyway, I wanted to go ahead and learn how to configure tomcat for session replication and fail-over.)

As I had not much idea of configuring tomcat, I picked up an existing tomcat 6.0.20 instance and a small session example. The I configured tomcat, following the session replication / cluster how-to. Finally, I copied the tomcat instance and changed any colliding ports.

But I was not able to make it work. I was looking into the log for any message about my instances following the cluster, but without luck. After trying other ports, reconfiguring the network to support multicast ping (icmp), googleing around, reading a lot of docs, etc. I found a email message (which I can't find anymore), suggesting that there is a bug in tomcat-6.0.20 not sending multicasts for cluster instance detection!

I downloaded immediately a new version (6.0.29) and configured the two instances. It worked at the first attempt.

I use Apache proxy_balancer to test the instances. Here goes my Apache config file:
<Location /balancer-manager>
SetHandler balancer-manager
</Location>

<Proxy balancer://ajpCluster>
BalancerMember ajp://localhost:8809 route=jvm1
BalancerMember ajp://localhost:8810 route=jvm2
</Proxy>

<Location /sessiontest>
ProxyPass balancer://ajpCluster/sessiontest stickysession=JSESSIONID nofailover=off
</Location>

<Location /favicon.ico>
ProxyPass balancer://ajpCluster/favicon.ico
</Location>

The "route" parameter of BalanceMember adds just its value to the session id. The /balancer-manager url helps you to debug the cluster, displaying if both instances accept requests, how may have been processed, and to enable or disable any instances. As we can see here, my tomcat instances are listening for AJP requests on ports 8809 and 8810.

Here goes the interesting part of my conf/server.xml of both (the have just different ports):
<Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">

<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
channelSendOptions="8">
<Manager className="org.apache.catalina.ha.session.DeltaManager"
expireSessionsOnShutdown="false"
notifyListenersOnReplication="true"/>

<Channel className="org.apache.catalina.tribes.group.GroupChannel">

<Membership className="org.apache.catalina.tribes.membership.McastService"
address="228.0.0.4"
ttl="15"
port="45564"
frequency="500"
dropTime="3000" />

<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="auto"
port="4200"
autoBind="100"
selectorTimeout="5000"
maxThreads="6" />

<Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
<Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
</Sender>

<Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>

<Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>

</Channel>
<Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
filter=".*\.gif;.*\.js;.*\.jpg;.*\.htm;.*\.html;.*\.txt;" />

<Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
tempDir="/tmp/war-temp/"
deployDir="/tmp/war-deploy/"
watchDir="/tmp/war-listen/"
watchEnabled="false" />

<ClusterListener className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener"/>

<ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
</Cluster>
...
</Engine>


Take into account that you just have to change all enabled connector (HTTP, HTTPS, AJP, SHUTDOWN) ports and the cluster message Receiver port (4200, in the example above) for the tomcat instances which work in the machine.

13 dic 2009

Install Oracle PDO PHP (php_oci) driver in Ubuntu Hardy (8.04)

Ubuntu Hardy comes with some PHP PDO driver like mysql, postgresql and sqlite. Trying to install Oracle driver with:
sudo pecl install php_oci

fails in my Ubuntu with the following error:
pear/PDO_OCI requires PHP extension "pdo" (version  >= 1.0)

Isn't "pdo" installed already? It is part of php5-common, but PEAR/PECL doesn't know about it. So let's go ahead with ignoring dependency checks:
sudo pecl install -n php_oci

If you haven't installed php5-dev you'll get the following error:
sh: phpize: not found
ERROR: `phpize` failed

So install php5-dev, if you didn't already do it. Now, running the former pecl could yield:
...configure: error:
You need to tell me where to find oracle SDK, or set ORACLE_HOME.

Isn't ORACLE_HOME already set? Running a
echo $ORACLE_HOME
should answer this doubt. In my environment, it's set, but sudo doesn't pass it to the command, let's try again:
sudo -E pecl install -n pdo_oci

configure: error: Cannot find php_pdo_driver.h

The configure script tries to find it in:
/usr/include/php/....

This directory tree is empty or non-existent (in my Ubuntu).
cd /usr/include
sudo rm -rf php
sudo ln -s /usr/include/php5 php


Now the install succeeds. We have to enable it in /etc/php5/conf.d. Create a file 'pdo_oci.ini' with the following content:
#config por PDO OCI Oracle
extension=pdo_oci.so


Let's test is with php interactive mode:

$ php -a
php > $dbh = new PDO('oci:dbname=xe', 'system', '<password>');
php > $sql = 'select * from dual';
php > foreach ($dbh->query($sql) as $row){
php { print_r($row);
php { }
Array
(
[DUMMY] => X
[0] => X
)
php > [Control-D]


If we create now a simple php page which tries to show some Oracle output we get a PHP error message (depending on how error reporting is configured in PHP, it may not be rendered in PHP page, but be present in the log):
PDOException: SQLSTATE[]: pdo_oci_handle_factory: OCI_INVALID_HANDLE (/tmp/pear/cache/PDO_OCI-1.0/oci_driver.c:463) in ... on line ...

The problem is that Apache has no access to the ORACLE_HOME environment variable. Just add it to /etc/apache2/envvars:
export ORACLE_HOME=<path to Oracle, mine is: /usr/lib/oracle/xe/app/oracle/product/10.2.0/server>


Now we should be done.

30 oct 2009

OIOSAML and Blackboard / WebCT Vista/CE 8.0

Summary


When installing OIOSAML, incompatibility issues arise. This blog entry provides detailed information about how to fix this problem and may be applicable to other applications that need to run with Sun Java 1.5 and updated versions of JAXP 1.3.

Introduction


Blackboard Vista / CE 8.0, formerly known as WebCT, uses Bea Weblogic 9.2 application server, including Sun Java 1.5. OIOSAML requires and provides an updated version of JAXP 1.3, which is part of Java 1.5 core library. To override internal libs in Java 1.5, you have to "endorse" the new libraries. This is correctly described in OIOSAML docs.

Symptoms


When starting the Weblogic server, the following error appears:
weblogic.management.ManagementException: [Management:141266]Parsing Failure in config.xml:
javax.xml.namespace.QName; local class incompatible:
stream classdesc serialVersionUID = 4418622981026545151, local class serialVersionUID = -9120448754896609940


How to fix the problem


You have to include the following option in JAVA_OPTIONS ($webct_domain_dir/customconfig/startup.properties):
-Dorg.apache.xml.namespace.QName.useCompatibleSerialVersionUID=1.0


The startup.properties already contains:
-Dcom.sun.xml.namespace.QName.useCompatibleSerialVersionUID=1.0


But the provided xerces library (2.9.1) has another property name as the one provided with Sun Java 1.5.

Detailed description


When Sun included JAXP 1.3 in Java 1.5, it changed the namespace in all source files. It also specified an explicit serialVersionUID for javax.xml.namespace.QName. In Java 1.5, the serialVersionUID was explicitly specified as 4418622981026545151 (0x3d521a30bc76fdff). But in previous versions of JAXP (xerces) this was implicitly calculated as : -9120448754896609940 ( 0x816da82dfc3bdd6c).

The already serialized data could not be deserialized. SUN corrected this error in next releases, introducing a compatibility flag

com.sun.xml.namespace.QName.useCompatibleSerialVersionUID
which having the value 1.0 switches to the Java 1.5 serial UID.

The Xerces project people picked up this change in newer version of xerces, but changed the flag name to
class="wiki">org.apache.xml.namespace.QName.useCompatibleSerialVersionUID


I recommend setting both of the settings. As the 'com.sun' options is already specified you just need to specify the 'org.apache' one:
JAVA_OPTIONS="$JAVA_OPTIONS -Dorg.apache.xml.namespace.QName.useCompatibleSerialVersionUID=1.0

20 nov 2007

Problems launching VirtualBox in headless mode with ssh

I recently installed a VirtualBox image to run remotely using ssh. My command was:

VBoxVRDP -startvm "GuestOS" >~/VBoxVRDP.log 2>&1 &

inside the ssh session.

Whenever closed the ssh session, the virtual machine was killed. I thought it was a problem of the terminal, so I tried out screen and dtach. But the problem was remaining. Googling around, I finally found:
http://www.virtualbox.de/ticket/722
In effect, I had shared clipboard enabled. After I disabled it, I could finally start VirtualBox using:
ssh -x zope@192.168.11.201 'VBoxVRDP -startvm "GuestOS" >~/VBoxVRDP.log 2>&1 &'

Now, I'm a bit happier.
Cheers.