Move DB compaction to CompactionView
Introduce CompactionView and CompactionLoadingView to schedule and run Ndb compaction on a background task before showing the main UI. Replace the direct compaction calls in ContentView and make CompactionView the app's launch entry so startup compaction presents a loading UI and avoids blocking the main thread. Changelog-Fixed: Fixed startup crash when app performs long-running compaction procedure Closes: #3726 Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
@@ -1755,6 +1755,9 @@
|
|||||||
D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */ = {isa = PBXBuildFile; productRef = D78DB8582C1CE9CA00F0AB12 /* SwipeActions */; };
|
D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */ = {isa = PBXBuildFile; productRef = D78DB8582C1CE9CA00F0AB12 /* SwipeActions */; };
|
||||||
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */; };
|
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */; };
|
||||||
D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */; };
|
D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */; };
|
||||||
|
D78E5FC52F9AF73C00E3EE91 /* CompactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E5FC42F9AF70200E3EE91 /* CompactionView.swift */; };
|
||||||
|
D78E5FC62F9AF7F700E3EE91 /* CompactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E5FC42F9AF70200E3EE91 /* CompactionView.swift */; };
|
||||||
|
D78E5FC72F9AF80100E3EE91 /* CompactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E5FC42F9AF70200E3EE91 /* CompactionView.swift */; };
|
||||||
D78F080C2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; };
|
D78F080C2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; };
|
||||||
D78F080D2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; };
|
D78F080D2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; };
|
||||||
D78F080E2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; };
|
D78F080E2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; };
|
||||||
@@ -2889,6 +2892,7 @@
|
|||||||
D78CD5972B8990300014D539 /* DamusAppNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAppNotificationView.swift; sourceTree = "<group>"; };
|
D78CD5972B8990300014D539 /* DamusAppNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAppNotificationView.swift; sourceTree = "<group>"; };
|
||||||
D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VectorMath.swift; sourceTree = "<group>"; };
|
D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VectorMath.swift; sourceTree = "<group>"; };
|
||||||
D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = "<group>"; };
|
D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = "<group>"; };
|
||||||
|
D78E5FC42F9AF70200E3EE91 /* CompactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompactionView.swift; sourceTree = "<group>"; };
|
||||||
D78F080B2D7F78EB00FC6C75 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
|
D78F080B2D7F78EB00FC6C75 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
|
||||||
D78F08102D7F78F600FC6C75 /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = "<group>"; };
|
D78F08102D7F78F600FC6C75 /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = "<group>"; };
|
||||||
D78F08162D7F7F6C00FC6C75 /* NIP04.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP04.swift; sourceTree = "<group>"; };
|
D78F08162D7F7F6C00FC6C75 /* NIP04.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP04.swift; sourceTree = "<group>"; };
|
||||||
@@ -4253,6 +4257,7 @@
|
|||||||
5C78A7792E22FDFE00CF177D /* Features */ = {
|
5C78A7792E22FDFE00CF177D /* Features */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D78E5FC32F9AF6EC00E3EE91 /* Compaction */,
|
||||||
5C8F97042EB45E39009399B1 /* Live */,
|
5C8F97042EB45E39009399B1 /* Live */,
|
||||||
D5C1AFC22E5DFF040092F72F /* ContactCard */,
|
D5C1AFC22E5DFF040092F72F /* ContactCard */,
|
||||||
5C78A7BC2E304D7400CF177D /* Translations */,
|
5C78A7BC2E304D7400CF177D /* Translations */,
|
||||||
@@ -5368,6 +5373,14 @@
|
|||||||
path = Chat;
|
path = Chat;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D78E5FC32F9AF6EC00E3EE91 /* Compaction */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D78E5FC42F9AF70200E3EE91 /* CompactionView.swift */,
|
||||||
|
);
|
||||||
|
path = Compaction;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D78F080A2D7F78B000FC6C75 /* WalletConnect */ = {
|
D78F080A2D7F78B000FC6C75 /* WalletConnect */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -6311,6 +6324,7 @@
|
|||||||
D7DB1FDE2D5A78CE00CF06DA /* NIP44.swift in Sources */,
|
D7DB1FDE2D5A78CE00CF06DA /* NIP44.swift in Sources */,
|
||||||
B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */,
|
B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */,
|
||||||
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
|
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
|
||||||
|
D78E5FC52F9AF73C00E3EE91 /* CompactionView.swift in Sources */,
|
||||||
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
|
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
|
||||||
5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */,
|
5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */,
|
||||||
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
|
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
|
||||||
@@ -7053,6 +7067,7 @@
|
|||||||
D7F4F0BA2F03689300B61683 /* DeveloperSettingsView.swift in Sources */,
|
D7F4F0BA2F03689300B61683 /* DeveloperSettingsView.swift in Sources */,
|
||||||
82D6FC722CD99F7900C925F4 /* EULAView.swift in Sources */,
|
82D6FC722CD99F7900C925F4 /* EULAView.swift in Sources */,
|
||||||
82D6FC732CD99F7900C925F4 /* RepostsView.swift in Sources */,
|
82D6FC732CD99F7900C925F4 /* RepostsView.swift in Sources */,
|
||||||
|
D78E5FC62F9AF7F700E3EE91 /* CompactionView.swift in Sources */,
|
||||||
82D6FC742CD99F7900C925F4 /* Launch.storyboard in Sources */,
|
82D6FC742CD99F7900C925F4 /* Launch.storyboard in Sources */,
|
||||||
82D6FC752CD99F7900C925F4 /* QRCodeView.swift in Sources */,
|
82D6FC752CD99F7900C925F4 /* QRCodeView.swift in Sources */,
|
||||||
82D6FC762CD99F7900C925F4 /* RelayFilterView.swift in Sources */,
|
82D6FC762CD99F7900C925F4 /* RelayFilterView.swift in Sources */,
|
||||||
@@ -7462,6 +7477,7 @@
|
|||||||
D73E5F452C6A97F5007EB227 /* AddRelayView.swift in Sources */,
|
D73E5F452C6A97F5007EB227 /* AddRelayView.swift in Sources */,
|
||||||
D73E5F472C6A97F5007EB227 /* BookmarksView.swift in Sources */,
|
D73E5F472C6A97F5007EB227 /* BookmarksView.swift in Sources */,
|
||||||
D73E5F482C6A97F5007EB227 /* CarouselView.swift in Sources */,
|
D73E5F482C6A97F5007EB227 /* CarouselView.swift in Sources */,
|
||||||
|
D78E5FC72F9AF80100E3EE91 /* CompactionView.swift in Sources */,
|
||||||
D73E5F492C6A97F5007EB227 /* ConfigView.swift in Sources */,
|
D73E5F492C6A97F5007EB227 /* ConfigView.swift in Sources */,
|
||||||
D733F9E12D92C1D900317B11 /* SubscriptionManager.swift in Sources */,
|
D733F9E12D92C1D900317B11 /* SubscriptionManager.swift in Sources */,
|
||||||
D73E5F4A2C6A97F5007EB227 /* CreateAccountView.swift in Sources */,
|
D73E5F4A2C6A97F5007EB227 /* CreateAccountView.swift in Sources */,
|
||||||
|
|||||||
@@ -142,15 +142,6 @@ struct ContentView: View {
|
|||||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
init(keypair: Keypair, appDelegate: AppDelegate?) {
|
init(keypair: Keypair, appDelegate: AppDelegate?) {
|
||||||
// Schedule automatic compaction based on the user's configured interval, then
|
|
||||||
// run compaction if it was previously requested (either manually or by the scheduler).
|
|
||||||
// Both calls run before opening the main Ndb instance so they work on an idle database.
|
|
||||||
// This also gets run here instead of `connect` because we should anticipate this to add a few seconds of delay in worst case scenarios.
|
|
||||||
// If we were to add this in the `connect` function, parallel functions that depend on `damus_state!` could cause crashes in the app.
|
|
||||||
// By placing this here, we only delay the splash screen a bit
|
|
||||||
Ndb.schedule_auto_compact_if_needed()
|
|
||||||
Ndb.compact_if_needed()
|
|
||||||
|
|
||||||
self.keypair = keypair
|
self.keypair = keypair
|
||||||
self.appDelegate = appDelegate
|
self.appDelegate = appDelegate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
//
|
||||||
|
// CompactionView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A view that handles database compaction on a background thread before showing the main content.
|
||||||
|
///
|
||||||
|
/// This view sits between `MainView` and `ContentView` to prevent the app from being killed by
|
||||||
|
/// the watchdog due to long-running compaction operations blocking the main thread during startup.
|
||||||
|
///
|
||||||
|
/// ## Implementation
|
||||||
|
///
|
||||||
|
/// When compaction is needed, this view:
|
||||||
|
/// 1. Shows a loading screen with progress information
|
||||||
|
/// 2. Performs the compaction on a background thread
|
||||||
|
/// 3. Transitions to `ContentView` once compaction completes
|
||||||
|
/// 4. Requires explicit user acknowledgement before continuing if compaction fails
|
||||||
|
///
|
||||||
|
/// The compaction process is performed before `Ndb` is opened for normal use, ensuring the
|
||||||
|
/// database is in a consistent state before the main app logic begins.
|
||||||
|
struct CompactionView: View {
|
||||||
|
let keypair: Keypair
|
||||||
|
let appDelegate: AppDelegate?
|
||||||
|
|
||||||
|
@State private var isCompacting: Bool = false
|
||||||
|
@State private var compactionComplete: Bool = false
|
||||||
|
@State private var compactionError: String? = nil
|
||||||
|
@State private var hasStartedCompactionFlow: Bool = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if compactionComplete {
|
||||||
|
ContentView(keypair: keypair, appDelegate: appDelegate)
|
||||||
|
} else {
|
||||||
|
CompactionLoadingView(
|
||||||
|
isCompacting: isCompacting,
|
||||||
|
error: compactionError,
|
||||||
|
continueAfterError: continueAfterError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
guard !hasStartedCompactionFlow else { return }
|
||||||
|
hasStartedCompactionFlow = true
|
||||||
|
performCompactionIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if compaction is needed and performs it on a background thread.
|
||||||
|
///
|
||||||
|
/// This function:
|
||||||
|
/// - Schedules auto-compaction based on the configured interval
|
||||||
|
/// - Checks if compaction was previously requested
|
||||||
|
/// - If compaction is needed, performs it on a background thread with a loading UI
|
||||||
|
/// - If no compaction is needed, immediately transitions to the main content
|
||||||
|
private func performCompactionIfNeeded() {
|
||||||
|
Ndb.schedule_auto_compact_if_needed()
|
||||||
|
|
||||||
|
let needsCompaction = UserDefaults.standard.bool(forKey: Ndb.compact_on_next_launch_key)
|
||||||
|
guard needsCompaction else {
|
||||||
|
compactionComplete = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isCompacting = true
|
||||||
|
|
||||||
|
Task.detached(priority: .userInitiated) {
|
||||||
|
do {
|
||||||
|
try Ndb.compact_if_needed()
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
isCompacting = false
|
||||||
|
compactionComplete = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
isCompacting = false
|
||||||
|
compactionError = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Continues to the main content after the user acknowledges a compaction error.
|
||||||
|
private func continueAfterError() {
|
||||||
|
compactionComplete = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A loading view displayed during database compaction.
|
||||||
|
///
|
||||||
|
/// Shows a spinner and informative text to the user while the compaction process runs.
|
||||||
|
/// If compaction fails, it shows the error and requires explicit acknowledgement before continuing.
|
||||||
|
struct CompactionLoadingView: View {
|
||||||
|
let isCompacting: Bool
|
||||||
|
let error: String?
|
||||||
|
let continueAfterError: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Color(uiColor: .systemBackground)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
Image("icon")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 80, height: 80)
|
||||||
|
.cornerRadius(16)
|
||||||
|
|
||||||
|
if isCompacting {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(1.5)
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
Text("Optimizing Database", comment: "Title shown during database compaction")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
Text("This may take a moment…", comment: "Subtitle shown during database compaction")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
Image(systemName: "exclamationmark.triangle")
|
||||||
|
.font(.system(size: 40))
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
|
||||||
|
Text("Compaction Warning", comment: "Title shown when database compaction encounters an error")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
Text(error)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
Text("The app can continue, but some storage may not have been freed.", comment: "Message shown when compaction fails but app can continue after acknowledgement")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
Button(action: continueAfterError) {
|
||||||
|
Text("Continue", comment: "Button title used to continue into the app after acknowledging a compaction error")
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.padding(.top, 8)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
CompactionLoadingView(
|
||||||
|
isCompacting: true,
|
||||||
|
error: nil,
|
||||||
|
continueAfterError: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("With Error") {
|
||||||
|
CompactionLoadingView(
|
||||||
|
isCompacting: false,
|
||||||
|
error: "Failed to compact database",
|
||||||
|
continueAfterError: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ struct MainView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if let kp = keypair, !needs_setup {
|
if let kp = keypair, !needs_setup {
|
||||||
ContentView(keypair: kp, appDelegate: appDelegate)
|
CompactionView(keypair: kp, appDelegate: appDelegate)
|
||||||
.environmentObject(orientationTracker)
|
.environmentObject(orientationTracker)
|
||||||
} else {
|
} else {
|
||||||
SetupView()
|
SetupView()
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ final class NdbCompactionTests: XCTestCase {
|
|||||||
// Given: the flag is false (set in setUp)
|
// Given: the flag is false (set in setUp)
|
||||||
let dbPath = testDirectory.path
|
let dbPath = testDirectory.path
|
||||||
|
|
||||||
// When
|
// When / Then
|
||||||
Ndb.compact_if_needed(db_path: dbPath)
|
XCTAssertNoThrow(try Ndb.compact_if_needed(db_path: dbPath))
|
||||||
|
|
||||||
// Then: no temp directory was created, no files were touched
|
// Then: no temp directory was created, no files were touched
|
||||||
let tempPath = "\(dbPath)/ndb_compact_temp"
|
let tempPath = "\(dbPath)/ndb_compact_temp"
|
||||||
@@ -79,8 +79,8 @@ final class NdbCompactionTests: XCTestCase {
|
|||||||
let emptyPath = testDirectory.appendingPathComponent("empty_db").path
|
let emptyPath = testDirectory.appendingPathComponent("empty_db").path
|
||||||
try? FileManager.default.createDirectory(atPath: emptyPath, withIntermediateDirectories: true)
|
try? FileManager.default.createDirectory(atPath: emptyPath, withIntermediateDirectories: true)
|
||||||
|
|
||||||
// When
|
// When / Then
|
||||||
Ndb.compact_if_needed(db_path: emptyPath)
|
XCTAssertNoThrow(try Ndb.compact_if_needed(db_path: emptyPath))
|
||||||
|
|
||||||
// Then: flag is cleared, no temp directory left over
|
// Then: flag is cleared, no temp directory left over
|
||||||
XCTAssertFalse(
|
XCTAssertFalse(
|
||||||
@@ -112,8 +112,8 @@ final class NdbCompactionTests: XCTestCase {
|
|||||||
|
|
||||||
Ndb.set_compact_on_next_launch()
|
Ndb.set_compact_on_next_launch()
|
||||||
|
|
||||||
// When
|
// When / Then
|
||||||
Ndb.compact_if_needed(db_path: dbPath)
|
XCTAssertNoThrow(try Ndb.compact_if_needed(db_path: dbPath))
|
||||||
|
|
||||||
// Then: the flag is cleared
|
// Then: the flag is cleared
|
||||||
XCTAssertFalse(
|
XCTAssertFalse(
|
||||||
@@ -157,7 +157,7 @@ final class NdbCompactionTests: XCTestCase {
|
|||||||
|
|
||||||
// When: compact_if_needed runs
|
// When: compact_if_needed runs
|
||||||
Ndb.set_compact_on_next_launch()
|
Ndb.set_compact_on_next_launch()
|
||||||
Ndb.compact_if_needed(db_path: dbPath)
|
XCTAssertNoThrow(try Ndb.compact_if_needed(db_path: dbPath))
|
||||||
|
|
||||||
// Then: lock.mdb should NOT exist (it was deleted during compaction)
|
// Then: lock.mdb should NOT exist (it was deleted during compaction)
|
||||||
XCTAssertFalse(
|
XCTAssertFalse(
|
||||||
@@ -279,7 +279,7 @@ final class NdbCompactionTests: XCTestCase {
|
|||||||
Ndb.set_compact_on_next_launch()
|
Ndb.set_compact_on_next_launch()
|
||||||
|
|
||||||
let beforeCompact = Date()
|
let beforeCompact = Date()
|
||||||
Ndb.compact_if_needed(db_path: dbPath)
|
XCTAssertNoThrow(try Ndb.compact_if_needed(db_path: dbPath))
|
||||||
let afterCompact = Date()
|
let afterCompact = Date()
|
||||||
|
|
||||||
guard let lastDate = Ndb.get_last_compact_date() else {
|
guard let lastDate = Ndb.get_last_compact_date() else {
|
||||||
|
|||||||
@@ -38,6 +38,37 @@ enum AutoCompactSchedule: String, CaseIterable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension Ndb {
|
extension Ndb {
|
||||||
|
/// Errors that can occur while compacting the database.
|
||||||
|
enum CompactionError: LocalizedError {
|
||||||
|
case missingDatabasePath
|
||||||
|
case createTempDirectoryFailed(underlyingError: Error)
|
||||||
|
case openDatabaseFailed(path: String)
|
||||||
|
case snapshotFailed(underlyingError: Error)
|
||||||
|
case compactedFileMissingOrEmpty(path: String)
|
||||||
|
case replaceDatabaseFailed(underlyingError: Error)
|
||||||
|
case postReplaceSizeMismatch(expected: Int, actual: Int)
|
||||||
|
|
||||||
|
/// A user-presentable description of the compaction failure.
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .missingDatabasePath:
|
||||||
|
return "Could not determine the database path."
|
||||||
|
case .createTempDirectoryFailed(let underlyingError):
|
||||||
|
return "Failed to create the temporary compaction directory: \(underlyingError.localizedDescription)"
|
||||||
|
case .openDatabaseFailed(let path):
|
||||||
|
return "Failed to open the database for compaction at path: \(path)"
|
||||||
|
case .snapshotFailed(let underlyingError):
|
||||||
|
return "Failed to create a compacted database snapshot: \(underlyingError.localizedDescription)"
|
||||||
|
case .compactedFileMissingOrEmpty(let path):
|
||||||
|
return "The compacted database file is missing or empty at path: \(path)"
|
||||||
|
case .replaceDatabaseFailed(let underlyingError):
|
||||||
|
return "Failed to replace the original database with the compacted copy: \(underlyingError.localizedDescription)"
|
||||||
|
case .postReplaceSizeMismatch(let expected, let actual):
|
||||||
|
return "The compacted database size verification failed. Expected \(expected) bytes, got \(actual) bytes."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Makes a compacted copy of the database in a separate directory.
|
/// Makes a compacted copy of the database in a separate directory.
|
||||||
///
|
///
|
||||||
/// This uses `mdb_env_copy2` with `MDB_CP_COMPACT` (flag = 0x01), which omits free pages
|
/// This uses `mdb_env_copy2` with `MDB_CP_COMPACT` (flag = 0x01), which omits free pages
|
||||||
@@ -133,12 +164,13 @@ extension Ndb {
|
|||||||
///
|
///
|
||||||
/// - Parameter db_path: Override the database directory path. Pass `nil` (default) to use
|
/// - Parameter db_path: Override the database directory path. Pass `nil` (default) to use
|
||||||
/// `Ndb.db_path`. Mainly useful for testing.
|
/// `Ndb.db_path`. Mainly useful for testing.
|
||||||
static func compact_if_needed(db_path: String? = nil) {
|
/// - Throws: `CompactionError` when compaction was requested but could not be completed.
|
||||||
|
static func compact_if_needed(db_path: String? = nil) throws {
|
||||||
guard UserDefaults.standard.bool(forKey: compact_on_next_launch_key) else { return }
|
guard UserDefaults.standard.bool(forKey: compact_on_next_launch_key) else { return }
|
||||||
|
|
||||||
guard let path = db_path ?? Self.db_path else {
|
guard let path = db_path ?? Self.db_path else {
|
||||||
Log.error("compact_if_needed: could not determine db path", for: .storage)
|
Log.error("compact_if_needed: could not determine db path", for: .storage)
|
||||||
return
|
throw CompactionError.missingDatabasePath
|
||||||
}
|
}
|
||||||
|
|
||||||
guard db_file_exists(path: path) else {
|
guard db_file_exists(path: path) else {
|
||||||
@@ -158,14 +190,14 @@ extension Ndb {
|
|||||||
try FileManager.default.createDirectory(atPath: tempPath, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(atPath: tempPath, withIntermediateDirectories: true)
|
||||||
} catch {
|
} catch {
|
||||||
Log.error("compact_if_needed: failed to create temp dir: %@", for: .storage, String(describing: error))
|
Log.error("compact_if_needed: failed to create temp dir: %@", for: .storage, String(describing: error))
|
||||||
return
|
throw CompactionError.createTempDirectoryFailed(underlyingError: error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open a temporary Ndb instance just to drive the compaction.
|
// Open a temporary Ndb instance just to drive the compaction.
|
||||||
guard let tempNdb = Ndb(path: path) else {
|
guard let tempNdb = Ndb(path: path) else {
|
||||||
Log.error("compact_if_needed: failed to open ndb for compaction", for: .storage)
|
Log.error("compact_if_needed: failed to open ndb for compaction", for: .storage)
|
||||||
try? FileManager.default.removeItem(atPath: tempPath)
|
try? FileManager.default.removeItem(atPath: tempPath)
|
||||||
return
|
throw CompactionError.openDatabaseFailed(path: path)
|
||||||
}
|
}
|
||||||
// Ensure the temporary Ndb is closed regardless of how this function exits.
|
// Ensure the temporary Ndb is closed regardless of how this function exits.
|
||||||
defer { tempNdb.close() }
|
defer { tempNdb.close() }
|
||||||
@@ -175,7 +207,7 @@ extension Ndb {
|
|||||||
} catch {
|
} catch {
|
||||||
Log.error("compact_if_needed: compaction failed: %@", for: .storage, String(describing: error))
|
Log.error("compact_if_needed: compaction failed: %@", for: .storage, String(describing: error))
|
||||||
try? FileManager.default.removeItem(atPath: tempPath)
|
try? FileManager.default.removeItem(atPath: tempPath)
|
||||||
return
|
throw CompactionError.snapshotFailed(underlyingError: error)
|
||||||
}
|
}
|
||||||
|
|
||||||
tempNdb.close()
|
tempNdb.close()
|
||||||
@@ -192,7 +224,7 @@ extension Ndb {
|
|||||||
guard compactedSize > 0 else {
|
guard compactedSize > 0 else {
|
||||||
Log.error("compact_if_needed: compacted file is missing or empty — aborting", for: .storage)
|
Log.error("compact_if_needed: compacted file is missing or empty — aborting", for: .storage)
|
||||||
try? FileManager.default.removeItem(atPath: tempPath)
|
try? FileManager.default.removeItem(atPath: tempPath)
|
||||||
return
|
throw CompactionError.compactedFileMissingOrEmpty(path: compactedDataMdb.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the stale lock.mdb BEFORE replacing data.mdb.
|
// Delete the stale lock.mdb BEFORE replacing data.mdb.
|
||||||
@@ -213,7 +245,7 @@ extension Ndb {
|
|||||||
} catch {
|
} catch {
|
||||||
Log.error("compact_if_needed: failed to replace db file: %@", for: .storage, String(describing: error))
|
Log.error("compact_if_needed: failed to replace db file: %@", for: .storage, String(describing: error))
|
||||||
try? FileManager.default.removeItem(atPath: tempPath)
|
try? FileManager.default.removeItem(atPath: tempPath)
|
||||||
return
|
throw CompactionError.replaceDatabaseFailed(underlyingError: error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post-replace sanity check: verify the destination file exists with the expected size.
|
// Post-replace sanity check: verify the destination file exists with the expected size.
|
||||||
@@ -221,7 +253,7 @@ extension Ndb {
|
|||||||
if finalSize != compactedSize {
|
if finalSize != compactedSize {
|
||||||
Log.error("compact_if_needed: post-replace size mismatch — expected %d, got %d", for: .storage, compactedSize, finalSize)
|
Log.error("compact_if_needed: post-replace size mismatch — expected %d, got %d", for: .storage, compactedSize, finalSize)
|
||||||
try? FileManager.default.removeItem(atPath: tempPath)
|
try? FileManager.default.removeItem(atPath: tempPath)
|
||||||
return
|
throw CompactionError.postReplaceSizeMismatch(expected: compactedSize, actual: finalSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.info("NostrDB compacted successfully", for: .storage)
|
Log.info("NostrDB compacted successfully", for: .storage)
|
||||||
|
|||||||
Reference in New Issue
Block a user