How to: Clean Up Metadata for Collaborative Synchronization (SQL Server)

This topic describes how to clean up metadata for SQL Server and SQL Server Compact databases that are synchronized by using Sync Framework. The code in this topic focuses on the following Sync Framework classes:

For more information about how to run sample code, see "Example Applications in the How to Topics" in Synchronizing SQL Server and SQL Server Compact.

Understanding Metadata Cleanup

Cleanup involves deleting metadata for rows that have been deleted from a base table. Sync Framework uses two kinds of metadata:

  • Table-level metadata that tracks inserts, updates, and deletes for each table that is synchronized.

    There is one row of metadata for each row in the base table. If a row is deleted from the base table and all nodes in all scopes have received it, the metadata row can be safely deleted.

  • Database-level metadata that tracks which changes each node has received from other nodes.

    This metadata is typically stored in one scope table for each node database. Rows in the scope table should never be deleted unless the scope is dropped.

Cleanup is retention-based, which means that metadata that is older than the specified number of days is deleted. For SQL Server databases, use the SqlSyncStoreMetadataCleanup object; and for SQL Server Compact databases, use the SqlCeSyncStoreMetadataCleanup object. Both objects have the same properties and methods.

SQL Server SQL Server Compact Description

PerformCleanup

PerformCleanup

The method that you call from your application to clean up metadata.

RetentionInDays

RetentionInDays

The property that specifies how old change-tracking metadata must be, in days, for that metadata to be deleted when the cleanup method is called.

If a node tries to synchronize changes whose metadata has already been cleaned up, an exception of type DbOutdatedSyncException is thrown. The SyncPeerOutdated event is raised, which provides access to a DbOutdatedEventArgs object. There are two options for handling this event:

  • Set the Action property to PartialSync. This synchronizes the data for which metadata is present, but some deletes are missed.

  • Set the Action property to AbortSync (the default). This ends the synchronization session. The client should be re-initialized in the next synchronization session so that it will have the correct data.

Complete Code Example

The complete code example performs the following steps:

  1. Synchronizes SyncSamplesDb_SqlPeer1 (Node1) and SyncSamplesDb_SqlPeer1 (Node2). Nine rows are uploaded to Node2.

  2. Synchronizes Node2 and SyncSampleClient1.sdf (Node3).

  3. Performs an insert, update, and delete at Node1.

  4. Calls PerformCleanup for metadata that is more than 7 days old at Node1. The PerformCleanup method returns successfully, but no metadata is cleaned up because no deletes have been made at Node1 that are older than 7 days.

    SqlSyncStoreMetadataCleanup metadataCleanup = new SqlSyncStoreMetadataCleanup(serverConn);
    bool cleanupSuccessful; 
    metadataCleanup.RetentionInDays = 7;
    cleanupSuccessful = metadataCleanup.PerformCleanup();
    
    Dim metadataCleanup As New SqlSyncStoreMetadataCleanup(serverConn)
    Dim cleanupSuccessful As Boolean
    metadataCleanup.RetentionInDays = 7
    cleanupSuccessful = metadataCleanup.PerformCleanup()
    
  5. Synchronizes Node1 and Node3, and Node2 and Node3. Synchronization is successful because all relevant metadata is still available at both nodes.

  6. Deletes a row from Node1.

  7. Calls PerformCleanup for all metadata at Node1. The metadata for the delete in the previous step is cleaned up.

    metadataCleanup.RetentionInDays = 0;
    cleanupSuccessful = metadataCleanup.PerformCleanup();
    
    metadataCleanup.RetentionInDays = 0
    cleanupSuccessful = metadataCleanup.PerformCleanup()
    
  8. Attempts to synchronize Node1 and Node3, and Node2 and Node3. Synchronization fails because the synchronization knowledge no longer matches the state of the node. An exception of type DbOutdatedSyncException is thrown.

It is important to clean up only the metadata that is no longer required by other nodes. If the second cleanup had occurred after Node1 had received the delete from Node3, synchronization would have succeeded.

Important

Running the following example code intentionally leaves the sample databases in an inconsistent state. After you run this code, drop the databases and re-create them by executing the first script in Setup Scripts for Database Provider How-to Topics.

// NOTE: Before running this application, run the database sample script that is
// available in the documentation. The script drops and re-creates the tables that 
// are used in the code, and ensures that synchronization objects are dropped so that 
// Sync Framework can re-create them.

