Aujourd’hui, je vais répondre à un problème récurrent concernant les synchronisations d’un serveur SQL SERVER 2005 (2008) avec le change tracking activé.
D’où vient cette erreur ?
De temps en temps, votre synchronisation échoue et vous renvoie l’erreur suivante :
SQL Server Change Tracking has cleaned up tracking information
for table Client.
To recover from this error,
the client must reinitialize its local database and try again'
Le problème vient de la période de rétention des données (notamment supprimées) sur le serveur :
Supposons que vous n’ayez pas synchronisé votre application pendant une période de 7 jours. Il se peut donc que certaines données à supprimer sur votre application soient puremment et simplement non disponibles sur le serveur car celui ci a nettoyer ces données.
Votre application se retrouverait donc non correctement synchronisée.
Sync Services envoie avant chaque synchronisation, l’ancre de dernière synchronisation, une sorte de “marque” qui enregistre votre dernière synchronisation. Il stocke cette information et l’envoie via le paramètre @sync_last_received_anchor.
A chaque fin de synchronisation, Sync Services récupère une nouvelle ancre générée via l’instruction :
Select @sync_new_received_anchor = CHANGE_TRACKING_CURRENT_VERSION()
Et avant chaque synchronisation il demande au serveur de vérifier cette fameuse ancre. Si celle-ci est incorrecte, une exception SqlServer est levée :
IF CHANGE_TRACKING_MIN_VALID_VERSION(object_id(N'dbo.Client')) > @sync_last_received_anchor
RAISERROR (N'SQL Server Change Tracking has cleaned up tracking information for table ''%s''.
To recover from this error, the client must reinitialize its local database and try again',
16,
3,
N'dbo.Client')
Solution
J’ai une application de type Synchronisation via un service WCF.
Voici un schéma de la solution de départ :
Je suis donc dans une solution relativement classique, utilisant WCF pour se connecter au serveur.
Voici un schéma récapitulatif de l’architecture de mes projets sous VS :
L’erreur provient du Serveur SQL. C’est lui qui connait la dernière date de synchronisation possible et la dernière date de synchronisation du client en cours.
Il va nous falloir “catcher” l’erreur et la renvoyer au client, lui demandant de resynchroniser totalement son application.
Voici l’ordre d’exécution:
- Tentative de synchronisation du client. Envoie des infos nécessaires au serveur
- Récupération coté serveur de la demande. Levé de l’exception, pour cause de “ça fait trop longtemps que t’es pas venu te synchroniser vilain”. Renvoie de l’erreur au client
- Récupération de l’erreur coté client. Nettoyage de la base de donnée locale, en vue d’une resynchro complète. Demande de resynchro.
- Récupération coté serveur d’une demande de synchro complète. Envoie des données au client
Coté Serveur
Premièrement, il nous faut récupérer l’erreur coté client. Il est donc important d’indiquer à WCF que les erreurs doivent être remontées sur le client.
Je marque donc la classe héritant de mon interface avec un ServiceBehavior particulier
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public partial class DonutSyncSyncService : object, IDonutSyncSyncContract
{
private DonutSyncServerSyncProvider _serverSyncProvider;
public DonutSyncSyncService()
public virtual SyncContext ApplyChanges(SyncGroupMetadata groupMetadata, DataSet dataSet, SyncSession syncSession)
public virtual SyncContext GetChanges(SyncGroupMetadata groupMetadata, SyncSession syncSession)
public virtual SyncSchema GetSchema(Collection<string> tableNames, SyncSession syncSession)
public virtual SyncServerInfo GetServerInfo(SyncSession syncSession)
}
Nous allons maintenant récupérer l’erreur générée par SQL SERVER :
Dans chaque corps de méthode (ApplyChanges, GetChanges, GetSchema, GetServerInfo) je rajoute le code suivant (je prends pour exemple ApplyChanges, le code est indentique pour la partie Catch, sur les autres méthodes)
public virtual SyncContext ApplyChanges(SyncGroupMetadata groupMetadata, DataSet dataSet, SyncSession syncSession)
{
try
{
return this._serverSyncProvider.ApplyChanges(groupMetadata, dataSet, syncSession);
}
catch (SyncException se)
{
if (se.ErrorNumber == SyncErrorNumber.StoreException)
{
SqlException sqlExcept = se.InnerException as SqlException;
if (sqlExcept != null && sqlExcept.Class == 16 && sqlExcept.State == 3)
{
throw new ApplicationException("ChangeTrackingCleanedUp", se);
}
}
throw se;
}
catch (Exception ex)
{
throw ex;
}
}
Vous noterez que pour récupérer l’erreur correcte, je vérifie 3 choses :
- L’erreur est une SyncException : C’est bien une erreur générée par Sync Services.
- L’innerException est une SqlException : C’est au départ une erreur levée par Sql Server
- La Class et le State de l’erreur sont respectivement 16 et 3 (données générées par le RAISERROR des instructions de synchronisations)
Une fois récupérée, je génère une erreur simple à comprendre coté client, coté le mot clé “ChangeTrackingCleanUp”.
Coté Client
Il va me falloir, coté client, récupérer cette erreur et la traiter correctement:
- Récupérer l’erreur contenue dans une CommunicationException
- Récupérer le message indiquant “ChangeTrackingCleanUp”
- Réinitialiser la base de donnée cliente pour une synchro totale.
- Relancer la synchro (aprés demande ou non à l’utilisateur)
Comment marquer la base de données locale pour une synchro Totale ?
- Vous écrasez la base de donnée locale
- Vous effacez les informations de synchronisations de chaque table
Dans le premier cas, vous perdez toutes les informations des tables. Pas grave me direz vous, si vous refaites une synchro totale. SAUF si votre utilisateur a saisi des données supplémentaires entre temps. Dans ce cas là elle sont perdues.
Dans le deuxième cas, les tables sont marquées pour synchro totale, mais les dernière modifications et insertions sont tout de même envoyées au serveur.
Comment marquer une table pour synchro complète . En indiquant sont LastReceivedAnchor à null:
sqlCeProvider.SetTableReceivedAnchor(st.TableName, new SyncAnchor());
Je me suis fait une petite méthode qui lance une synchro avec réinitialisation ou non des tables, comme suit :
public SyncStatistics Synchronize(bool reinit)
{
if (!reinit)
return base.Synchronize();
SqlCeClientSyncProvider sqlCeProvider;
sqlCeProvider = (SqlCeClientSyncProvider)this.LocalProvider;
foreach (SyncTable st in this.Configuration.SyncTables)
{
if (st.SyncDirection != SyncDirection.Snapshot)
{
sqlCeProvider.SetTableReceivedAnchor(st.TableName, new SyncAnchor());
}
}
return base.Synchronize();
}
Il ne reste plus qu’à lancer la synchro, vérifier les erreurs et relancer si nécessaire :
DonutSyncSyncContractClient proxy = new DonutSyncSyncContractClient("WSHttpBinding_IDonutSyncSyncContract");
DonutSyncSyncAgent myAgent = new DonutSyncSyncAgent(proxy);
SyncParameter param = new SyncParameter("@OwnerEmployeId", DonutSettings.Default.EmployeId);
myAgent.Configuration.SyncParameters.Add(param);
try
{
var stats = myAgent.Synchronize(false);
}
catch (TargetInvocationException ex)
{
if (ex.InnerException != null && ex.InnerException.Message == "ChangeTrackingCleanedUp")
{
try
{
var stats = myAgent.Synchronize(true);
}
catch
{
// Serious Error need to be catched here
}
}
}
Voilà votre application est resynchronisée correctement.
Bonne synchro !

Sync Fx