Patates et exceptions

mai 30, 2013 1 commentaire

On a vu précédement pourquoi les exceptions vérifiées sont une mauvaise idée (que je vous conseille de lire en préambule), voyons à présent comment bien gérer les exceptions.

Rassurez vous, c’est tout simple : faut voir ça comme le jeu de la patate chaude. Par défaut, on sait pas quoi en faire et faut juste refiler le bébé le plus vite possible aux autres. Facile non ?

Bon, on est joueur, on va faire ça en trois parties :
1. Passer la patate efficacement
2. Savoir mettre le feu à la patate
3. l’art de bien attraper la patate

1. Passer la patate efficacement

Comme vous le savez, bien jouer à la patate chaude implique, surtout, de ne pas avoir à s’occuper de la patate. Si on ne l’a pas, on risque pas de se rater lors de la relance. En Java, c’est pareil. Si on ne sait pas quoi faire d’une exception, le mieux est de la laisser filer. On ne l’attrape (catch) pas, rien, nada.

Bien sûr, on a ces merveilles d’exceptions vérifiées (Checked exception), celles qui vous forcent à les catcher. On en fait quoi alors ? C’est tout simple, on en fait des RuntimeException, par exemple :

try {
	// code lançant des exceptions vérifiées
} catch (MonExceptionVerifiee e){
	throw ExceptionUtils.wrap(e);
}

avec :

public class ExceptionUtils {
	
	public static RuntimeException wrap(Throwable t){
        if (e instanceof RuntimeException) {
            return (RuntimeException) e;
        } else {
            return new RuntimeException(e);
        }
	}
}

Du coup l’appelant n’à rien à attraper et l’exception sera traitée à plus haut niveau. La vie est belle non ?

Certes, par rapport aux exceptions vérifiées, l’appelant en sait moins sur les façons dont votre méthode est susceptible de mal tourner. Mais, ça n’a rien d’inéluctable : le mot clé throws, indiquant les exceptions lancées par une méthode, marche tout autant pour des exceptions vérifiées que nous vérifiées. Aussi, rien ne vous empêche d’écrire:

public Foo bar() throws BarException{
	// ..
}

avec

public BarException extends RuntimeException {
	// ..
}

Vu que BarException étend RuntimeException, vous ne serez pas obligé de catcher à chaque throw, par contre votre IDE préféré devrait vous indiquer son existence à la moindre occasion.

Par contre, par pitié, n’indiquez pas que vous êtes susceptibles de jeter n’importe quelle exception, genre:

public Bad doBad() throws Throwable

ainsi que la variante

public Bad doBad() throws Exception

En effet, là vous vous contentez d’indiquer que le code est susceptible de mal tourner, d’avoir des bugs et ainsi de suite. Or c’est le cas de tout bout de code. Du coup n’importe quelle application se doit de gérer le cas de l’exception inattendue, celle due à un bug bien idiot. Aussi, en indiquant « throws Throwable », vous ne faites que rappeller l’évidence verbeusement. Maaaal.

Il y a encore pire : avaler l’exception. Vous savez, ce que propose de faire Eclipse par défaut:

} catch (Exception e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}

Mais encore plus pire est possible (youpee) :

} catch (Throwable t) {
}

Certes, vous n’aurez plus d’exception dans l’application. Mais celles ci ne sont que les symptômes du mal… Sans elles, le mal sera toujours présent mais en plus indétectable. Plantages et comportements aléatoires en perspective, que du bonheur.

A noter que catch(Throwable t) détient la palme. En effet, pour mémoire, la hiérarchie des exceptions Java est la suivante:

Throwable (vérifiée) <- Error (non vérifiée)
^
|

Exception (vérifiée)
^
|
RuntimeException (non vérifiée)

Tous les problèmes du niveau de la JVM remontent via des sous classes d'Error. Quelques exemples fameux: StackOverflowError, OutOfMemoryError et autres KernelError.

Les problèmes dus au code sont signalés via des classes héritant d'Exception ou de RuntimeException. Genre les IOException ou les ClassCastException.

Du coup, le petit malin qui avale tout à partir de Throwable ne saura notifié ni des erreurs de son code ni des problèmes de la JVM. Sympa non ?

2. Bien allumer la patate lors de sa cuisson

Maintenant qu’on sait comment passer la patate, voyons comment la créer.

En premier lieu, toujours hériter de RuntimeException (sauf pour les développeurs derrière la JVM, bien sûr, qui eux étendent Error).

Pourquoi RuntimeException ? Facile : on veut faciliter la passe de la patate chaude, donc autant que l’appelant n’ait rien à faire par défaut, comme ça au moins il ne risque pas de faire mal. Genre avaler l’exception.

Ensuite, quel est le but de la patate ? Signaler un problème, un dysfonctionnement, une erreur. Du coup, autant communiquer un maximum d’informations : vos exceptions doivent toujours avoir un constructeur avec un message de type String. A chaque création d’une instance on pourra alors donner un maximum d’information de contexte. Genre les paramètres de la méthode appelée. Par contre, le nom de la méthode et la pile d’appel sont là par défaut, dans la pile d’appels (stack trace) pas besoin de dupliquer…