using System;
using System.IO;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlServerCe;
using Microsoft.Synchronization;
using Microsoft.Synchronization.Data;
using Microsoft.Synchronization.Data.SqlServer;
using Microsoft.Synchronization.Data.SqlServerCe;

namespace Microsoft.Samples.Synchronization
{
    class Program
    {
        static void Main(string[] args)
        {

            // Create the connections over which provisioning and synchronization
            // are performed. The Utility class handles all functionality that is not
            //directly related to synchronization, such as holding connection 
            //string information and making changes to the server database.
            SqlConnection serverConn = new SqlConnection(Utility.ConnStr_SqlSync_Server);
            SqlConnection clientSqlConn = new SqlConnection(Utility.ConnStr_SqlSync_Client);
            SqlCeConnection clientSqlCe1Conn = new SqlCeConnection(Utility.ConnStr_SqlCeSync1);

            // Create a scope named "customer", and add the Customer and CustomerContact 
            // tables to the scope.
            // GetDescriptionForTable gets the schema of the table, so that tracking 
            // tables and triggers can be created for that table.
            DbSyncScopeDescription scopeDesc = new DbSyncScopeDescription("customer");

            scopeDesc.Tables.Add(
            SqlSyncDescriptionBuilder.GetDescriptionForTable("Sales.Customer", serverConn));

            scopeDesc.Tables.Add(
            SqlSyncDescriptionBuilder.GetDescriptionForTable("Sales.CustomerContact", serverConn));

            // Create a provisioning object for "customer" and specify that
            // base tables should not be created (They already exist in SyncSamplesDb_SqlPeer1).
            SqlSyncScopeProvisioning serverConfig = new SqlSyncScopeProvisioning(scopeDesc);
            serverConfig.SetCreateTableDefault(DbSyncCreationOption.Skip);

            // Configure the scope and change-tracking infrastructure.
            serverConfig.Apply(serverConn);

            // Retrieve scope information from the server and use the schema that is retrieved
            // to provision the SQL Server and SQL Server Compact client databases.           

            // This database already exists on the server.
            DbSyncScopeDescription clientSqlDesc = SqlSyncDescriptionBuilder.GetDescriptionForScope("customer", serverConn);
            SqlSyncScopeProvisioning clientSqlConfig = new SqlSyncScopeProvisioning(clientSqlDesc);
            clientSqlConfig.Apply(clientSqlConn);

            // This database does not yet exist.
            Utility.DeleteAndRecreateCompactDatabase(Utility.ConnStr_SqlCeSync1, true);
            DbSyncScopeDescription clientSqlCeDesc = SqlSyncDescriptionBuilder.GetDescriptionForScope("customer", serverConn);
            SqlCeSyncScopeProvisioning clientSqlCeConfig = new SqlCeSyncScopeProvisioning(clientSqlCeDesc);
            clientSqlCeConfig.Apply(clientSqlCe1Conn);


            // Initial synchronization sessions.
            SampleSyncOrchestrator syncOrchestrator;
            SyncOperationStatistics syncStats;

            // Data is downloaded from the server to the SQL Server client.
            syncOrchestrator = new SampleSyncOrchestrator(
                new SqlSyncProvider("customer", clientSqlConn),
                new SqlSyncProvider("customer", serverConn)
                );
            syncStats = syncOrchestrator.Synchronize();
            syncOrchestrator.DisplayStats(syncStats, "initial");

            // Data is downloaded from the SQL Server client to the 
            // SQL Server Compact client.
            syncOrchestrator = new SampleSyncOrchestrator(
                new SqlCeSyncProvider("customer", clientSqlCe1Conn),
                new SqlSyncProvider("customer", clientSqlConn)
                );
            syncStats = syncOrchestrator.Synchronize();
            syncOrchestrator.DisplayStats(syncStats, "initial");


            // Make changes on the server: 1 insert, 1 update, and 1 delete.
            Utility.MakeDataChangesOnNode(Utility.ConnStr_SqlSync_Server, "Customer");

            SqlSyncStoreMetadataCleanup metadataCleanup = new SqlSyncStoreMetadataCleanup(serverConn);
            bool cleanupSuccessful; 
            metadataCleanup.RetentionInDays = 7;
            cleanupSuccessful = metadataCleanup.PerformCleanup();

            if (cleanupSuccessful == true)
            {
                Console.WriteLine(String.Empty);
                Console.WriteLine("Metadata cleanup ran in the database.");
                Console.WriteLine("Metadata more than 7 days old was deleted.");
            }
            else
            {
                Console.WriteLine("Metadata cleanup failed, most likely due to concurrency issues.");
            }

            
            // Synchronize the three changes.
            syncOrchestrator = new SampleSyncOrchestrator(
                new SqlSyncProvider("customer", clientSqlConn),
                new SqlSyncProvider("customer", serverConn)
                );
            syncStats = syncOrchestrator.Synchronize();
            syncOrchestrator.DisplayStats(syncStats, "subsequent");

            syncOrchestrator = new SampleSyncOrchestrator(
                new SqlSyncProvider("customer", clientSqlConn),
                new SqlCeSyncProvider("customer", clientSqlCe1Conn)
                );
            syncStats = syncOrchestrator.Synchronize();
            syncOrchestrator.DisplayStats(syncStats, "subsequent");


            Utility.MakeDataChangesOnNode(Utility.ConnStr_SqlSync_Server, "CustomerContact");

            metadataCleanup.RetentionInDays = 0;
            cleanupSuccessful = metadataCleanup.PerformCleanup();

            if (cleanupSuccessful == true)
            {
                Console.WriteLine(String.Empty);
                Console.WriteLine("Metadata cleanup ran in the database.");
                Console.WriteLine("All metadata was deleted.");
            }
            else
            {
                Console.WriteLine("Metadata cleanup failed, most likely due to concurrency issues.");
            }

            try
            {

                // Synchronize a final time.
                syncOrchestrator = new SampleSyncOrchestrator(
                    new SqlCeSyncProvider("customer", clientSqlCe1Conn),
                    new SqlSyncProvider("customer", serverConn)
                    );
                syncStats = syncOrchestrator.Synchronize();
                syncOrchestrator.DisplayStats(syncStats, "subsequent");

                syncOrchestrator = new SampleSyncOrchestrator(
                    new SqlSyncProvider("customer", clientSqlConn),
                    new SqlCeSyncProvider("customer", clientSqlCe1Conn)
                    );
                syncStats = syncOrchestrator.Synchronize();
                syncOrchestrator.DisplayStats(syncStats, "subsequent");
            }

            catch (DbOutdatedSyncException ex)
            {

                Console.WriteLine(String.Empty);
                Console.WriteLine("Synchronization failed due to outdated synchronization knowledge,");
                Console.WriteLine("which is expected in this sample application.");
                Console.WriteLine("Drop and recreate the sample databases.");
                Console.WriteLine(String.Empty);
                Console.WriteLine("Outdated Knowledge: " + ex.OutdatedPeerSyncKnowledge.ToString() +
                                  " Clean up knowledge: " + ex.MissingCleanupKnowledge.ToString());
                Console.WriteLine(String.Empty);

            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

            serverConn.Close();
            serverConn.Dispose();
            clientSqlConn.Close();
            clientSqlConn.Dispose();
            clientSqlCe1Conn.Close();
            clientSqlCe1Conn.Dispose();

            Console.Write("\nPress any key to exit.");
            Console.Read();
            
        }

    }

