Pérennité des données sous windows phone

ou “comment éviter la corruption des données”

Une chose très importante dans une application mobile est la pérénnité des données. Quoi de plus frustrant pour un utilisateur qu’une application qui n’arrive plus à se lancer à cause d’un fichier de configuration corrompu. Et cela peut arriver assez facilement sous windows phone :

  • l’utilisateur éteint son téléphone pendant que vous sauvegardez vos données (ou plus de batterie)
  • Une exception se déclenche pendant la sauvegarde
  • On a dépassé les 15 secondes autorisées par windows phone lors de la fermeture de l’application
  • On a dépassé les 20 secondes du periodic agent

mais encore plus gênant, cela peut arriver car vous n’avez pas assez sécurisé vos données.

Nous allons donc voir dans cet article comment sauvegarder vos données en minimisant au maximum les corruptions.

Utiliser les using

Une de premier conseil est d’utiliser et même d’abuser des using pour tout ce qui concerne la manipulation de l’isolated storage.

using (var isf = IsolatedStorageFile.GetUserStoreForApplication())
{
using (IsolatedStorageFileStream fs = isf.OpenFile("settings.xml", System.IO.FileMode.Create))
{
...
}
}

Cela va permettre d’automatiser implicitement la fermeture et le “flush” de votre fichier, vous aurez ainsi aucun risque d’oublier de fermer votre fichier.

Sérialisation

Il ne faut jamais sérialiser des données directement dans un fichier de l’isolated Storage, si vous avez beaucoup de données, celle-ci va prendre quelques secondes, et pendant ces quelques secondes votre fichier est vulnérable et risque d’être corrompu si l’écrire s’interrompt.

Pire, si la sérialisation lance une exception, votre fichier sera corrompu et illisible par la suite. Lorsque vous tenterez de désérialiser les données de votre fichier au prochain lancement, une exception sera lancé et votre application risque de planter si vous n’avez pas fait attention. Il est donc fortement déconseillé d’écrire :

XmlSerializer ser = new XmlSerializer(typeof(Settings));
using (IsolatedStorageFileStream fs = isf.OpenFile("settings.xml", System.IO.FileMode.Open))
{
ser.Serialize(fs, mydata);
}

Pour éviter cela, je conseille fortement de minimiser au maximum le temps d’écriture du fichier. Pour cela, on va écrire nos données en deux temps :

  • Sérialisation des données dans la RAM
  • Copie de la RAM vers l’isolated storage

Le fichier est alors beaucoup moins longtemps vulnérable et les corrumptions seront donc plus rares.

On peut donc utiliser un MemoryStream qui va nous servir de mémoire tampon :

//étape 1 : la sérialisation
var mem = new MemoryStream();
XmlSerializer ser = new XmlSerializer(typeof(Settings));
ser.Serialize(mem, this);
mem.Seek(0, SeekOrigin.Begin);

//étape 2 : le transfert dans l'isolated storage
using (var isf = IsolatedStorageFile.GetUserStoreForApplication())
{
using (IsolatedStorageFileStream fs = isf.OpenFile("settings.xml", System.IO.FileMode.Create))
{
mem.CopyTo(fs);
}
}

La copie de sauvegarde

Grâce à l’étape précédente, on a minimisé nos risques de corrumption, mais on les a pas pour autant totalement supprimés. Une méthode simple et efficace est de toujours garder une copie de sauvegarde des anciennes données au cas où les nouvelles sont corrompues.

Il suffit de créer une copie dans l’isolated storage de notre fichier lorsque l’on est certain qu’il est viable. Pour s’assurer qu’il est viable, je conseille de créer ce backup après la désérialisation du fichier, qui intervient souvent au lancement de l’application.

Pourquoi faire cela au lancement et non à la fermeture de l’application ?

Premièrement car on ne risque pas que le processus se coupe car on a dépassé les 15 secondes autorisées par le système, si vous faites cela bien (c’est à dire pas dans le thread UI), cela n’aura aucun impact sur vos performance. De plus c’est au lancement de l’application que l’on désérialise le fichier d’origine, c’est donc le meilleur endroit pour s’assurer que celui-ci est viable.

Afin d’optimiser le temps d’exécution, nous allons utiliser la même instance du fichier pour désérialiser les données pour notre application et pour réaliser le backup :

