Category: Technology

Posted on in Technology, Python

Vaikka olen ohjelmoinut Pythonilla jo jonkin aikaa, opin tämän vasta nyt kantapään kautta:

class Pizza(object):
  tyyppi = 'tavallinen' # tästä tulee luokkamuuttuja
  
  def __init__(self):
    self.juusto = 'tupla' # tästä tulee instanssimuuttuja

Oleellinen ero on siinä, että luokkamuuttujan muuttaminen vaikuttaa kaikkiin luokan instansseihin:

capricciosa = Pizza()
mexicana = Pizza()
# molemmat ovat tyyppiä 'tavallinen'

Pizza.tyyppi = 'pannu'
# nyt molemmat ovat tyyppiä 'pannu'

En tajunnut tätä heti, koska luokkamuuttujien määrittely näyttää Pythonissa samalta kuin instanssimuuttujien määrittely PHP/Java/C#-tyyppisissä kielissä. Ne kuitenkin vastaavat näiden kielten "static"-muuttujia.

Tätä juttua ei välttämättä huomaa ohjelmoidessa, kunnes jossain vaiheessa asettaa esimerkin mukaisesti Pizza.tyyppi = 'jotain' koko luokalle, ja silloin kaikki objektit muuttuvat kerralla. Luokkamuuttujan voi myös huomaamattaan peittää samannimisellä instanssimuuttujalla:

capricciosa = Pizza()
capricciosa.tyyppi = 'tavallinen'
mexicana = Pizza()
# molemmat ovat tyyppiä 'tavallinen'

Pizza.tyyppi = 'pannu'
# nyt ainoastaan mexicana on tyyppiä 'pannu'

Kyseinen ominaisuus on hyödyllinen ja noudattaa Pythonin logiikkaa, mutta sen kanssa pitää olla tarkkana.

Posted on in Technology

Lueskelin uusinta MSDN Magazinea. Mielenkiintoista, miten Microsoft käsittelee nyt sellaisia aiheita "kuumina uutuuksina", jotka olivat uusia muilla alustoilla toimiville web-kehittäjille noin kaksi vuotta sitten.

Artikkeleissa puhutaan esimerkiksi RESTistä, MVC:stä ja hajautetuista cacheista. Kuin pisteenä i:n päällä on C#:n "uusi" tietotyyppi nimeltä Tuple, joka on ollut esimerkiksi Pythonissa ja Rubyssa 90-luvun alkupuolelta lähtien.

Väistämättä tulee sellainen tunne, että Microsoft sätkii nyt kuolinkouristusten vallassa. Se yritti vielä hiljattain jatkaa omaa vendorlock-linjaansa WebFormeilla, SOAP Web Serviceillä ja vastaavilla hirvittävillä .NET-teknologioilla. Nyt on yllättäen ihan toinen ääni kellossa, kun muu maailma onkin valinnut MVC:n, RESTin ja muut avoimet ja valmistajariippumattomat teknologiat.

Lehdessä oli kyllä yksi ihan mielenkiintoinenkin juttu. Jon Flanders vertailee ansiokkaan objektiivisesti RESTiä ja SOAPia. Voin yhtyä hänen johtopäätökseensä, että SOAPia kannattaa käyttää vain silloin, kun rakennettava järjestelmä ehdottomasti vaatii jotain sen tarjoamaa ominaisuutta, kuten vaikka hajautettuja transaktioita. Silloinkin on syytä pohtia, onko kyseinen vaatimus oikeasti järkevä. Muuten kannattaa aina käyttää RESTiä ja pitää asiat yksinkertaisina.

Posted on in Technology, Python, Django

Ryhdyin tänään toteuttamaan unit-testausta erääseen Django-pohjaiseen web-projektiin. Ohjelmointikielenä on siis Python. Olen erittäin tyytyväinen siihen, miten kätevästi koko projektin voi testata yhdistelemällä sopivasti unittest- ja doctest-testejä.

Django tukee molempia frameworkkeja automaattisesti. Testipatteriston voi ajaa läpi yhdellä komennolla, joka näyttää onnistuessaan suunnilleen tältä:

$ ./manage.py test

.................
--------------
Ran 17 tests in 2.780s

OK
Destroying test database...

Aloitin testien rakentamisen luomalla joukon luokkia, jotka perivät unittest.TestCasen. Ne tyhjentävät tietokannan ja luovat sinne esimerkkidataa jokaista testiä varten. Näissä luokissa suoritetaan sellaiset laajemmat testit, jotka eivät liity pelkästään yksittäisiin funktioihin.