    public class SampleSyncOrchestrator : SyncOrchestrator
    {
        public SampleSyncOrchestrator(RelationalSyncProvider localProvider, RelationalSyncProvider remoteProvider)
        {

            this.LocalProvider = localProvider;
            this.RemoteProvider = remoteProvider;
            this.Direction = SyncDirectionOrder.UploadAndDownload;
        }

        public void DisplayStats(SyncOperationStatistics syncStatistics, string syncType)
        {
            Console.WriteLine(String.Empty);
            if (syncType == "initial")
            {
                Console.WriteLine("****** Initial Synchronization ******");
            }
            else if (syncType == "subsequent")
            {
                Console.WriteLine("***** Subsequent Synchronization ****");
            }

            Console.WriteLine("Start Time: " + syncStatistics.SyncStartTime);
            Console.WriteLine("Total Changes Uploaded: " + syncStatistics.UploadChangesTotal);
            Console.WriteLine("Total Changes Downloaded: " + syncStatistics.DownloadChangesTotal);
            Console.WriteLine("Complete Time: " + syncStatistics.SyncEndTime);
            Console.WriteLine(String.Empty);
        }
    }
}
' NOTE: Before running this application, run the database sample script that is
' available in the documentation. The script drops and re-creates the tables that 
' are used in the code, and ensures that synchronization objects are dropped so that 
' Sync Framework can re-create them.

Imports System
Imports System.IO
Imports System.Text
Imports System.Data
Imports System.Data.SqlClient
Imports System.Data.SqlServerCe
Imports Microsoft.Synchronization
Imports Microsoft.Synchronization.Data
Imports Microsoft.Synchronization.Data.SqlServer
Imports Microsoft.Synchronization.Data.SqlServerCe

Namespace Microsoft.Samples.Synchronization

