Avec WF, les poupées russes consomment trop!
Quand on me parle WF4, il y a une remarque qui revient souvent :
Les workflows consomment de la mémoire qu'il ne libèrent jamais.
Après avoir participé à quelques profilages d'applications, j'ai remarqué qu'il s'agissait bien trop souvent d'une erreur de conception que j'ai surnommée "Poupées russes". Le principe est simple, vous avez besoin qu'un workflow lance un second worklow, qui lui même peut lancer un troisième workflow. Ceci sans qu'aucun workflow n'attende la fin de l'exécution de celui qui l'a invoqué (son parent).
L'erreur consiste à penser que les premiers workflows sont finis et donc qu’il n’on plus rien à faire en mémoire. Malheureusement, ce n’est pas le cas. La raison en est toute simple : vous invoquer vos activités de manières séquentielles.
Afin d’imager mes propos, j’ai réalisé un petit schéma. Celui-ci représente le cycle de vie de nos 3 workflows.
On peut très clairement constater que :
- Le workflow1 est présent en mémoire de l’instant T1 à T5.
- Le workflow2 est présent en mémoire de l’instant T2 à T4+le temps pour se terminer.
- Le workflow3 est présent en mémoire de l’instant T3 à T4.
Heureusement, nous sommes en mesure d’invoquer nos workflows de manière asynchrone. Ce qui permet d’avoir une représentation similaire à ceci :
On peut très clairement constater que :
- Le workflow1 est présent en mémoire de l’instant T1 à T2+le temps pour se terminer.
- Le workflow2 est présent en mémoire de l’instant T2 à T3+le temps pour se terminer.
- Le workflow3 est présent en mémoire de l’instant T3 à T4.
Chaque workflow ne sera donc utile en mémoire qu’un temps réduit (sa période d’activité réelle). Le garbage collector pourra donc nous restituer la mémoire utilisée par les workflows 1 et 2 avant que le 3 ne soit terminé.
Contrairement à ce que certain peuvent penser, avec WF4 invoquer un workflow au sein d’un autre n’est pas très compliquer. Afin de présenter l’ensemble des erreurs et approches possible, je vais vous présenter ici différentes approches:
1) Utilisation des workflows dans la définition des workflows parents
Il s’agit là du cas courant, on utilise les workflows 2 et 3 directement au sein de leur workflow parent.
Après exécution, le résultat est sans appel.
On obtient le cas d’une exécution séquentiel où le workflow 1 est présent du début à la fin.
2) Utilisation du WorkflowInvoker
À la place des workflows, on utilise une activité chargée de l’invocation. J’utilise ici le WorkflowInvoker tel que l'on peut le voir le plus soulant avec WF4.
public sealed class Invoke<T> : CodeActivity where T:Activity
{
protected override void Execute(CodeActivityContext context)
{
var workflow = Activator.CreateInstance<T>();
WorkflowInvoker.Invoke(workflow);
}
}
Les workflows sont ensuite modifiés comme ceci :
Malheureusement, à l’exécution on obtient la même sortie que si l’on ne disposait pas de la classe Invoke<T>.
Ceci est parfaitement logique, car la méthode Invoke n’est pas une méthode asynchrone. Le fait de ne pas utiliser le l’ActivityContext des workflows parents n’a donc pas d’impacte.
3) Utilisation du WorkflowApplication
Je modifie le code de l’activité Invoke<T> afin d’utiliser un WorkflowApplication, comme ceci:
public sealed class Invoke<T> : CodeActivity where T:Activity
{
protected override void Execute(CodeActivityContext context)
{
var workflow = Activator.CreateInstance<T>();
var invoker = new WorkflowApplication(workflow);
invoker.Run();
}
}
Ce qui lors de l’exécution donne enfin le résultat escompté :
L’exécution d’un appel asynchrone à une méthode sans attendre la fin de son exécution ne bloque donc pas le workflow parent. L’ActivityContext n’est pas non plus perturbé par cet appel.
NOTE : si vous souhaitez utiliser des variables d’un workflow parent comme argument d’un workflow enfant, il faut juste utiliser l’ActivityContext pour résoudre ces variables avant l’invocation du workflow enfant.
4) Utilisation une instance du WorkflowInvoker
Le WorkflowInvoker dispose d’une méthode asynchrone. Celle-ci est souvent méconnue (la faute aux démonstrations/showroom publiques qui se font toujours avec les méthodes statistiques).
Si on l’utilise dans notre classe Invoke<T> on obtient le code suivant
public sealed class Invoke<T> : CodeActivity where T:Activity
{
protected override void Execute(CodeActivityContext context)
{
var workflow = Activator.CreateInstance<T>();
var invoker = new WorkflowInvoker(workflow);
invoker.InvokeAsync();
}
}
Bien entendu, ceci donne le résultat escompté lors de l’exécution :
Pour résumer
L’invocation asynchrone d’un workflow enfant sans attendre qu’il se termine peut se faire de manière très simple. Il n’est donc pas utile de réinventer la roue sous prétexte que l’on utilise Workflow Foudation