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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
// Conserve backup system.
// Copyright 2017, 2018, 2019, 2020 Martin Pool.

// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

//! A `DeleteGuard` prevents block deletion while either a backup is pending,
//! or if a band is created concurrently with garbage enumeration, or if
//! another gc operation is underway.
//!
//! Deletion of blocks works by: finding all the blocks that are present,
//! then finding all the blocks that are referenced, then deleting the
//! blocks that are present but unreferenced.
//!
//! This matches the order in which data is written to the archive: data
//! blocks first, and then the index hunks that reference them.
//!
//! However, if a backup was running concurrently with garbage collection,
//! it's possible that we'd see the block and read the index before the
//! backup gets around to writing the index.
//!
//! Therefore, before starting enumeration, we check the latest band id,
//! and if it exists it must be complete. Then, after finding the blocks to
//! delete but before starting to actually delete them, we check that no
//! new bands have been created.

use crate::*;

pub static GC_LOCK: &str = "GC_LOCK";

#[derive(Debug)]
pub struct GarbageCollectionLock {
    archive: Archive,

    /// Last band id present when the guard was created. May be None if
    /// there are no bands.
    band_id: Option<BandId>,
}

/// Lock on an archive for gc, that excludes backups and gc by other processes.
///
/// The lock is released when the object is dropped.
impl GarbageCollectionLock {
    /// Lock this archive for garbage collection.
    ///
    /// Returns `Err(Error::DeleteWithIncompleteBackup)` if the last
    /// backup is incomplete.
    pub fn new(archive: &Archive) -> Result<GarbageCollectionLock> {
        let archive = archive.clone();
        let band_id = archive.last_band_id()?;
        if let Some(band_id) = band_id {
            if !archive.band_is_closed(band_id)? {
                return Err(Error::DeleteWithIncompleteBackup { band_id });
            }
        }
        if archive.transport().is_file(GC_LOCK).unwrap_or(true) {
            return Err(Error::GarbageCollectionLockHeld);
        }
        archive.transport().write_file(GC_LOCK, b"{}\n")?;
        Ok(GarbageCollectionLock { archive, band_id })
    }

    /// Take a lock on an archive, breaking any existing gc lock.
    ///
    /// Use this only if you're confident that the process owning the lock
    /// has terminated and the lock is stale.
    pub fn break_lock(archive: &Archive) -> Result<GarbageCollectionLock> {
        if GarbageCollectionLock::is_locked(archive)? {
            archive.transport().remove_file(GC_LOCK)?;
        }
        GarbageCollectionLock::new(archive)
    }

    /// Returns true if the archive is currently locked by a gc process.
    pub fn is_locked(archive: &Archive) -> Result<bool> {
        archive.transport().is_file(GC_LOCK).map_err(Error::from)
    }

    /// Check that no new versions have been created in this archive since
    /// the guard was created.
    pub fn check(&self) -> Result<()> {
        let current_last_band_id = self.archive.last_band_id()?;
        if self.band_id == current_last_band_id {
            Ok(())
        } else {
            Err(Error::GarbageCollectionLockHeldDuringBackup)
        }
    }
}

impl Drop for GarbageCollectionLock {
    fn drop(&mut self) {
        if let Err(err) = self.archive.transport().remove_file(GC_LOCK) {
            // Print directly to stderr, in case the UI structure is in a
            // bad state during unwind.
            eprintln!("Failed to delete GC_LOCK: {err:?}")
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::monitor::collect::CollectMonitor;
    use crate::test_fixtures::{ScratchArchive, TreeFixture};

    #[test]
    fn empty_archive_ok() {
        let archive = ScratchArchive::new();
        let delete_guard = GarbageCollectionLock::new(&archive).unwrap();
        assert!(archive.transport().is_file("GC_LOCK").unwrap());
        delete_guard.check().unwrap();

        // Released when dropped.
        drop(delete_guard);
        assert!(!archive.transport().is_file("GC_LOCK").unwrap());
    }

    #[test]
    fn completed_backup_ok() {
        let archive = ScratchArchive::new();
        let source = TreeFixture::new();
        backup(
            &archive,
            source.path(),
            &BackupOptions::default(),
            CollectMonitor::arc(),
        )
        .unwrap();
        let delete_guard = GarbageCollectionLock::new(&archive).unwrap();
        delete_guard.check().unwrap();
    }

    #[test]
    fn concurrent_complete_backup_denied() {
        let archive = ScratchArchive::new();
        let source = TreeFixture::new();
        let _delete_guard = GarbageCollectionLock::new(&archive).unwrap();
        let backup_result = backup(
            &archive,
            source.path(),
            &BackupOptions::default(),
            CollectMonitor::arc(),
        );
        assert_eq!(
            backup_result.expect_err("backup fails").to_string(),
            "Archive is locked for garbage collection"
        );
    }

    #[test]
    fn incomplete_backup_denied() {
        let archive = ScratchArchive::new();
        Band::create(&archive).unwrap();
        let result = GarbageCollectionLock::new(&archive);
        assert!(result.is_err());
        assert_eq!(
            result.err().unwrap().to_string(),
            "Can't delete blocks because the last band (b0000) is incomplete and may be in use"
        );
    }

    #[test]
    fn concurrent_gc_prevented() {
        let archive = ScratchArchive::new();
        let _lock1 = GarbageCollectionLock::new(&archive).unwrap();
        // Should not be able to create a second lock while one gc is running.
        let lock2_result = GarbageCollectionLock::new(&archive);
        assert_eq!(
            lock2_result.unwrap_err().to_string(),
            "Archive is locked for garbage collection"
        );
    }

    #[test]
    fn sequential_gc_allowed() {
        let archive = ScratchArchive::new();
        let _lock1 = GarbageCollectionLock::new(&archive).unwrap();
        drop(_lock1);
        let _lock2 = GarbageCollectionLock::new(&archive).unwrap();
        drop(_lock2);
    }

    #[test]
    fn break_lock() {
        let archive = ScratchArchive::new();
        let lock1 = GarbageCollectionLock::new(&archive).unwrap();
        // Pretend the process owning lock1 died, and get a new lock.
        std::mem::forget(lock1);
        let _lock2 = GarbageCollectionLock::break_lock(&archive).unwrap();
    }
}