class SiteTestCase(TestCase):
    def setUp(self):
        cleanup()
        create_sites(self)
    def tearDown(self):
        cleanup()
    def testXxx(self):
        ...

Käytin globaaleja funktioita (cleanup, create_sites) tietokannan käsittelyyn, koska silloin niitä on kätevä hyödyntää myös doctesteissä. Doctestejä on puolestaan mukava ripotella suoraan funktioiden yhteyteen tuotantokoodiin. Ne ovat normaalikäytössä ikäänkuin kommentteja, eivätkä häiritse tuotantoa.

class Site(CompatibleModel):
    objects = CustomManager('site')
    name = SlugField(max_length=254)
    title = CharField(max_length=254)
    secret = CharField(max_length=254)
    notify_url = CharField(max_length=254)

    @classmethod
    def get_by_name(cls, name):
        """
        >>> from djangoapp import tests
        >>> tests.cleanup()
        >>> data = tests.create_sites()
        >>> site = Site.get_by_name('testsite1')
        >>> site.name
        u'testsite1'
        """
        return cls.objects.get(name__exact=name)

Ylläolevaan esimerkkiin on upotettu doctest-testi. Se siivoaa ensin tietokannan, luo esimerkkidatan, ja varmistaa sitten, että esimerkkidata löytyy tietokannasta kyseisellä funktiolla. Doctestin syntaksi on sama kuin interaktiivisessa Python-tulkissa. Tässä viimeinen rivi kertoo, mitä aiemman rivin pitää tulostaa, jotta testi menee läpi. Vastaavia ehtoja voi olla useita.

Doctestien selkeänä etuna on se, että ohjelmoija näkee koko ajan koodista, onko kaikkiin funktioihin liitetty tarvittavat testitapaukset. Niiden ohella voi vielä käyttää python-coverage -työkalua. Se mittaa testien ajamisen yhteydessä, montako prosenttia koodin kokonaismäärästä käytiin läpi.

Posted on in Technology, Python, Django

Disclaimer: I'm still learning Django so there may be smarter ways of doing the things I describe here. I also haven't tested this much though the principle seems to work. The example code has been edited and may not be fully correct.

Background

By default, Django 1.0 supports only a single, hardcoded database connection, defined by the settings.DATABASE_XXX variables. All models use the same connection.

A scalable website needs several different database connections when MySQL is used. Models might be stored in separate databases, and sometimes a read-only replica of a database might be used for querying. For sharding purposes, several different databases might be used for the same model.

Using a custom Manager to choose the database

The "objects" attribute of Django models can be replaced with a custom Manager class. This allows you to override the get_query_set() method, which chooses the database connection. For example:

from django import db
import new

class CustomSettings:
    DATABASE_HOST = '...'

CUSTOM_SETTINGS = CustomSettings()

class CustomManager(db.models.Manager):
    def custom_conn(self):
        conn = db.backend.DatabaseWrapper()
        cursor = lambda mgr: mgr._cursor(CUSTOM_SETTINGS)
        conn.cursor = new.instancemethod(cursor, conn,
            db.backend.DatabaseWrapper)

    def get_query_set(self):
        query = db.models.sql.Query(self.custom_conn())
        return CustomQuerySet(self.model, query)

class MyModel(db.models.Model):
    objects = CustomManager()

Managing transaction commits

Unfortunately, Django still uses a hard-coded connection object to commit transactions after saving models. This must be overridden or nothing will happen. First the INSERT:

class CustomManager(...):
    ...

    def _insert(self, values, **kwargs):
        return self.insert_query(self.model, values, **kwargs)

    def insert_query(self, model, values,
            return_id=False, raw_values=False):
        conn = self.custom_conn()
        query = db.sql.InsertQuery(model, conn)
        query.insert_values(values, raw_values)
        rv = query.execute_sql(return_id)
        # Need to commit here
        conn._commit();
        return rv

The UPDATE can't be committed in CustomManager, because it's called directly in the QuerySet object. This means you have to use an custom QuerySet object like this:

class CustomQuerySet(db.models.query.QuerySet):
    def _update(self, values, **kw):
        rv = super(CustomQuerySet, self)._update(values, **kw)
        # Need to commit here
        self.query.connection._commit()
        return rv

Choosing the shard in queries

When sharding is used, the database connection depends not only on the model, but also some parameter that needs to be passed with the query. Choosing a readonly connection is very similar. This can be done by adding a method to the custom Manager class:

class CustomManager(...):
    ...
    def get_query_set(self, shard_id=None, readonly=False):
        # Use the parameters to choose the connection
        ....

    def shard(self, shard_id):
        return self.get_query_set(shard_id)

    def readonly(self):
        return self.get_query_set(None, True)

# To query the model from a specific shard
MyModel.objects.shard(42).get(id=76)

# To use a read-only connection
MyModel.objects.readonly().get(id=76)

Conclusion

Multiple database and sharding are possible, but they require overriding some Django internals. This might break in future versions of Django.

Apparently work is already in progress for Django support multiple databases by default. Hopefully all the cases described in this article will be supported (multiple databases, read-only databases and sharded databases).

Update:

The DELETE operation was actually quite difficult to implement, because Django uses a global module-level delete_objects() function instead of going through the Manager and/or QuerySet. My current solution is to override the Model's delete() and manually delete by the primary key from the table.

ForeignKey references in models also cause problems when the field is not really a foreign key but resides in another database. I had to change them into IntegerFields and handle them manually in application code.

It would be really nice if Django had some generic database query context, which could be set up when querying or inserting/updating/deleting models. This context could be used to carry information about what kind of database connection is needed. The global module-level connection object could then be replaced with a call to some overrideable function that would see the context and could decide which connection to return.

Posted on in Technology, NoSQL

Disclaimer: I'm still learning CouchDB so there may be smarter ways of doing the things I propose here.

The problem

A typical requirement of any website is to enforce all registered usernames (and sometimes also email addresses) to be unique. This is trivial to do with a relational database like MySQL, but CouchDB doesn't have unique keys.

The standard solution

The usual way is to make the username (as chosen by the user) the _id of the User document. That will enforce uniqueness, but it also makes it difficult to change the username later, when there are lots of references to the document using the _id. It also doesn't solve the problem of having additional uniqueness requirements for other fields like email addresses.

Separating the unique id documents

Instead of the usual way, I'm using a separate document to hold the unique username as its _id, and another (auto-id) document that contains the actual user data. Each user would then be described in the CouchDB database by two documents like this:

{ "_id": "username/kennu", "type": "UsernameReservation", "user_ref": "71bacba712381adefg13712312312369" }

{ "_id": "71bacba712381adefg13712312312369",
"type": "User",
"username": "kennu",
"password": "xxxx",
"email": "webmaster@kfalck.net" }

This makes it easy to refer to the User document using the permanent auto-generated _id, while uniqueness is enforced by the separate UsernameReservation document. When the username is changed, a new UsernameReservation document is created to reserve the new name, and then the old document is deleted.

Additional unique fields can be added fairly trivially with documents like this:

{ "_id": "email/webmaster@kfalck.net", "user_ref": "71bacba712381adefg13712312312369" }

Handling the transaction