var ser = new XmlSerializer(typeof(Settings));
using (IsolatedStorageFileStream fs = isf.OpenFile("settings.xml", System.IO.FileMode.Open))
{
obj = ser.Deserialize(fs);

//If successfully deserialized, initialize data object variable with it
_instance = obj as Settings;
if (_instance != null)
{
//if data is ok, then create backup
using (IsolatedStorageFileStream fsbackup = isf.OpenFile("settings.xml.bak",System.IO.FileMode.Create))
{
fs.Seek(0, SeekOrigin.Begin);
fs.CopyTo(fsbackup);
}
return _instance;
}
}

Pour compléter cela, il faut penser à lire le fichier backup si le fichier d’origine est corrompu, entourons donc le code précédent d’un try..catch et géront la lecture de fichier backup dans le catch

object obj = null;
XmlSerializer ser = new XmlSerializer(typeof(Settings));
try
{
using (var isf = IsolatedStorageFile.GetUserStoreForApplication())
{
if (isf.FileExists("settings.xml"))
{
using (IsolatedStorageFileStream fs = isf.OpenFile("settings.xml", System.IO.FileMode.Open))
{
obj = ser.Deserialize(fs);

_instance = obj as Settings;
if (_instance != null)
{
using (IsolatedStorageFileStream fsbackup = isf.OpenFile("settings.xml.bak", System.IO.FileMode.Create))
{
fs.Seek(0, SeekOrigin.Begin);
fs.CopyTo(fsbackup);
}
return _instance;
}
}
}
}
}
catch
{
using (var isf = IsolatedStorageFile.GetUserStoreForApplication())
{
if (isf.FileExists("settings.xml.bak"))
{
using (IsolatedStorageFileStream fs = isf.OpenFile("settings.xml.bak", System.IO.FileMode.Open))
{
obj = ser.Deserialize(fs);

_instance = obj as Settings;
if (_instance != null)
return _instance;
}
}
}
}

Attention aux accès concurrents

Une chose importante encore pour garder l’intégrité de vos données est de faire attention aux accès concurrents. Si deux processus ou deux threads tentent d’écrire en même temps des données dans le même fichier, il est très fortement probable que vos données ne soient pas valides après cela.

Pour empêcher cela, on peut utiliser un verrou (lock) en déclarant un objet “verrou” et en l’utilisant avec l’instruction lock(..)

exemple :

private object _lockObject=new Object();

public void SaveData()
{
lock(_lockobject)
{
using (var isf = IsolatedStorageFile.GetUserStoreForApplication())
{
if (isf.FileExists("settings.xml"))
{
...
}
}
}
}

Grâce à cela, on est certain qu’un seul thread pourra accéder aux instructions du lock à un moment donnée.

A savoir, l’instruction lock utilise en fait la classe Monitor, les deux sont donc similaires.

Prenons un exemple :

  • le thread A et B appellent la méthode SaveData
  • le thread A va s’emparer du verrou
  • le thread B va tenter de s’emparer du verrou, mais en vain, il se mettra alors en pause
  • le thread A va effectuer sa sauvegarde dans l’isolated storage puis va libérer le verrou
  • le thread B va se réveiller et va s’emparer du verrou
  • le thread B va effectuer sa sauvegarde dans l’isolated storage puis va libérer le verrou

On résoud ainsi les problématiques d’accès concurrents… enfin presque, car le verrou résoud le problème d’accès concurrent entre thread, mais pas entre processus ! En effet, chaque processus aura une instance différente du verrou, les accès seront verrouillés à l’intérieur des processus (dans leurs threads) mais pas entre processus.

Mais vu que les applications sont sandboxées et que seule l’application peut accéder à l’isolated storage, comment peut-on avoir plusieurs processus qui accèdent à notre fichier ?

Accès concurrents entre processus

Mango a ajouté la notion d’agent dans windows phone, parmis eux, on peut trouver les periodic agent, les ressource intensive agent, etc… Arrêtons nous sur le plus utilisé : le periodic agent.

Lorsqu’une application active un periodic agent, un processus va se lancer toutes les 30 minutes pendant 20 secondes pour exécuter une tâche. Une idée reçue est que l’agent se lance uniquement lorsque l’application n’est pas lancée, ce qui est faux et tant bien même, on pourrait toutefois lancer l’application alors qu’un agent est en train de faire sa tâche.

Dans ce cas de figure, on peut donc avoir deux processus qui accède en même temps au fichier, même si la probabilité est faible si on la multiplie par le nombre d’utilisateurs, celle-ci devient fortement probable.

Pour corriger cela, il faut remplacer le verrou par un mutex qui lui gère les exclusions mutuelles entre processus (et donc thread).

Création du mutex :