    Class Program

        Public Shared Sub Main(ByVal args As String())

            ' Create the connections over which provisioning and synchronization 
            ' are performed. The Utility class handles all functionality that is not 
            'directly related to synchronization, such as holding connection 
            'string information and making changes to the server database. 
            Dim serverConn As New SqlConnection(Utility.ConnStr_SqlSync_Server)
            Dim clientSqlConn As New SqlConnection(Utility.ConnStr_SqlSync_Client)
            Dim clientSqlCe1Conn As New SqlCeConnection(Utility.ConnStr_SqlCeSync1)

            ' Create a scope named "customer", and add the Customer and CustomerContact 
            ' tables to the scope. 
            ' GetDescriptionForTable gets the schema of the table, so that tracking 
            ' tables and triggers can be created for that table. 
            Dim scopeDesc As New DbSyncScopeDescription("customer")

            scopeDesc.Tables.Add(SqlSyncDescriptionBuilder.GetDescriptionForTable("Sales.Customer", serverConn))

            scopeDesc.Tables.Add(SqlSyncDescriptionBuilder.GetDescriptionForTable("Sales.CustomerContact", serverConn))

            ' Create a provisioning object for "customer" and specify that 
            ' base tables should not be created (They already exist in SyncSamplesDb_SqlPeer1). 
            Dim serverConfig As New SqlSyncScopeProvisioning(scopeDesc)
            serverConfig.SetCreateTableDefault(DbSyncCreationOption.Skip)

            ' Configure the scope and change-tracking infrastructure. 
            serverConfig.Apply(serverConn)

            ' Retrieve scope information from the server and use the schema that is retrieved 
            ' to provision the SQL Server and SQL Server Compact client databases. 

            ' This database already exists on the server. 
            Dim clientSqlDesc As DbSyncScopeDescription = SqlSyncDescriptionBuilder.GetDescriptionForScope("customer", serverConn)
            Dim clientSqlConfig As New SqlSyncScopeProvisioning(clientSqlDesc)
            clientSqlConfig.Apply(clientSqlConn)

            ' This database does not yet exist. 
            Utility.DeleteAndRecreateCompactDatabase(Utility.ConnStr_SqlCeSync1, True)
            Dim clientSqlCeDesc As DbSyncScopeDescription = SqlSyncDescriptionBuilder.GetDescriptionForScope("customer", serverConn)
            Dim clientSqlCeConfig As New SqlCeSyncScopeProvisioning(clientSqlCeDesc)
            clientSqlCeConfig.Apply(clientSqlCe1Conn)


            ' Initial synchronization sessions. 
            Dim syncOrchestrator As SampleSyncOrchestrator
            Dim syncStats As SyncOperationStatistics

            ' Data is downloaded from the server to the SQL Server client. 
            syncOrchestrator = New SampleSyncOrchestrator( _
                New SqlSyncProvider("customer", clientSqlConn), _
                New SqlSyncProvider("customer", serverConn))
            syncStats = syncOrchestrator.Synchronize()
            syncOrchestrator.DisplayStats(syncStats, "initial")

            ' Data is downloaded from the SQL Server client to the 
            ' SQL Server Compact client. 
            syncOrchestrator = New SampleSyncOrchestrator( _
                New SqlCeSyncProvider("customer", clientSqlCe1Conn), _
                New SqlSyncProvider("customer", clientSqlConn))
            syncStats = syncOrchestrator.Synchronize()
            syncOrchestrator.DisplayStats(syncStats, "initial")


            ' Make changes on the server: 1 insert, 1 update, and 1 delete. 
            Utility.MakeDataChangesOnNode(Utility.ConnStr_SqlSync_Server, "Customer")

            Dim metadataCleanup As New SqlSyncStoreMetadataCleanup(serverConn)
            Dim cleanupSuccessful As Boolean
            metadataCleanup.RetentionInDays = 7
            cleanupSuccessful = metadataCleanup.PerformCleanup()

