Skip to content

Commit

Permalink
add some mutation flags to moonfire-nvr check
Browse files Browse the repository at this point in the history
For recovering from corruption, as in #107. These should aid in
restoring database integrity without throwing away the entire database.
I only added the conditions that came up in #107, so far.

*   "Missing ... row" => --trash-orphan-sample-files
*   "Recording ... missing file" => --delete-orphan-rows
*   "bad video_index" => --trash-corrupt-rows
  • Loading branch information
scottlamb committed Feb 12, 2021
1 parent 5acca1a commit 7f711ee
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 8 deletions.
60 changes: 55 additions & 5 deletions server/db/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use crate::dir;
use crate::raw;
use crate::recording;
use failure::Error;
use fnv::FnvHashMap;
use fnv::{FnvHashMap, FnvHashSet};
use log::{info, error, warn};
use nix::fcntl::AtFlags;
use rusqlite::params;
Expand All @@ -45,9 +45,18 @@ use std::os::unix::io::AsRawFd;

pub struct Options {
pub compare_lens: bool,
pub trash_orphan_sample_files: bool,
pub delete_orphan_rows: bool,
pub trash_corrupt_rows: bool,
}

pub fn run(conn: &rusqlite::Connection, opts: &Options) -> Result<i32, Error> {
#[derive(Default)]
pub struct Context {
rows_to_delete: FnvHashSet<CompositeId>,
files_to_trash: FnvHashSet<(i32, CompositeId)>, // (dir_id, composite_id)
}

pub fn run(conn: &mut rusqlite::Connection, opts: &Options) -> Result<i32, Error> {
let mut printed_error = false;

info!("Checking SQLite database integrity...");
Expand Down Expand Up @@ -124,6 +133,7 @@ pub fn run(conn: &rusqlite::Connection, opts: &Options) -> Result<i32, Error> {
}

// Scan known streams.
let mut ctx = Context::default();
{
let mut stmt = conn.prepare(r#"
select
Expand All @@ -145,7 +155,7 @@ pub fn run(conn: &rusqlite::Connection, opts: &Options) -> Result<i32, Error> {
Some(d) => d.remove(&stream_id).unwrap_or_else(Stream::default),
};
stream.cum_recordings = Some(cum_recordings);
printed_error |= compare_stream(conn, stream_id, opts, stream)?;
printed_error |= compare_stream(conn, dir_id, stream_id, opts, stream, &mut ctx)?;
}
}

Expand All @@ -163,6 +173,30 @@ pub fn run(conn: &rusqlite::Connection, opts: &Options) -> Result<i32, Error> {
}
}

if !ctx.rows_to_delete.is_empty() || !ctx.files_to_trash.is_empty() {
let tx = conn.transaction()?;
if !ctx.rows_to_delete.is_empty() {
info!("Deleting {} recording rows", ctx.rows_to_delete.len());
let mut d1 = tx.prepare("delete from recording where composite_id = ?")?;
let mut d2 = tx.prepare("delete from recording_playback where composite_id = ?")?;
let mut d3 = tx.prepare("delete from recording_integrity where composite_id = ?")?;
for &id in &ctx.rows_to_delete {
d1.execute(params![id.0])?;
d2.execute(params![id.0])?;
d3.execute(params![id.0])?;
}
}
if !ctx.files_to_trash.is_empty() {
info!("Trashing {} recording files", ctx.files_to_trash.len());
let mut g = tx.prepare(
"insert or ignore into garbage (sample_file_dir_id, composite_id) values (?, ?)")?;
for (dir_id, composite_id) in &ctx.files_to_trash {
g.execute(params![dir_id, composite_id.0])?;
}
}
tx.commit()?;
}

Ok(if printed_error { 1 } else { 0 })
}

Expand Down Expand Up @@ -254,8 +288,8 @@ fn read_dir(d: &dir::SampleFileDir, opts: &Options) -> Result<Dir, Error> {
}

/// Looks through a known stream for errors.
fn compare_stream(conn: &rusqlite::Connection, stream_id: i32, opts: &Options,
mut stream: Stream) -> Result<bool, Error> {
fn compare_stream(conn: &rusqlite::Connection, dir_id: i32, stream_id: i32, opts: &Options,
mut stream: Stream, ctx: &mut Context) -> Result<bool, Error> {
let start = CompositeId::new(stream_id, 0);
let end = CompositeId::new(stream_id, i32::max_value());
let mut printed_error = false;
Expand Down Expand Up @@ -312,6 +346,10 @@ fn compare_stream(conn: &rusqlite::Connection, stream_id: i32, opts: &Options,
Err(e) => {
error!("id {} has bad video_index: {}", id, e);
printed_error = true;
if opts.trash_corrupt_rows {
ctx.rows_to_delete.insert(id);
ctx.files_to_trash.insert((dir_id, id));
}
continue;
},
};
Expand Down Expand Up @@ -360,9 +398,18 @@ fn compare_stream(conn: &rusqlite::Connection, stream_id: i32, opts: &Options,
None => {
if db_rows_expected {
error!("Missing recording row for {}: {:#?}", id, recording);
if opts.trash_orphan_sample_files {
ctx.files_to_trash.insert((dir_id, id));
}
if opts.delete_orphan_rows { // also delete playback/integrity rows, if any.
ctx.rows_to_delete.insert(id);
}
printed_error = true;
} else if recording.playback_row.is_some() {
error!("Unexpected playback row for {}: {:#?}", id, recording);
if opts.delete_orphan_rows {
ctx.rows_to_delete.insert(id);
}
printed_error = true;
}
continue;
Expand All @@ -387,6 +434,9 @@ fn compare_stream(conn: &rusqlite::Connection, stream_id: i32, opts: &Options,
},
None => {
error!("Recording {} missing file: {:#?}", id, recording);
if opts.delete_orphan_rows {
ctx.rows_to_delete.insert(id);
}
printed_error = true;
},
}
Expand Down
29 changes: 26 additions & 3 deletions server/src/cmds/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,35 @@ pub struct Args {
/// Compare sample file lengths on disk to the database.
#[structopt(long)]
compare_lens: bool,

/// Trash sample files without matching recording rows in the database.
/// This addresses "Missing ... row" errors.
///
/// The ids are added to the "garbage" table to indicate the files need to
/// be deleted. Garbage is collected on normal startup.
#[structopt(long)]
trash_orphan_sample_files: bool,

/// Delete recording rows in the database without matching sample files.
/// This addresses "Recording ... missing file" errors.
#[structopt(long)]
delete_orphan_rows: bool,

/// Trash recordings when their database rows appear corrupt.
/// This addresses "bad video_index" errors.
///
/// The ids are added to the "garbage" table to indicate their files need to
/// be deleted. Garbage is collected on normal startup.
#[structopt(long)]
trash_corrupt_rows: bool,
}

pub fn run(args: &Args) -> Result<i32, Error> {
// TODO: ReadOnly should be sufficient but seems to fail.
let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
check::run(&conn, &check::Options {
let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
check::run(&mut conn, &check::Options {
compare_lens: args.compare_lens,
trash_orphan_sample_files: args.trash_orphan_sample_files,
delete_orphan_rows: args.delete_orphan_rows,
trash_corrupt_rows: args.trash_corrupt_rows,
})
}

0 comments on commit 7f711ee

Please sign in to comment.