System.Data.SQLite

Login
This project makes use of Eagle, provided by Mistachkin Systems.
Eagle: Secure Software Automation
Ticket Hash: d4e6100b80c7121ae699da69369c7f46000a46a2
Title: AppDomain unload while database backup pending fails
Status: Closed Type: Incident
Severity: Important Priority: Medium
Subsystem: Connection Resolution: Works_As_Designed
Last Modified: 2016-02-04 18:52:20
Version Found In: 1.0.99.0
User Comments:
anonymous added on 2016-02-02 12:46:33: (text/x-fossil-plain)
I'm using a windows service with different application domains for some application modules. For upgrades, it is necessary to remove these modules from the application at runtime. If a SQLite database backup runs while unloading the application domain, a System.CannotUnloadAppDomainException is always thrown. It seems that a deadlock occurs in the library. After this incident, the backup never completes. And the application domain can never be unloaded.
To reproduce the problem, I wrote a simple test program that uses a 1GB database, so that the backup takes a bit longer.

The test program reproduces the error reliable and always gives the following result:

>Backup started.
>Try to unload application domain.
>Failed to unload application domain.
>System.CannotUnloadAppDomainException: Error while unloading appdomain. (Exception from HRESULT: 0x80131015)
   at System.AppDomain.Unload(AppDomain domain)
   at AppDomainSQLiteTest.Program.Main(String[] args) 

---------
    internal class Program
    {
        private static void Main(string[] args)
        {
            try
            {
                System.AppDomain domain = System.AppDomain.CreateDomain("NewApplicationDomain");

                RemoteObject obj = (RemoteObject)domain.CreateInstanceFromAndUnwrap("RemoteAssembly.dll", "RemoteAssembly.RemoteObject",
                    false, BindingFlags.Default | BindingFlags.CreateInstance | BindingFlags.Instance | BindingFlags.Public, null, null, null, null
                );

                ThreadPool.QueueUserWorkItem(delegate
                {
                    //calling database backup in another application domain
                    obj.BackupDatabase();
                });

                Thread.Sleep(5000);

                Console.WriteLine("Try to unload application domain.");
                AppDomain.Unload(domain);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Failed to unload application domain.");
                Console.WriteLine(ex);
            }

            Console.ReadKey();
        }
	}

   public class RemoteObject : System.MarshalByRefObject
    {
        private string m_DatabaseFilePath; 

        public RemoteObject()
        {
            m_DatabaseFilePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestDatabase.db");
        }

        public void BackupDatabase()
        {
            System.Console.WriteLine("Backup started.");

            SQLiteConnectionStringBuilder destConStr = new SQLiteConnectionStringBuilder(ConnectionString);

            string backupFilePath = destConStr.DataSource + ".bak";
            destConStr.DataSource = backupFilePath;

            try
            {
                //remove existing .bak file
                if (File.Exists(backupFilePath))
                    File.Delete(backupFilePath);
            }
            catch (IOException)
            {
                Console.WriteLine("It was not possible to remove obsolete database .bak file.");
            }

            try
            {
                using (SQLiteConnection dest = new SQLiteConnection(destConStr.ConnectionString))
                {
                    using (SQLiteConnection source = new SQLiteConnection(ConnectionString))
                    {
                        source.Open();
                        dest.Open();
                        source.BackupDatabase(dest, "main", "main", -1, null, 0);
                    }
                }
            }
            catch
            {
                Console.WriteLine("Database backup failed.");

                try
                {
                    //remove incomplete database backup file
                    if (File.Exists(backupFilePath))
                        File.Delete(backupFilePath);
                }
                catch (IOException)
                {
                    Console.WriteLine("It was not possible to remove obsolete database .bak file.");
                }
            }

            System.Console.WriteLine("Backup completed.");
        }

        public string ConnectionString
        {
            get
            {
                SQLiteConnectionStringBuilder conStr = new SQLiteConnectionStringBuilder();
                conStr.DataSource = m_DatabaseFilePath;
                conStr.FailIfMissing = false;
                conStr.JournalMode = SQLiteJournalModeEnum.Wal;
                conStr.DefaultIsolationLevel = IsolationLevel.Serializable;
                return conStr.ToString();
            }
        }
    }

mistachkin added on 2016-02-02 21:35:30: (text/x-fossil-plain)
Can you try modifying the BackupDatabase call to supply the optional callback?  The callback would need to return false if the code detects the domain is being unloaded.  Meanwhile, I'll do further research on the issue.