public static Mutex _mutex = new Mutex(false, "MyAppSettingsMutex");

Ici le false permet d’indiquer que le verrou n’est pas vérouillé à la création, un processus peut donc s’en emparer et le verrouiller.

Pour prendre le mutex :

_mutex.WaitOne();

Si ce dernier est déjà pris, le thread se mettra en pause jusqu’au relâchement du verrou par un autre thread/process.

_mutex.ReleaseMutex();

Cette commande permet de relâcher le verrou, à ne surtout pas oublier !

Cela nous donne donc au final :

public static Mutex _mutex = new Mutex(false, "FriendTrackerSettingsMutex");

private static Settings _instance;
public static Settings Instance
{
get
{
_mutex.WaitOne();

if (_instance != null)
{
_mutex.ReleaseMutex();
return _instance;
}

object obj = null;
XmlSerializer ser = new XmlSerializer(typeof(Settings));

try
{
using (var isf = IsolatedStorageFile.GetUserStoreForApplication())
{
if (isf.FileExists("settings.xml"))
{
using (IsolatedStorageFileStream fs = isf.OpenFile("settings.xml", System.IO.FileMode.Open))
{
obj = ser.Deserialize(fs);

//If successfully deserialized, initialize data object variable with it
_instance = obj as Settings;
if (_instance != null)
{
_mutex.ReleaseMutex();

using (IsolatedStorageFileStream fsbackup = isf.OpenFile("settings.xml.bak", System.IO.FileMode.Create))
{
fs.Seek(0, SeekOrigin.Begin);
fs.CopyTo(fsbackup);
}

return _instance;
}
}
}
}
}
catch
{

using (var isf = IsolatedStorageFile.GetUserStoreForApplication())
{
if (isf.FileExists("settings.xml.bak"))
{
using (IsolatedStorageFileStream fs = isf.OpenFile("settings.xml.bak", System.IO.FileMode.Open))
{
obj = ser.Deserialize(fs);

//If successfully deserialized, initialize data object variable with it
_instance = obj as Settings;
if (_instance != null)
{
_mutex.ReleaseMutex();
return _instance;
}
}
}
}
}

//create new setting
var res = new Settings();

_instance = res;
_mutex.ReleaseMutex();
return res;

}
}

et pour la sauvegarde


public void Save()
{
_mutex.WaitOne();
using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
{

using (IsolatedStorageFileStream fs = isf.CreateFile("settings.xml"))

{
try
{
XmlSerializer ser = new XmlSerializer(typeof(Settings));
ser.Serialize(fs, this);

}
catch (Exception)
{
}
}

}

_mutex.ReleaseMutex();
}

Et autres stockages ?

Il existe deux autres façons de sauvegarder des données sous windows phone : SQL CE et ApplicationSettings.

SQL CE

Au niveau intégrité de l’information, SQL CE est plutôt bien conçu, la base de données possédant déjà un mutex, il n’est donc pas nécessaire de le gérer dans notre app.

Toutefois, même si le cas est extrêment rare, il peut être intéressant de réaliser un back-up du fichier sdf, correspondant à la base de données, qui pourra remplacer le fichier d’origine au besoin.

ApplicationSettings

C’est la méthode la plus simple pour stocker des données pérennes mais pas la plus fiable. Pour résumer, ApplicationSettings gère lui même la sauvegarde et le chargement des données, ceci dit, il ne le fait pas forcément de façon process-safe. Il y a en effet pas de gestion de mutex et il est impossible de le gérer manuellement car on a pas forcément toujours la main sur la sauvegarde des données.

Même si on peut l’appeler explicitement, la fonction Save peut être appelé implicitement, lorsque l’on sort de l’application par exemple, il est donc impossible d’ajouter un mutex avant l’appel. De plus, il n’y a pas de gestion de backup dans applicationSettings.

Bon point toutefois, si le fichier __ApplicationSettings est corrompu, le système l’ignorera et ne lancera pas d’exception.

Je conseillerais donc soit d’éviter d’utiliser ApplicationSettings, soit de l’utiliser pour des données non critique pour le chargement de l’application.

 

Et pour Windows 8 ?

On a les même problématiques mais avec des usages différents et il se trouve que Stéphanie Hertrich, évangéliste pour Microsoft, a blogué en même que moi sans qu’on se concerte :D Je vous invite donc à lire son post :

http://blogs.msdn.com/b/stephe/archive/2012/06/11/windows-8-et-async-await-attention-aux-acc-232-s-fichiers.aspx