Si jamais votre exception encapsule une autre exception, merci de la lui donner. En effet, que se passe-t-il si je code ainsi:

catch (IOException e){
	throw new MyException("File access failed");
}

C’est tout simple : la personne traitant mon exception ne pourra pas en connaître la cause réelle. Et même dans le cas de cette bonne vieille IOException, si commune, ne vous laissez pas abuser : ces sous classes sont aussi diverses que ErrorInQuitException ou BadAudioHeaderException. Bref, si une exception a pour origine une autre exception, toujours la transmettre.

Bien sûr, si on peut donner du contexte, c’est encore mieux :

catch (IOException e){
	throw new MyException("File access failed for file '" + file + "' and user '" + user + "'", e);
}

In fine, le squelette d’une bonne exception à soi est, à mon humble avis, quelque chose du genre :

public class MyException extends RuntimeException{
	
	public MyException(String message){
		super(message);
	}

	public MyException(String message, Throwable throwable){
		super(message, throwable);
	}

	public MyException(Throwable throwable){
		super(throwable);
	}

} 

Pas de constructeur sans paramètre: on veut nécessairement du contexte. (Bon des fois y en a vraiment pas au delà de la pile d’appels, mais bon, comprenez le message ;)).

Si jamais vous décidez d’ajouter des paramètres supplémentaires et spécifiques à votre application, n’oubliez pas alors de surcharger getMessage(), afin que tout le contexte soit toujours affiché.

3. L’art de bien attraper la patate

A l’occasion, si on sait quoi faire sur l’exception, alors on s’en occupe.

Cela arrive généralement en deux circonstances : soit au plus près de l’exception, quand on peut retomber sur ses pieds autrement, soit plusieurs niveaux d’abstraction plus haut, aux frontières de l’application ou du module. Genre pour afficher un message d’erreur à l’utilisateur, ou au système tiers.

Gérer l’exception au plus près est normalement simple : on ne fait cela que si on sait quoi faire.

Par exemple:

public boolean isValidInteger(String text){
	try {
		Integer.parseInt(text);
		return true;
	} catch (NumberFormatException e){
		return false;
	}
}

Pour la gestion à plus haut niveau, il faut s’assurer d’attraper vraiment toutes les exceptions. Pour cela, il y a depuis Java 5 la méthode Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler) qui définit qui sera appelé en dernier recours, si rien n’a attrapé l’exception au préalable. Ainsi toutes les exceptions seront attrapées de façon certaines, qu’elles aient été lancées le thread courant ou un autre. Faites en bon usage.

La gestion de l’erreur implique souvent la communication d’un message à un tiers, que ce soit l’utilisateur ou le système appelant. Il arrive parfois que des actions plus drastiques soient nécessaires, particulièrement dans le cas des Error, où parfois l’arrêt est la seule option (dans le cas d’une OutOfMemoryError, c’est le seul remède que je connaisse par exemple).

Toutefois, qu’on traite l’exception au plus près ou à plus haut niveau, toujours, j’ai bien dit TOUJOURS, faire en sorte de donner tout son contenu au développeur qui devra débugger le code. Ne serait ce qu’un log, fusse-t-il de niveau debug, ou, même, au pire, une sortie console. L’essentiel est que toute l’info soit accessible sans devoir changer le code. Se contenter d’afficher le message de l’exception (sans la pile d’appels) est, par exemple, notoirement insuffisant. De même, si vous utilisez un framework de logging, assurez vous qu’il affiche bien tout le contenu de l’exception, ainsi que des diverses différentes exceptions éventuellement contenues dans l’exception courante. La stack trace complète est de la plus haute importance.

Espérons que ces conseils améliorent votre gameplay à la patate chaude. Bien que souvent négligé et bien moins hype que d’autres jeux plus récents (comme réinventer la roue par exemple), gérer correctement les exceptions est crucial pour toute application, afin d’en assurer l’évolution et la maintenance sereinement.

Au plaisir de vous lire !

NB: pour les utilisateurs d’Eclipse (je sais, ça aussi c’est pas à la mode, désolé), notez qu’il y a une « Java Stack Trace Console » disponible au niveau de la console eclipse, via le bouton « Open Console ». Lorsque vous y collez une stack trace, eclipse vous permet d’aller aux différents points concernés en cliquant sur les lignes de la stack trace. Pratique. Pas révolutionnaire mais pratique.

NB2: Java 7 introduit le multi catch et autres try with resources. Bien que pratiques, ils ne changent pas fondamentalement la donne (sauf pour l’art obscur de la fermeture de ressources, non abordé ici).

Publicités
Étiquettes : , , ,

Pourquoi les exceptions vérifiées sont une mauvaise idée

mai 30, 2013 5 commentaires

Dans la série « je blogue pour éviter de me répéter », je demande les exceptions vérifiées.