            If cleanupSuccessful = True Then
                Console.WriteLine([String].Empty)
                Console.WriteLine("Metadata cleanup ran in the database.")
                Console.WriteLine("Metadata more than 7 days old was deleted.")
            Else
                Console.WriteLine("Metadata cleanup failed, most likely due to concurrency issues.")
            End If


            ' Synchronize the three changes. 
            syncOrchestrator = New SampleSyncOrchestrator( _
                New SqlSyncProvider("customer", clientSqlConn), _
                New SqlSyncProvider("customer", serverConn))
            syncStats = syncOrchestrator.Synchronize()
            syncOrchestrator.DisplayStats(syncStats, "subsequent")

            syncOrchestrator = New SampleSyncOrchestrator( _
                New SqlSyncProvider("customer", clientSqlConn), _
                New SqlCeSyncProvider("customer", clientSqlCe1Conn))
            syncStats = syncOrchestrator.Synchronize()
            syncOrchestrator.DisplayStats(syncStats, "subsequent")


            Utility.MakeDataChangesOnNode(Utility.ConnStr_SqlSync_Server, "CustomerContact")

            metadataCleanup.RetentionInDays = 0
            cleanupSuccessful = metadataCleanup.PerformCleanup()

            If cleanupSuccessful = True Then
                Console.WriteLine([String].Empty)
                Console.WriteLine("Metadata cleanup ran in the database.")
                Console.WriteLine("All metadata was deleted.")
            Else
                Console.WriteLine("Metadata cleanup failed, most likely due to concurrency issues.")
            End If

            Try

                ' Synchronize a final time. 
                syncOrchestrator = New SampleSyncOrchestrator( _
                    New SqlCeSyncProvider("customer", clientSqlCe1Conn), _
                    New SqlSyncProvider("customer", serverConn))
                syncStats = syncOrchestrator.Synchronize()
                syncOrchestrator.DisplayStats(syncStats, "subsequent")

                syncOrchestrator = New SampleSyncOrchestrator( _
                    New SqlSyncProvider("customer", clientSqlConn), _
                    New SqlCeSyncProvider("customer", clientSqlCe1Conn))
                syncStats = syncOrchestrator.Synchronize()
                syncOrchestrator.DisplayStats(syncStats, "subsequent")
            Catch ex As DbOutdatedSyncException


                Console.WriteLine([String].Empty)
                Console.WriteLine("Synchronization failed due to outdated synchronization knowledge,")
                Console.WriteLine("which is expected in this sample application.")
                Console.WriteLine("Drop and recreate the sample databases.")
                Console.WriteLine([String].Empty)
                Console.WriteLine(("Outdated Knowledge: " & ex.OutdatedPeerSyncKnowledge.ToString() & " Clean up knowledge: ") + ex.MissingCleanupKnowledge.ToString())

                Console.WriteLine([String].Empty)
            Catch ex As Exception
                Console.WriteLine(ex.Message)
            End Try

            serverConn.Close()
            serverConn.Dispose()
            clientSqlConn.Close()
            clientSqlConn.Dispose()
            clientSqlCe1Conn.Close()
            clientSqlCe1Conn.Dispose()

            Console.Write(vbLf & "Press any key to exit.")

            Console.Read()
        End Sub

    End Class

    Public Class SampleSyncOrchestrator
        Inherits SyncOrchestrator
        Public Sub New(ByVal localProvider As RelationalSyncProvider, ByVal remoteProvider As RelationalSyncProvider)

            Me.LocalProvider = localProvider
            Me.RemoteProvider = remoteProvider
            Me.Direction = SyncDirectionOrder.UploadAndDownload
        End Sub

        Public Sub DisplayStats(ByVal syncStatistics As SyncOperationStatistics, ByVal syncType As String)
            Console.WriteLine([String].Empty)
            If syncType = "initial" Then
                Console.WriteLine("****** Initial Synchronization ******")
            ElseIf syncType = "subsequent" Then
                Console.WriteLine("***** Subsequent Synchronization ****")
            End If

            Console.WriteLine("Start Time: " & syncStatistics.SyncStartTime)
            Console.WriteLine("Total Changes Uploaded: " & syncStatistics.UploadChangesTotal)
            Console.WriteLine("Total Changes Downloaded: " & syncStatistics.DownloadChangesTotal)
            Console.WriteLine("Complete Time: " & syncStatistics.SyncEndTime)
            Console.WriteLine([String].Empty)
        End Sub
    End Class
End Namespace

See Also

Concepts

Synchronizing SQL Server and SQL Server Compact