Quantcast
Channel: kerrubin's blog » ASP.Net
Viewing all articles
Browse latest Browse all 11

CallContext et migration de Framework

$
0
0

Récemment, j’ai été impliqué sur plusieurs projets (c’est le bordel niveau CRA, d’ailleurs ^^).
L’un de ces projets est la migration de Framework : du 2.0 vers le 4.0.

Dans le billet Legacy Applications, j’expliquais mon avis sur le bond technologique, donc je n’y reviendrais pas ici.

Nous avons d’abord eu une démarche d’identifier les Breaking Changes – autrement dit les choses qui deviennent obsolètes ou qui disparaissent – avant de réellement faire la migration.

C’est une bonne chose.

Mais…eh bien oui, il y a toujours un mais.
Mais, donc, il faut bien connaître l’applicatif, bien connaître le code.
Ce qui n’était pas réellement notre (mon, du moins) cas.
Parce que, au final, si on prend la liste des breaking changes, soit on la parcours point par point (ce qui peut s’avérer assez long), soit…on la garde sous le coude.

Mais surtout, il y a des choses qui ne sont pas forcément présentes dans les breaking changes et qui peuvent occasionner des drames (n’ayons pas peur des grands mots ! :) ).

Donc, dans ce billet, on va voir le Grand Méchant (avec majuscules, s’il vous plait) CallContext.

 

Qu’est ce que le CallContext ?

 
La question est pertinente.

Le CallContext (System.Runtime.Remoting.Messaging) permet de stocker des informations dans une collection, il est dédié à un processus.

En pratique, c’est quoi ?
La classe CallContext est une classe scellée (et sérialisable) possédant quatre méthodes statiques (dans le cadre de ce qui nous intéresse ici) :

/// <summary>Récupère un objet avec le nom spécifié à partir du contexte d'appel logique.</summary>
/// <returns>Objet dans le contexte d'appel logique associé au nom spécifié.</returns>
/// <param name="name">Nom de l'élément dans le contexte d'appel logique. </param>
/// <exception cref="T:System.Security.SecurityException">L'appelant immédiat n'a pas d'autorisation d'accès à l'infrastructure. </exception>
[SecurityCritical]
public static object LogicalGetData(string name)
{
	return Thread.CurrentThread.GetExecutionContextReader().LogicalCallContext.GetData(name);
}

/// <summary>Récupère un objet portant le nom spécifié de <see cref="T:System.Runtime.Remoting.Messaging.CallContext" />.</summary>
/// <returns>Objet dans le contexte d'appel associé au nom spécifié.</returns>
/// <param name="name">Nom de l'élément dans le contexte d'appel. </param>
/// <exception cref="T:System.Security.SecurityException">L'appelant immédiat n'a pas d'autorisation d'accès à l'infrastructure. </exception>
/// <PermissionSet>
///   <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="Infrastructure" />
/// </PermissionSet>
[SecurityCritical]
public static object GetData(string name)
{
	object obj = CallContext.LogicalGetData(name);
	if (obj == null)
	{
		return CallContext.IllogicalGetData(name);
	}
	return obj;
}

/// <summary>Stocke un objet donné et l'associe au nom spécifié.</summary>
/// <param name="name">Nom auquel associer le nouvel élément du contexte d'appel. </param>
/// <param name="data">Objet à stocker dans le contexte d'appel. </param>
/// <exception cref="T:System.Security.SecurityException">L'appelant immédiat n'a pas d'autorisation d'accès à l'infrastructure. </exception>
/// <PermissionSet>
///   <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="Infrastructure" />
/// </PermissionSet>
[SecurityCritical]
public static void SetData(string name, object data)
{
	if (data is ILogicalThreadAffinative)
	{
		CallContext.LogicalSetData(name, data);
		return;
	}
	ExecutionContext mutableExecutionContext = Thread.CurrentThread.GetMutableExecutionContext();
	mutableExecutionContext.LogicalCallContext.FreeNamedDataSlot(name);
	mutableExecutionContext.IllogicalCallContext.SetData(name, data);
}