Attendez, je vous devine: c’est quoi une exception vérifiée ?

C’est une invention de Java (à ma connaissance) consistant en une exception que le contexte est obligé de considérer

Autrement dit, si j’écris:

throw new MonExceptionVerifiee();

avec

public class MonExceptionVerifiee extends Exception

alors le code entourant cette instanciation est obligé de prendre en compte l’exception.

Deux façons pour cela, la première consistant à relancer l’exception pour ne pas avoir à s’en préocupper :

public void maMethode() throws MonExceptionVerifiee {	
	throw new MonExceptionVerifiee("Pour la cause");
}

Le throws au niveau du nom de la méthode est ce qui permet de relancer l’exception.

La seconde façon consiste à attraper l’exception pour la traiter, comme suit:

public void monAutreMethode() {	
	try {
		throw new MonExceptionVerifiee("Pour la cause");
	} catch(MonExceptionVerifiee e){
		// ?
	}
}

Bon, là ça présente peu d’intérêt, en général on attrape plutôt les exceptions vérifiées lancées par les autres, par exemple :

public void monAutreMethode() {	
	try {
		maMethode();
	} catch(MonExceptionVerifiee e){
		// ?
	}
}

Pour finir avec l’introduction aux exceptions vérifiées, sachez qu’une exception, pour être vérifiée, doit hériter de Throwable ou Exception, mais qu’elle ne l’est pas si elle hérite d’Error ou de RuntimeException, ce qui la hiérarchie des exceptions Java suivante :

Throwable (vérifiée) <- Error (non vérifiée)
^
|

Exception (vérifiée)
^
|
RuntimeException (non vérifiée)

Autrement dit, Throwable est tout en haut et est une exception vérifiée. Error étend Throwable, et n'est pas vérifiée. Exception étend Throwable, et est vérifiée. RuntimeException étend Exception et… n'est pas vérifiée.

Alambiqué hein ? Je suis d'accord, et en général si la conception est compliquée, alors l'idée est foireuse. Mais j'anticipe.

En effet, pourquoi diable les exceptions vérifiées?

L'intention initiale est de forcer le code à considérer les exceptions. En effet, comme cela, en lisant un fichier, je serai bien conscient que la lecture de ce dernier peut planter. Certes, c'est appréciable, mais que puis je faire pour autant ? Et là est tout le mal des exceptions vérifiées. Leur traitement n'est pas simple et, surtout, rien ne garanti qu'il sera bien fait.

Revenons à l'exemple précédent :

public void monAutreMethode() {	
	try {
		maMethode();
	} catch(MonExceptionVerifiee e){
		// ?
	}
}

A noter que si une méthode lance plusieurs exceptions vérifiées, on est obligé de les traiter chacune. Par exemple, l’autre de maMethode pourrait choisir de lancer également des IOException, comme suit :

public void maMethode() throws MonExceptionVerifiee, IOException {	
	throw new MonExceptionVerifiee("Pour la cause");
}

alors la méthode appelante sera du genre :

public void monAutreMethode() {	
	try {
		maMethode();
	} catch(MonExceptionVerifiee e){
		// ?
	} catch(IOException e){
		// ??
	}
}

Verbeux. Et embêtant: la prochaine fois que l’auteur de maMethode choisira d’ajouter un throw, il faudra que je change à nouveau mon code. Que faire quand on est malin ? Facile, l’obligation de traiter les exceptions vérifiées prend en compte la hiérarchie, du coup je peux écrire ça :

public void monAutreMethode() {	
	try {
		maMethode();
	} catch(Throwable t){
		// ?
	}
}

Tadamm ! C’est nettement moins verbeux, et même plus robuste au changement : le petit gars derrière maMethode peut se lâcher. Désormais, j’suis blindé.

Vous noterez le commentaire « // ? » dans le catch, vu que sur le coup je ne savais pas quoi faire. Faire un tel catch qui ne renvoie aucune exception s’appelle avaler une exception, ou dans le cas présent avaler toutes les exceptions. Fort hein ? Ca semble la parade ultime…

Sauf que… sauf que désormais, plus aucune exception ne remonte. Vérifiée ou pas. Lancée par le programme ou la JVM (toutes les exceptions découlant d’Error). Nada. Rien. Et là, bonjour l’enfer : désormais en cas de plantage, plus aucune info n’est dispo. Avec un peu de chance l’appelant se rend compte qu’un effet de bord n’est pas présent (genre je clique et rien ne se passe), mais ce n’est pas garanti. A débugger, cela relève des travaux d’Hercule : aucune info, sauf que ça marche pas. Pas de trace.

Les seules options que je voie est de modifier tous les catch (ah ah ! – surtout sur du code tiers) ou de s’taper toute l’application au débuggeur… Dans les deux cas, vous serez forcé de faire ça après coup sans aucun élément sur l’origine du problème. Dans les deux cas, vous chercherez une aiguille dans une botte de foin. Et si votre code contient beaucoup de catch avalant des exceptions, vous avez un sacré paquet d’aiguilles différentes à chercher une à une, tout au long de la vie de votre application…

