summaryrefslogtreecommitdiff
path: root/src/db/backup.rs
blob: bb36aeab8b469a373171decc7c63e40870b67d98 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
use rusqlite::{
    backup::{self},
    Connection,
};
use sqlx::sqlite::{LockedSqliteHandle, SqlitePool};

pub struct Builder<'p> {
    from: &'p SqlitePool,
}

impl<'p> Builder<'p> {
    pub fn to(self, to: &'p SqlitePool) -> Backup<'p> {
        Backup {
            from: self.from,
            to,
        }
    }
}

pub struct Backup<'p> {
    from: &'p SqlitePool,
    to: &'p SqlitePool,
}

impl<'p> Backup<'p> {
    pub fn from(from: &'p SqlitePool) -> Builder<'p> {
        Builder { from }
    }
}

impl<'p> Backup<'p> {
    pub async fn backup(&mut self) -> Result<(), Error> {
        let mut to = self.to.acquire().await?;
        let mut to = Self::connection(&mut to.lock_handle().await?)?;
        let mut from = self.from.acquire().await?;
        let from = Self::connection(&mut from.lock_handle().await?)?;

        let backup = backup::Backup::new(&from, &mut to)?;
        loop {
            use rusqlite::backup::StepResult::{Busy, Done, Locked, More};
            match backup.step(-1)? {
                Done => break Ok(()),
                // In the system as actually written, yielding does nothing: this is the
                // only task actually in flight at the time. However, that may change; if
                // it does, we _definitely_ want to give other tasks an opportunity to
                // make progress before we retry.
                More => tokio::task::consume_budget().await,
                // If another task is actually using the DB, then it _definitely_ needs
                // an opportunity to make progress before we retry.
                Busy | Locked => tokio::task::yield_now().await,
                // As of this writing, this arm is formally unreachable: all of the enum
                // constants that actually exist in rusqlite are matched above. However,
                // they've reserved the prerogative to add additional arms without a
                // breaking change by making the type as non-exhaustive; if we get one,
                // stop immediately as there's no way to guess what the correct handling
                // will be, a priori.
                other => panic!("unexpected backup step result: {other:?}"),
            }
        }
    }

    fn connection(handle: &mut LockedSqliteHandle) -> Result<Connection, rusqlite::Error> {
        let handle = handle.as_raw_handle();
        let connection = unsafe { Connection::from_handle(handle.as_ptr()) }?;

        Ok(connection)
    }
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error(transparent)]
    Sqlx(#[from] sqlx::Error),
    #[error(transparent)]
    Rusqlite(#[from] rusqlite::Error),
}