/// <summary>Stocke un objet donné dans le contexte d'appel logique et l'associe au nom spécifié.</summary>
/// <param name="name">Nom auquel associer le nouvel élément du contexte d'appel logique. </param>
/// <param name="data">Objet à stocker dans le contexte d'appel logique. </param>
/// <exception cref="T:System.Security.SecurityException">L'appelant immédiat n'a pas d'autorisation d'accès à l'infrastructure. </exception>
[SecurityCritical]
public static void LogicalSetData(string name, object data)
{
	ExecutionContext mutableExecutionContext = Thread.CurrentThread.GetMutableExecutionContext();
	mutableExecutionContext.IllogicalCallContext.FreeNamedDataSlot(name);
	mutableExecutionContext.LogicalCallContext.SetData(name, data);
}

En creusant un peu, on peut donc voir que cette classe s’appuie sur la classe LogicalCallContext (System.Threading.ExecutionContext possède une propriété de ce type ; System.Runtime.Remoting.Messaging, implémentant ISerializable et ICloneable).
Cette classe possède une collection :

		private Hashtable m_Datastore;
		private Hashtable Datastore
		{
			get
			{
				if (this.m_Datastore == null)
				{
					this.m_Datastore = new Hashtable();
				}
				return this.m_Datastore;
			}
		}

Donc, au final, nos informations vont se retrouver dans cette hashtable.

Ça, c’est pour le code du Framework (C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll).

 

Quel est donc le problème ?

 

Si vous saviez…

Dans la situation actuelle, nous utilisons du remoting pour communiquer entre un client et un serveur.
L’important n’est pas le serveur, c’est le client. De l’ASP.Net, donc IIS (7.5 pour nous).

Et dans le cas du remoting, toutes les données stockées dans le CallContext, implémentant l’interface ILogicalThreadAffinative et étant sérialisable seront propagées dans les différents AppDomain.
Là, tout va bien.

Par contre, le CallContext est spécifique à un et un seul processus (comme on peut très bien le voir dans le code).
C’est-à-dire qu’il n’est pas partagé entre différents processus (y compris au sein du même AppDomain).

Hors, au niveau de IIS, il y a ce que l’on appelle le Thread Agility.
Le principe est assez simple : IIS dispose d’un nombre maximal de processus pouvant être créés et, grosso modo, s’il considère qu’une requête (objet Request) peut être servie plus rapidement sur un autre thread, il va changer de thread.
Tout l’objet Request va être copié du thread de départ vers le thread d’arrivé.

Mais pas le CallContext, donc.

Le fait de changer de processus en cours de requête dépend de la charge, du nombre de processus disponibles (le nombre de cœurs du CPU ayant une influence), s’il y a du I/O ou encore des appels asynchrones (exemple : Ajax) et probablement encore d’autres "petites" choses.
Dans mon cas, le problème semblait bien plus présent en debug sur ma machine que sur le serveur de recette.

Avec IIS 7.0 et .Net 2.0, le nombre par défaut est de 12 (configurable dans le registre : HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ASP.NET\2.0.50727.0, DWORD MaxConcurrentRequestsPerCPU, non présent par défaut).
Avec le .Net 4.0, le nombre par défaut est de 5.000.

Cependant, avec le Framework 2.0, le problème ne se posait pas sur mon poste, en développement.
Avec le Framework 4.0, le problème de CallContext à null se posait.
Mais avec le DWORD MaxConcurrentRequestsPerCPU (dans sa version 4.0) à 12, le problème se posait toujours.
Donc, ce n’est potentiellement pas cela qui va nous arranger…

 

Conclusion

 
Déjà, ma conclusion basique serait : le remoting caymal.
Mais c’est pas très constructif.

Donc, le remoting, caymal pour les clients ASP.Net (bon, j’avoue, c’est pas super plus constructif).

Non seulement le remoting a quand même été pas mal rendu obsolète avec l’ajout de WCF, mais en plus, cela (du moins le CallContext) peut occasionner des problèmes qui peuvent se produire. Ou pas.
Ça dépend des fois. Du sens du vent, de l’âge du Capitaine (42 pour info) et ainsi de suite.

En somme : ne pas faire confiance au CallContext permettra de s’éviter un bon mal de crâne de plus (et ça, c’est toujours bon à prendre).

Dans notre cas, la solution a été d’encapsuler l’appel au remoting dans une autre méthode qui force l’alimentation du CallContext.
C’est un peu brutal comme méthode, mais ça à e mérite de couvrir tous les problèmes potentiels (façon couverture écossaise avec des franges).

 

Pour en savoir plus

 

Tout ce billet ne s’est pas construit tout seul, ni sur ma simple bonne fois :)

Donc, voici quelques références :



Viewing all articles
Browse latest Browse all 11

Latest Images

Trending Articles





Latest Images