Bien sûr, me direz vous, aucun développeur digne de ce nom n’aurait la bonne idée d’avaler toutes les exceptions. Mais le catch vide est juste très facile, tentant même. Dans l’exemple ci dessus, dans monAutreMethode, je ne savais pas quoi mettre lors de la rédaction de l’exemple, j’ai mis un commentaire vide, le // ?, pour remplir. Et devinez quoi, ça compile et ça peut partir en prod sans problème! Combien de développeurs se retrouvent exactement dans la même situation ? Combien laissent un commentaire type « TODO » et autres « Euh, à traiter? » puis continuent comme si de rien n’était ? Beaucoup. Trop. De plus, cela semble résoudre le problème du traitement d’erreur, de ces satanés IOEXception dont on ne sait que faire…

Sans compter que cela ne semble pas être une si mauvaise pratique : eclipse propose par défaut de quasiment avaler les exceptions vérifiées. En effet, le quick fix par défaut d’eclipse pour gérer cette fameuse maMethode() throws MonExceptionVerifiee, IOException est le suivant :

try {
    maMethode();
} catch (MonExceptionVerifiee e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}

Génial non ? Exception avalée, pile d’appels imprimée mais… sans le message de l’exception (accessible via e.getMessage()). Noter cet appel direct à e.printStackTrace() : votre API de logging préférée n’a aucune chance de s’en mêler.

Bref, le professionnalisme est mis à rude épreuve et avaler les exceptions semble la bonne solution. Du coup, chaque code lançant une exception vérifiée est à la merci du premier développeur commettant une erreur dans son traitement. Quelques lignes et zou plus rien, des erreurs cachées et une application à débugger dès qu’on se rendra compte de l’erreur. Peut être après mille appels à la méthode plantant. Peut être après milles confirmations envoyées à tort. Splendide et… imparable.

Pour la petite histoire, avaler les exceptions n’est pas toujours le fait de développeurs. Eh oui, j’ai vu des architectes, et pas nécessairement Java, faire ainsi dans leur code. Sur toute l’application. A chaque fois qu’ils pouvaient se faire mordre par une saleté d’exception. Pour la défense de ces érudits, notons que nos glorieuses écoles d’ingénieur en informatique n’enseignent rien en la matière. J’ai discuté avec de nombreux étudiants de nombreuses (hautes) écoles différentes, ou d’université, et jamais le thème n’avait été abordé. Après tout, les exceptions, c’est pour ceux qui font du mauvais code en premier lieu non ?

In fine, sommes nous désormais d’accord que les exceptions vérifiées sont une mauvaise chose ? Rassurez vous, y a moyen de les traiter correctement : c’est l’objet de l’article suivant, Patates et exceptions. On se voit là bas 😉

Étiquettes : , , ,

CloudBees talk at the ElsassJug

Yesterday night, Sacha Labourey came to the Elsass Jug to introduce CloudBees.

It was pretty nice to see Sacha IRL: he’s a very friendly and humble person (while being, for those not knowing him, creator and CEO of CloudBees after quite a career). It’s really easy and interesting to speak with him so go and get him whenever you need/can.

CloudBees specializes in using the clouds resources and tools for developing and running Java (web) applications (Platform As A Service way). It’s what they call their Dev@Cloud and Run@Cloud.

Having Koshuke among their staff, the whole stack is centered around Jenkins: if you use it, then you’ll be like at home. Actually, you’ll be most likely better than at home: their jenkins come with slaves running each job (infinite scaling FTW) as well as the usual plugins plus some on top from third parties.

Roughly put, it’s a nicely already set up Jenkins environment, with some goodies like remote console line tools or Eclipse plugins. To me it really felt like all the tools you wanted to use with Jenkins but already there and waiting for you (like having slaves running the jobs to avoid bottlenecks).

The Run@Cloud part is also interesting: basically, once your jenkins jobs are done, you can decide to promote some to production. They are then deployed to your web apps. The web apps location(s) are pretty flexible: they can either host them (if I got it right), put them in some well known hoisters or, in fact, any cloud (as long as openstack compatible). Actually, it’s not only about web apps, they can also handle DBs (mysql only AFAIK).

One option I wasn’t aware of is that the deployment part can also be building images (VSphere ones, so VMWare) and push these images wherever you want. In line with their concept, looks like they handle the OS and stack parts of these images: you just have to put in your WAR file and you’re done. Sweet isn’t it?

For sure, they’ve plenty of goodies not spoken of here: they can have webdav based maven repos (you can also rent on top some artifactory), the monitoring tools for the running apps look great (again, the fancy tools you heard of but never bothered to actually put in use are available here) and so forth.

To sum up, they really aim on doing the « devops » part of development, so developers can concentrate on… developing. Not too bad isn’t it? In terms of target, they have pay as you go pricing and others one for bigger users.