Because CouchDB doesn't have transactions, it's complicated to ensure that the username reservation happens only when the User document is also saved successfully with the corresponding username. In my code (I'm using couchdb-python and Document schema classes), I've defined a "save" method that will execute the following steps:


  1. Make sure the UsernameReservation document is created and belongs to the reserving user.

  2. Save changes to the User document.

  3. If an exception occurred, delete the UsernameReservation (unless it already existed before) and re-raise the exception.

  4. If a new User document was created, update the UsernameReservation with a reference to its _id.

  5. If the username was changed, delete the previous UsernameReservation document.

I'm pretty sure that can be made more beautiful. The basic problem is how to implement a multi-step CouchDB transaction properly in client-side code. It's impossible to make it survive server crashes and similar situations, but maybe a generic transaction could remember the _revs of the documents that were modified, and then restore them to the original state if something goes wrong.

Managing multi-master conflicts

There is one final challenge related to distributed CouchDB: what happens, if the same _id (e.g. username/kennu) is created simultaneously on two or more database servers?

In this situation, CouchDB will mark both documents as _conflicting revisions and more or less randomly choose one of them as the winner. It's up to the application to decide what to do when it encounters a username in _conflicting state. There are a few alternatives that are a bit nasty:


  • Assign a random username to both competing users and release the conflicting name to be re-acquired by whoever gets it first.

  • Put the user accounts on hold and require an administrator to manually resolve the conflict.

  • Somehow choose the winner and just apply the above to the loser.

Avoiding conflicts with a single master

Since usernames and emails are typically not changed very often, it might be more feasible to use only one master database for them. In this scenario, the application would write UsernameReservation documents only to the master, which guarantees they are never conflicting with each other.

Summary

Creating unique fields in CouchDB seems to be quite challenging compared to relational databases. It would be very helpful if CouchDB supported unique fields directly, or if it provided some kind of multi-step transactions for implementing them reliably using separate document _ids as described in this article.

Posted on in Technology, Apple

Kun ensimmäistä kertaa luin ZFS:stä jokunen vuosi sitten, olin innoissani. Vihdoinkin täysin virtuaalinen tiedostojärjestelmä, joka mahdollistaisi fyysisten levyjen lisäämisen ja poistamisen lennossa. Käyttäjä vain näkisi käytettävissä olevan kokonaislevytilan kasvavan tai pienenevän vastaavasti.

Normaalistihan uuden fyysisen levyn lisääminen virtuaaliseen levyjärjestelmään edellyttää RAID-konfiguraation päivittämistä ja koko tiedostojärjestelmän luomista uudelleen. Se on ainoa keino saada usea fyysinen levy näkymään yhtenä virtuaalisena kokonaisuutena.

Ei siis ihme, että Apple kiinnostui ZFS:stä. Sen idea sopisi mainiosti Mac OS X:ään, joka on muutenkin suunniteltu yksinkertaiseksi ja helppokäyttöiseksi mutta teknisesti edistyneeksi käyttöjärjestelmäksi.

Mutta nyt näyttää siltä, että ZFS ei ole lunastanut lupauksiaan. Solaris-käyttäjien kokemusten perusteella se on edelleen epäkypsä, sekoaa herkästi tietyissä tilanteissa ja aiheuttaa datan menetystä. Apple puolestaan on poistanut saitiltaan kaikki viittaukset ZFS:ään, vaikka siitä piti tulla osa Mac OS X:ää.

Posted on in Technology

Jouduin taas pitkästä aikaa tekemään asioita Tomcatilla ja servleteillä eli konfiguroimaan Javaa. Voi hyvänen aika että se on edelleen hankalaa ja monimutkaista. Sadoittain erilaisia propertyjä, classpatheja, jarreja ja xml-tiedostoja, tolkuttoman pitkiä error traceja.

Java-ihmisillä ei ilmeisesti koskaan käy edes mielessä, että arkkitehtuureja voisi yrittää yksinkertaistaa. Tämä on toisaalta loogista, koska he ovat lähinnä konsultteja, jotka saavat palkkion monimutkaisuuden luomisesta ja ymmärtämisestä.

Omassa tapauksessani piti tehdä simppeli servletti, joka kutsuu toisaalla olevaa SOAP-rajapintaa ja tulostaa sen palauttaman arvon. Käytännössä tämä edellytti ainakin seuraavia asioita (serverinä Ubuntu 9.04):


  • Tomcatin, Axis-kirjaston ja mod_jk:n asentaminen.

  • Mod_jk:n konfiguroiminen Apachen direktiiveillä.

  • Erillisen (turhan) workers.properties-tiedoston konfiguroiminen.

  • Tomcast-sovelluksen context-asetusten konfiguroiminen.

  • Tomcast-sovelluksen web.xml-asetusten konfiguroiminen (pitää määritellä servlet-name, servlet-description, url-mapping jne.)

  • Oikean classpathin ja komentorivin arpominen WSDL2Java-työkalulle (aina yksi puuttuva jarri lisäten ja kilometrin error tracea tulkiten).

  • SOAP-stubin generoiminen WSDL2Javalla.

  • Pohtiminen, mitä WSDL2Javan generoimasta neljästä luokasta (PortType, Service, ServiceLocator, SOAPStub) pitää oikeasti kutsua.

  • Servletin koodaaminen Javalla (tämä oli kaikkein yksinkertaisin osuus).

  • Ihmettely, miksi SOAP-stubin käyttäminen aiheuttaa Class Not Found -virheilmoituksen Tomcatissa (joka tietenkin vielä vaihtelee ja antaa hieman erilaisia ilmoituksia välillä).

  • Tomcatin permissioiden konfiguroiminen policy.d:hen siten, että servlet saa AllPermissionit, tarvittavat FilePermissionit ja RuntimePermissionin getClassLoaderiin. Ilman ei Axis toimi.

Voi tietysti väittää, että kaiken tämän osaaminen on rautaista ammattitaitoa. Minä taas väitän, että se on pelkkää puupäisyyttä. Käyttämällä SOAPin sijasta REST-rajapintaa tämän kaiken voisi tehdä vaikkapa Pythonilla tai PHP:llä yhdellä ohjelmarivillä, joka tekee yhden HTTP-kutsun.

Ratkaisun tulisi aina olla yhtä yksinkertainen kuin ongelmakin on.