mistachkin added on 2016-02-02 22:44:35: (text/x-fossil-plain)
Here is how to use the backup callback mechanism to solve this issue:

    public class RemoteObject : System.MarshalByRefObject
    {
        private bool m_Unloading;
        private string m_DatabaseFilePath;

        public RemoteObject()
        {
            AppDomain.CurrentDomain.DomainUnload += CurrentDomain_DomainUnload;

            m_DatabaseFilePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestDatabase.db");
        }

        private bool BackupCallback(
            SQLiteConnection source,
            string sourceName,
            SQLiteConnection destination,
            string destinationName,
            int pages,
            int remainingPages,
            int totalPages,
            bool retry)
        {
            return !m_Unloading;
        }

        private void CurrentDomain_DomainUnload(object sender, EventArgs e)
        {
            m_Unloading = true;
        }

        public void BackupDatabase()
        {
            System.Console.WriteLine("Backup started.");

            SQLiteConnectionStringBuilder destConStr = new SQLiteConnectionStringBuilder(ConnectionString);

            string backupFilePath = destConStr.DataSource + ".bak";
            destConStr.DataSource = backupFilePath;

            try
            {
                //remove existing .bak file
                if (File.Exists(backupFilePath))
                    File.Delete(backupFilePath);
            }
            catch (IOException)
            {
                Console.WriteLine("It was not possible to remove obsolete database .bak file.");
            }

            try
            {
                using (SQLiteConnection dest = new SQLiteConnection(destConStr.ConnectionString))
                {
                    using (SQLiteConnection source = new SQLiteConnection(ConnectionString))
                    {
                        source.Open();
                        dest.Open();
                        source.BackupDatabase(dest, "main", "main", -1, BackupCallback, 0);
                    }
                }
            }
            catch
            {
                Console.WriteLine("Database backup failed.");

                try
                {
                    //remove incomplete database backup file
                    if (File.Exists(backupFilePath))
                        File.Delete(backupFilePath);
                }
                catch (IOException)
                {
                    Console.WriteLine("It was not possible to remove obsolete database .bak file.");
                }
            }

            System.Console.WriteLine("Backup completed.");
        }

        public string ConnectionString
        {
            get
            {
                SQLiteConnectionStringBuilder conStr = new SQLiteConnectionStringBuilder();
                conStr.DataSource = m_DatabaseFilePath;
                conStr.FailIfMissing = false;
                conStr.JournalMode = SQLiteJournalModeEnum.Wal;
                conStr.DefaultIsolationLevel = IsolationLevel.Serializable;
                return conStr.ToString();
            }
        }
    }

mistachkin added on 2016-02-02 23:35:29: (text/x-fossil-plain)
By the way, there are no locks involved in this issue.  Hence, it's not technically a "deadlock".  Rather, the AppDomain being unloaded was busy because it was in native code backing up the entire database.

anonymous added on 2016-02-03 09:26:42: (text/x-fossil-plain)
Thank you for the information. I have tried it with the callback, however, in my case this solution does not work because the callback is never thrown. I have simplified the example to check this.  The commandline output just shows "Backup started." followed by "Backup completed." and no "Callback.".
Do I use it correctly now or do I have another flaw?



        private static void Main(string[] args)
        {
            System.Console.WriteLine("Backup started.");

            SQLiteConnectionStringBuilder sourceConStr = new SQLiteConnectionStringBuilder();
            sourceConStr.DataSource = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestDatabase.db");

            SQLiteConnectionStringBuilder destConStr = new SQLiteConnectionStringBuilder(sourceConStr.ConnectionString);
            destConStr.DataSource = destConStr.DataSource + ".bak"; ;

            //remove existing .bak file
            if (File.Exists(destConStr.DataSource))
                File.Delete(destConStr.DataSource);

            try
            {
                using (SQLiteConnection dest = new SQLiteConnection(destConStr.ConnectionString))
                {
                    using (SQLiteConnection source = new SQLiteConnection(sourceConStr.ConnectionString))
                    {
                        source.Open();
                        dest.Open();
                        source.BackupDatabase(dest, "main", "main", -1, BackupCallback, 0);
                    }
                }
            }
            catch
            {
                Console.WriteLine("Database backup failed.");
            }

            System.Console.WriteLine("Backup completed.");
        }

        private static bool BackupCallback(SQLiteConnection source, string sourceName, SQLiteConnection destination,
            string destinationName, int pages, int remainingPages, int totalPages, bool retry)
        {
            Console.WriteLine("Callback.");
            return true;
        }

mistachkin added on 2016-02-03 18:57:43: (text/x-fossil-plain)
Try setting the "pages" parameter of the BackupDatabase call to 1.

anonymous added on 2016-02-04 10:30:30: (text/x-fossil-plain)
Thank you very much, it works like a charm now. 
Maybe someone can add some more documentation for the backup function in the future.  The description of the "pages" parameter seams a bit misleading.