One example Sacha put forward to showcase the whole concept is Loose It. Loose It is a mobile/web app helping you to loose weight. The interesting part there is that they’re only 5 developers and they’re able to deliver 1.5 millions users and up to 20 000 transactions per minutes. All this without scaling issues and dedicated staff. Just 5 devs in the wild. Well, for sure, I guess they owe CloudBees some money, but obviously only a fraction of what it would have costed them to handle it all on their own.

My thought being put to words, I’m off 😉

++
joseph

Side note: in case you wonder why this post is in english, it’s pretty straightforward: I wanted this dear uwe to be able to be able to read it, since his company looks like the perfect fit for CloudBees and… I would be eager to have some first hand feedback. Nasty me? You bet 😉
BTW, thanks uwe for proof reading this post.

Étiquettes : , , ,

Apache Maven, 2eme édition

décembre 12, 2011 2 commentaires

Cet été (si loin déjà!), Pearson m’a envoyé un exemplaire gratuit du livre Apache Maven 2 et 3, 2eme édition, grâce à la sympathique entremise d’Arnaud Héritier et Nicolas de loof.

Bien que j’ai dévoré le livre dès réception, la présente critique a été retardée par un déménagement/changement de taff. Ceci dit, mieux vaut tard que jamais, et maintenant que le contexte a été posé voyons pour une critique sans faux semblant…

Alors, que dire d’Apache Maven 2 et 3? Pour les pressés, en quelques mots: si vous sentez débutant/intermédiaire en Maven, foncez. Si vous êtes expert et cherchez à tuner votre Maven, passez votre chemin.

Pourquoi foncez? Tout simplement parce que le livre réussi à bien mettre en avant tout l’intérêt et la force de Maven, abordant les multiples domaines d’usage, sans sombrer dans le guide de référence infâme et soporifique (et supplanté par internet depuis que l’ADSL existe), le tout avec un mode narratif franchement agréable passé la surprise initiale (il s’agit en effet de Nicolas et Arnaud narrant comme telle ou telle personne ont eu tels soucis et comment cela a été résolu). Et comble du raffinement, nos deux comparses fournissent en chemin des trucs et astuces pour des problèmes « courants » qui, perso, m’ont bien servi depuis.

Certes, tout un chacun trouvera des aspects qu’il aurait souhaité voir plus approfondi, mais c’est également une des forces du livre: il pousse à aller de l’avant, tout en donnant les bases pour qu’on se sente capable de le faire. A l’inverse, il ne cache pas certains aspects encore perfectibles de Maven, tout en fournissant assez d’info pour que le lecteur ne soit pas démuni.

Je ne ferai qu’une critique négative de taille: les auteurs tentent de rassurer sur le futur de Maven, entre Sonatype et la fondation Apache. Et perso ils ont produit tout l’effet contraire. En effet, si on lit bien que les deux organisations sont faites de personnes pleines de bonne volonté, on devine également aisément que des tensions sont présentes, sans en avoir le détail, tout en voyant de façon évidente les contradictions des deux approches, entre le rythme mesuré de la fondation Apache et de ses règles et le dynamisme zélé (et intéressé) du papa de Maven et son équipe.
J’ai également ressenti un certain flou sur les produits Sonatype. Comment s’intègrent ils avec le reste? Où commence le domaine payant? Quelle stratégie est suivie par Sonatype dans tout cela?
Bref, ça manque de vision d’ensemble, à mon sens, ainsi que d’une présentation claire et sans ambages des tensions présentes (ou passées, j’en sais rien moi) histoire de poser le cadre.

Avec le recul, toutefois, cette critique s’atténue pour laisser place à la gratitude de se servir au quotidien des conseils et de l’inspiration générées par ce bouquin. Aussi, vue la période, je concluerai simplement: un parfait cadeau de Noël pour geek ou geekette!

Allez, bonne lecture à tous et encore merci à nos deux auteurs!
++
joseph

Étiquettes : , , ,

mongoDB: 9 months on – conclusion

Article part of my « mongoDB 9 months » series:

First of all, if you made it till here, congratulations. I had much in my mind and I fear I wrote it too quickly, and even after some updates on different parts of the series I still feel poor with the way they’re written. I guess I’ll have to improve them over time. Hence, I would be eager to know how readers feel about it, so please, let me know!

Back to mongoDB, it’s a powerful tool, whose document orientation really help reducing the infamous impedance mismatch, thus greatly reducing mapping issues. Its indexation abilities, especially of « in document » lists, are also impressive, as are its performances overall.

This comes at the price of some radical technical choices: no join and no transaction (mostly).

Hence to me the following « rules » on when to use it:

  • Domain logic which can be mapped to document without needing relationships: go for it, mongoDB is the perfect match. Its high performance will do wonders. And since documents can embed so much, it actually means more than one could expect.
  • Domain logic with relationships and few if any transactional needs, where performance matters. mongoDB should be looked at. One should first carefully consider the handling of relationships. A proof of concept for it is a must have I think. The transactional needs are a different beast: one should make sure they could fit somehow in mongoDB non existing support for such things. And remember it’s highly unlikely 10gen will ever introduce new features there, due to its (potential) impact on performances. This actually leads me to the third point:
  • Domain logic with lot of needs for transactional operations: mongoDB doesn’t fit.

For sure, these are only my humble opinions and I’m eager to hear other ones.

Some side notes comes to my mind as well:

  • if ever a real need for transaction pops up, it won’t be solved for free. A solution will have to be found. For example, it could the limited 2 stage commit (more on it in the No transaction & limited ACID chapter). It could also be some other mechanism for ensuring transactional matters (some people went to mysql for this, but one could also consider something like JTA or, rather, multiverse). Finally, one could split the data in two between a « transactional able » DB and mongoDB. Anyway, the bottom line stays: transactions won’t come for free, whereas it’s a feature which is usually taken for granted due to the massive use of « traditionnal » RDBMS. This leads me to the next point:
  • Plan for quite some explanation about mongoDB choice. From JSON content and queries to document approach, all this without join and transaction, mongoDB has a lot which differs and which will, for sure, unsettle and disturb. It’s true for the developers, going away from their safe RDBMS land (« ugh, where is my transaction? »), to admin (« what, I can’t export to an CVS file? ») to business analyst/product lead with a past history in IT or some knack at it. They’ll all be surprised! Take the time to consider and win their heart & mind properly.
  • Plan to keep looking at mongoDB progress: new features will come often and bugs get fixed. One has to follow carefully not to miss some of them.
  • Finally, a point which I haven’t really spoken of it, but which matters as well: watch for your driver/mapping framework. It would be a pity for it to be the limiting factor, yet at the same time the limitation (mostly regarding type info) and flexibility of JSON makes the issue pretty hard, at least on the mapping side. On top of this, refinements like first and second level caches are still welcome performance booster. Then some support for relationships and their management would also be welcome. In the end, the driver business isn’t straightforward.

On a more personal matter, retrospectively, I’m also wondering if the approach of putting write and read all into (one) mongodb is the right approach. Indeed, de normalization means that, quickly, some decisions will be taken based on de normalized data which could be stale. Even without going so far, the documents can quickly end up being pretty messy, with de normalized content for specific views and the like. And still is lurking in the background the lack of transaction: how to be sure all potentially failing multiple updates in a row are well handled?

As such, my current interest in CQRS and EDA, which states the separation of view database and the write one, rings a bell. Indeed, mongoDB makes a perfect fit for the view database: it can handle both full text search and complex queries, yet being quite flexible in terms of mapping for your views. On the other hand, the write database could stick to some RDBMS able of join and transaction, where needed (which in CQRS should be less than in traditional « dump in all into the RDBMS » approach). Sure it may involve extra work, but if you choose mongoDB for write you weren’t afraid, most likely, of extra work anyway. And still, the clarity and flexibility given might well pay off quickly. Yet this is just wild thoughts: I hadn’t any occasion to test them, even if a akka/scala for events, some RDBMS for write and mongoDB for view feels like truly appealing to me.

Actually, I would also love a document oriented db for the write part. Basically mongoDB with better relationship support (from integrity constraints to join) and transaction would be perfect. Yet, while the relationship support can and is likely to improve over time, the transaction aspect feels like way more remote. It simply doesn’t match with the current performance minded approach and, furthermore, would imply a massive amount of changes… Pity!

Before we part, let me thank a hell of a lot codesmell, my tech lead, who has always been eager to endure my lengthy questions and talks on mongodb and related matters. Without him the current series wouldn’t have seen light, it’s as simple as this!

And don’t forget: I more than welcome your view on this series!

Étiquettes : , ,

mongoDB: 9 months on – The good to know

Article part of my « mongoDB 9 months » series:

Anyway, let’s have a look at « The good to know ».

Documents are (mostly) large beasts, and we don’t care

A document encompasses all related concepts, as well as potentially some de normalized data.

As such, a document can easily have its own content, plus a list of Cities, with each city having actually most of its content. Indeed, having a list in a document is normal in mongoDB, and while it changes from SQL one shouldn’t be surprised about it. In fact, mongoDB is even very good at indexing such lists. Then, for query/sorting purposes, de normalization kicks in quickly. While developing, one often finds oneself adding more and more into some document, which can then feel like « big » and, even worst feeling, growing quickly.

But then, it mostly doesn’t matter: if you’re only after one document, then normally its size shouldn’t matter much. mongoDB serves data fast and the network between DB server and application server should be blazing fast anyway. On top of that most likely the document « contains it all »: additional query for some other documents should seldom happen.

For list, one uses field selection. Then no matter how big some documents grow, you’ll only fetch what you need, making the size matter irrelevant.

Next to the query aspects, mongoDB has also some very good compression of the data stored and a high upper size limit for documents, of 16Mo in 1.8. This is huge! Our biggest document don’t come close from this. Actually, apart from embedding binary data (like images and the like), I hardly see how content made of string and numbers can take more than 16Mo when compressed. This must be huge and very unusual. So don’t worry: if you need some data in a document, put it in. Don’t try to reduce its size, for example by shortening the JSON keys. It’s premature optimization at best (and purely lost time most probably): press forward and enjoy!

Read/write query routing not predefined

While mongoDB provides some nice master/slaves asynchronous replication, query routing is done solely on the application server side, with a default of all queries to the master, on a per query basis. This is IMHO quite error prone and thus, if needed, requires either some carefully done queries or tools. Distributed read queries are thus to handle carefully and not directly « out of the box ».

About performance

mongoDB has very good performance. We did some tests and were amazed. Yet, retrospectively, this seems logic: mongoDB has no join and no transaction. All the complexity of joining is absent. No cartesian products and the like. The lack of transaction also significantly reduces lock and versions handling. No such thing as a Multiversion concurrency control engine. This almost has to be fast. Performance is almost the sole reason of all these ACID/transaction restrictions, so make good use of it!

Yet, while all true and well, let’s not forget than better than a (fast) DB query is… no query at all! At such, application server side first level and second level caches still provide welcomed performance boost and should be thought of, especially if some relationships are planned. They indeed save quite some additional and sequential queries, a danger always lurking around in mongoDB.

Étiquettes : ,

mongoDB: 9 months on – « Take it or leave it » technical choices

mai 11, 2011 1 commentaire

Article part of my « mongoDB 9 months » series:

Anyway, let’s have a look at mongoDB « Take it or leave it » technical choices.

No join and relationships

Coming from a key value store background, mongoDB provides no equivalent of RDBMs relationships. Actually, the only help provided is the DBRef. However, DBRefs are very limited: it’s a convention to store a collection name and a document identifier. They don’t behave at all like foreign keys: mongoDB keeps no count/trace of them and a document can be deleted whatever the DBRefs pointing at it.

Actually, query can act upon only one collection type at a time. It means it’s possible to have multiple documents from the same collections in one query but fetching 2 documents of 2 different collections requires 2 queries. Be it for retrieval or only to take part in some query, joining is simply not available.

In more details, when displaying the document, it means as many queries as distinct collection types referenced. Let say you have a building collection and you want to display it and where it is. Maybe for the location you have the City, Country and Region. Then you have to query the City, Country and Region collections one a a time.

Doing four queries instead of one for displaying a document might sound « not perfect but bearable ». However, the matter get worse if you want to display a list of buildings and their locations. There, for each building, you would have to query 3 times on top. For a list of 20 buildings, you would end up with 61 queries. This most likely won’t fit anymore.

Starting from there, a long list of improvements is possible:

  • You could have some kind of unit of work on the client side in order to avoid to request twice the same document. While this again involves some tooling, you can’t rely on it to make queries fast enough, since they may all reference different locations as well as referencing many other documents in your database.
  • Going further on the unit of work business, one could have a second level cache, and do the relevant caching on the application server side. As such one could avoid hitting the database for each document, skipping the network overhead and potentially some serialization/deserialization on the way.
  • Another improvement would be to somehow collect first all the City, Country and Region ids, in order to do only 3 extra queries on top, then to rewire the fetched locations to their original buildings. While doable, this once again requires extra tooling
  • Depending on the use case, loading lazily the referenced documents could also be of some help.

In the end, each of these improvements requires some server side tooling, and sometime some serious one. Applying them all isn’t this trivial neither. In the case of the list of entities, lazy loading of references could in fact be a real downer (if lazy loads happen a lot).

These options are even better combined actually. An unit of work extending over the lazy loading, for example, sounds like the way to go. Similarly for the second level cache.

On top of that, the query to get the building list in the first place matters as well. It’s not just about fetching entities.

Indeed, when creating the building/location list, you may want to order by countries and cities names. You most likely want to offer pagination as well, to make it all user friendly. If you stick to plain DBRefs and the like, it means:

  • fetching all the buildings of interest (and not only the 20 first – hopefully you don’t have too many of them),
  • for all of these buildings, fetching in one row all the relevant countries and then all the relevant cities,
  • sorting the original list of building according to the names of the countries and cities fetched,
  • displaying only the 20 first.

On the way, you’ll also need to use field selection.

However, field selection/filtering comes with its issue as well. Indeed, in the given example, you may have to first work on all buildings with filtered out fields, but then for the 20 remaining you may have to fetch them fully, adding one more query on the way.

Overall, the improvements to tackle the lack of joins are quite something to build, use and maintain properly. Furthermore, in the end, the number of queries might still be too important, depending on the use case.

In the end, using only documents and references won’t always fit. Actually, you need to de-normalize your documents to embed the relevant part of the referenced documents. With the list of buildings example, it would most probably mean having the country and city names stored with each building. As such ordering and limiting can be done in one query.

However, all this now de-normalized data requires special cares. If the referenced city name changes, you would like the de-normalized names in building collection to reflect this, isn’t it? So, how to do it?

Well, first of all, that’s where, IMHO, mongoDB documentation lacks by quite some extend. The documentation and « company line » is fairly easy. One can sum it up in 4 words: « you should rather embed », as seen in their Schema design page. If you’re to de-normalize, you’re on your own. From what I got/read, they didn’t provide more hints on how to proceed there because it’s a client side matter with many options. Yet, personally, I would have love at least some presentation of the main ones. No guidance at all is really not enough.

Currently, I see these options:

  • ad hoc de normalization support: when saving/updating/deleting some Foo entity, one explicitly goes at linked documents and updates there the de normalized data. Since synchronous when saving, this solution is only possible on small scales. It is usually the first approach.
  • self made tools for update propagation: you somehow have some way of declaring that the Bar entities contain de normalized content of the Foo entities. The tools in turn take care of propagating changes. This approach scales better than the first one. It works well as long as no complex logic is needed to build the de normalized content. In this case, one has to switch to the 3rd option which follows. Furthermore, one may want to avoid polluting some documents with plenty of use cases specific de normalized data. Again, the 3rd option might be the way to go.
  • pre aggregation for some specific query/views. The idea there is to build a document for a given query/view in order to fulfill it in one query. The document’s content is then highly specific and efficient. Tools are still required to maintain it and likely these pre aggregated query/view can do with different update delays (some might be ok with night batch for example).

The 2 last options need some sort of modification/insertion/deletion propagation. Ideally, some kind of trigger would be welcomed, but they aren’t available in mongoDB. It means you’ve to do it yourself application server side.

Regarding tooling for de-normalization, you’re on your own as well. As said before, DBRefs is the only « tool » provided. You won’t get a database provided list of documents referencing some other one. There’s no tool to propagate update or handled deletion.

While not impossible to achieve, de-normalization handling requires skills and time to do right. This extra work on the client side isn’t common in database work. Having to do such a update/deletion propagation mechanism is not trivial, especially with the limited toolset available.

While not casting a shadow over mongoDB as a whole, the complexity of this extra work shouldn’t be underestimated . You should really plan to develop your de-normalization tools early and test them thoroughly to know how it performs and what you can expect from them. Same stands for the improvements (first/second level cache, lazy loading, collect and then fetch strategy) you may choose. And don’t forget mongoDB – The bad, because some other current limitations of mongoDB may bit you as well: better find them before having to deploy some update propagation mechanism in a hurry for a business critical reason!

No transaction & limited ACID

Due to the lack of transaction, mongoDB provides limited support for ACID:

  • atomicity applies only for changes on one document. If your update batch affecting 10 documents fails somewhere, some of the document will be updated but not the others. Rollbacking the already changes is left to the application server.
  • consistency isn’t present either. A long running update could see new documents inserted somewhere in the collection and not affect them. A failed batch update leaves the database in a inconsistent as long as some one doesn’t clean it.
  • finally, isolation is not present at all. Ongoing updates can be seen in between of their execution by other operations.

Some implementations and API choices privileging performances can also have unexpected behaviors. For example, when listing all the documents of a collection, having twice the same is possible with the default settings.

To me, usually, transaction implies mostly two things:

  • a certain level of atomicity of multiples operations: you can make sure some operations won’t be seen before their are fully completed, avoiding stale data to be taken in account anywhere,
  • an « easy » safety net: without thinking of it, if one operation of a transaction fails then all of them will be roll backed. It comes « out of the box ».

For the atomicity part, while the documentation provides some workarounds, namely through a two phase commit approach, this approach comes with its price: complexity and ad hoc aspect of it, since it needs to be done each time you need such 2 phase commit). This solution also is limited to update of documents of the same collection (and queries not involving other collection neither).

IMHO, mongoDB simply doesn’t fit if you have many update involving related documents, like the classic Account transfer example. And while having transaction and some ACID properties for sure doesn’t come for cheap in RDBMs neither, it can still be done there when needed, and without this many hoop jumping. Actually, having read the two phase commit page, I almost feel compelled of saying that transactions are possible even in high load environment. Indeed, I know of some finance company having transactions with read committed isolation level for write on their DB2, even for the tables handling stock exchanges back office operations, so with a hell of a lot of transactions. Sure, it ain’t be cheap. But possible.

Regarding the safety net part, it’s still somehow doable. Indeed, with the proper write concern. Indeed, the SAFE write concern makes db operation synchronous and throws exception in case of issue. As such, one can and should care for this application server side. In case of multiple operations in a row, one should then handle roll backing them, manually. However, while possible, this isn’t always easy. Indeed, one doesn’t necessarily know which documents where actually updated, in the failing operation or some before. As such, properly roll backing doesn’t come for free, and might even, depending on the use case, be fairly hard to achieve.

Updates

On the 27/05/2011: enumeration of the possible de normalization strategies.

On the 06/06/2011: introduction of the part about the isolation and safety net aspects of transactions.

Étiquettes : , , ,
%d blogueurs aiment cette page :