Skip to content

Commit 2425995

Browse files
committed
Merge branch 'main' of github.com:opensensor/lightNVR
2 parents 9ec818e + 9972918 commit 2425995

4 files changed

Lines changed: 385 additions & 2 deletions

File tree

‎src/database/db_core.c‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,8 +1024,41 @@ void shutdown_database(void) {
10241024
// database and verifies the copy, so a process that only read a few rows
10251025
// would otherwise spend minutes on exit writing a duplicate of a database
10261026
// it never modified.
1027+
time_t backup_interval_seconds = g_config.db_backup_interval_minutes > 0
1028+
? (time_t)g_config.db_backup_interval_minutes * 60 : 0;
1029+
time_t now = time(NULL);
1030+
// now >= last_backup_time guards two edge cases: a backward system clock
1031+
// jump (NTP correction) after last_backup_time was recorded, and
1032+
// time(NULL) itself failing (returns (time_t)-1 per POSIX). Either would
1033+
// otherwise make `now - last_backup_time` negative -- always "less than"
1034+
// a positive interval -- incorrectly treating a backup as recent (or the
1035+
// timestamp as "in the future") and skipping the shutdown backup when it
1036+
// shouldn't be skipped.
1037+
bool recent_backup_exists = backup_interval_seconds > 0 &&
1038+
last_backup_time != 0 && now >= last_backup_time &&
1039+
now - last_backup_time < backup_interval_seconds;
1040+
10271041
if (db_init_flags & DB_INIT_NO_BACKUP) {
10281042
log_info("Skipping shutdown backup (read-only initialization)");
1043+
} else if (recent_backup_exists) {
1044+
// A backup already exists from within the configured scheduled
1045+
// interval -- most commonly, the hourly scheduled backup just ran a
1046+
// few minutes before a restart/shutdown was requested. Taking
1047+
// another one here would fully re-copy and re-verify a multi-
1048+
// gigabyte database (observed up to ~9-10 minutes) purely to
1049+
// capture a few minutes of additional changes, on every single
1050+
// restart. The on-disk database file itself is still fully
1051+
// checkpointed and closed cleanly below regardless -- this only
1052+
// skips the *extra* standalone backup-file snapshot, whose purpose
1053+
// is disaster recovery, not the shutdown's own data integrity. Worst
1054+
// case if corruption strikes shortly after, recovery falls back to
1055+
// a backup up to one scheduled interval old -- the same staleness
1056+
// bound already accepted during normal steady-state operation
1057+
// between scheduled backups.
1058+
log_info("Skipping final backup: last backup was %ld seconds ago, "
1059+
"within the %d-minute scheduled interval",
1060+
(long)(now - last_backup_time),
1061+
g_config.db_backup_interval_minutes);
10291062
} else if (db != NULL && db_file_path[0] != '\0') {
10301063
log_info("Creating final backup before shutdown");
10311064
// Not abortable: this is the deliberate one-time backup taken while

‎src/web/api_handlers_system.c‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,27 @@ static const system_health_observation_t *find_health_observation(
104104
static const system_health_observation_t *find_effective_health_observation(
105105
const system_health_snapshot_t *snapshot, const char *container_metric,
106106
const char *host_metric) {
107-
const system_health_observation_t *item = find_health_observation(
107+
// Prefer the container-scoped observation only when it actually has a
108+
// value -- not merely when one exists. linux_cgroup.c's collector picks
109+
// CONTAINER vs. HOST scope for a whole batch of metrics (cpu, memory,
110+
// pids) based on whether *any* of them has a real, finite limit; systemd
111+
// gives every unit a default (non-"max") TasksMax=, so a completely
112+
// unconstrained host (no CPUQuota=/MemoryMax=) still gets bumped to
113+
// CONTAINER scope purely because of that unrelated pids limit. Its
114+
// cpu.usage_ratio/memory.limit_bytes/etc. observations still get
115+
// emitted under "container.*" in that case, just marked unavailable
116+
// (unsupported/unlimited) -- and since find_health_observation() matches
117+
// on existence, not availability, this lookup used to latch onto that
118+
// permanently-unavailable container observation and never fall back to
119+
// the perfectly good host.cpu.busy_ratio/host.memory.* figures at all.
120+
const system_health_observation_t *container_item = find_health_observation(
108121
snapshot, container_metric, NULL);
109-
return item ? item : find_health_observation(snapshot, host_metric, NULL);
122+
if (container_item &&
123+
container_item->capability == SYSTEM_HEALTH_CAPABILITY_AVAILABLE)
124+
return container_item;
125+
const system_health_observation_t *host_item = find_health_observation(
126+
snapshot, host_metric, NULL);
127+
return host_item ? host_item : container_item;
110128
}
111129

112130
static bool health_observation_value(

‎tests/database/db_backup_test.c‎

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <sys/wait.h>
1515
#include <sys/stat.h>
1616
#include <limits.h>
17+
#include <dirent.h>
1718

1819
#include "database/db_core.h"
1920
#include "database/db_backup.h"
@@ -28,6 +29,7 @@
2829
#define TEST_LARGE_BACKUP_PATH "/tmp/test_db_large.sqlite.bak"
2930
#define TEST_ABORT_DB_PATH "/tmp/test_db_abort.sqlite"
3031
#define TEST_ABORT_BACKUP_PATH "/tmp/test_db_abort.sqlite.bak"
32+
#define TEST_SHUTDOWN_DB_PATH "/tmp/test_db_shutdown.sqlite"
3133

3234
static void remove_database_files(const char *path) {
3335
char sidecar[256];
@@ -650,6 +652,145 @@ static int test_backup_aborts_during_verification_when_shutdown_requested(void)
650652
return result;
651653
}
652654

655+
static int count_timestamped_backups(const char *db_path) {
656+
char backup_dir[PATH_MAX];
657+
snprintf(backup_dir, sizeof(backup_dir), "%s.backups", db_path);
658+
DIR *dir = opendir(backup_dir);
659+
if (!dir) return 0;
660+
int count = 0;
661+
struct dirent *entry;
662+
while ((entry = readdir(dir)) != NULL) {
663+
size_t name_len = strlen(entry->d_name);
664+
// Count only completed backups (name.sqlite3), not .tmp/-wal/-shm/
665+
// -journal debris from an interrupted or in-progress copy.
666+
if (name_len > 8 && strcmp(entry->d_name + name_len - 8, ".sqlite3") == 0) {
667+
count++;
668+
}
669+
}
670+
closedir(dir);
671+
return count;
672+
}
673+
674+
static void remove_test_db_and_backups(const char *db_path) {
675+
char path[PATH_MAX];
676+
static const char *sidecar_suffixes[] = {"", ".bak", "-wal", "-shm", "-journal"};
677+
for (size_t i = 0; i < sizeof(sidecar_suffixes) / sizeof(sidecar_suffixes[0]); i++) {
678+
snprintf(path, sizeof(path), "%s%s", db_path, sidecar_suffixes[i]);
679+
unlink(path);
680+
}
681+
682+
// A stale <db_path>.bak or -wal/-shm/-journal sidecar left behind by an
683+
// earlier test would otherwise let init_database_ex() pick up a bogus
684+
// last_backup_time (it stat()s the .bak path at startup) or a stale WAL,
685+
// causing cross-test interference in this shared-connection test binary.
686+
char backup_dir[PATH_MAX];
687+
snprintf(backup_dir, sizeof(backup_dir), "%s.backups", db_path);
688+
DIR *dir = opendir(backup_dir);
689+
if (dir) {
690+
struct dirent *entry;
691+
while ((entry = readdir(dir)) != NULL) {
692+
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
693+
continue;
694+
}
695+
snprintf(path, sizeof(path), "%s/%s", backup_dir, entry->d_name);
696+
unlink(path);
697+
}
698+
closedir(dir);
699+
rmdir(backup_dir);
700+
}
701+
}
702+
703+
// Regression test: shutdown_database() used to unconditionally take a full
704+
// backup-and-verify snapshot on every clean shutdown, no matter how recently
705+
// the hourly scheduled backup had already run -- on a multi-gigabyte
706+
// production database this made every restart take minutes just to
707+
// re-capture a few minutes of additional changes. Verifies that a shutdown
708+
// happening shortly after a scheduled backup skips the redundant one.
709+
static int test_shutdown_skips_backup_when_recent_backup_exists(void) {
710+
int result = -1;
711+
712+
// db_core.c holds a single global connection shared across this whole
713+
// file's main(); create_test_database() (run earlier in main()) opened
714+
// TEST_DB_PATH and left it open -- nothing in between closes it until
715+
// corrupt_database() does so itself, later, right before corrupting the
716+
// raw file. This test needs a second, separate database open at the same
717+
// time, so close the existing connection first, same as corrupt_database()
718+
// does for its own reason.
719+
shutdown_database();
720+
721+
remove_test_db_and_backups(TEST_SHUTDOWN_DB_PATH);
722+
723+
if (init_database(TEST_SHUTDOWN_DB_PATH) != 0) {
724+
printf("Failed to init database for shutdown-skip test\n");
725+
return -1;
726+
}
727+
728+
g_config.db_backup_interval_minutes = 60;
729+
730+
if (maybe_run_scheduled_database_backup() != 0) {
731+
printf("Scheduled backup failed in shutdown-skip test setup\n");
732+
goto cleanup;
733+
}
734+
int count_after_scheduled = count_timestamped_backups(TEST_SHUTDOWN_DB_PATH);
735+
if (count_after_scheduled != 1) {
736+
printf("Expected exactly 1 backup after the scheduled cycle, found %d\n",
737+
count_after_scheduled);
738+
goto cleanup;
739+
}
740+
741+
shutdown_database();
742+
743+
int count_after_shutdown = count_timestamped_backups(TEST_SHUTDOWN_DB_PATH);
744+
if (count_after_shutdown != count_after_scheduled) {
745+
printf("Shutdown took a redundant backup: had %d, now %d\n",
746+
count_after_scheduled, count_after_shutdown);
747+
return -1;
748+
}
749+
750+
printf("Shutdown correctly skipped a redundant backup\n");
751+
result = 0;
752+
753+
cleanup:
754+
remove_test_db_and_backups(TEST_SHUTDOWN_DB_PATH);
755+
return result;
756+
}
757+
758+
// Regression test for the same fix: when scheduled backups are disabled
759+
// (db_backup_interval_minutes <= 0), there is no defined freshness window,
760+
// so shutdown must still take its final backup rather than skipping
761+
// unconditionally.
762+
static int test_shutdown_backs_up_when_scheduled_backups_disabled(void) {
763+
remove_test_db_and_backups(TEST_SHUTDOWN_DB_PATH);
764+
765+
if (init_database(TEST_SHUTDOWN_DB_PATH) != 0) {
766+
printf("Failed to init database for shutdown-disabled-interval test\n");
767+
return -1;
768+
}
769+
770+
g_config.db_backup_interval_minutes = 0;
771+
772+
// init_database() takes its own "initial backup" of a brand-new database
773+
// regardless of the scheduled interval, so the baseline here is 1, not
774+
// 0 -- what this test actually verifies is that shutdown adds another
775+
// one on top, rather than caring about that unrelated initial-backup
776+
// implementation detail.
777+
int count_before_shutdown = count_timestamped_backups(TEST_SHUTDOWN_DB_PATH);
778+
779+
shutdown_database();
780+
781+
int count = count_timestamped_backups(TEST_SHUTDOWN_DB_PATH);
782+
remove_test_db_and_backups(TEST_SHUTDOWN_DB_PATH);
783+
if (count <= count_before_shutdown) {
784+
printf("Expected shutdown to take a backup with scheduled backups "
785+
"disabled: had %d before, %d after\n", count_before_shutdown,
786+
count);
787+
return -1;
788+
}
789+
790+
printf("Shutdown correctly backed up despite scheduled backups being disabled\n");
791+
return 0;
792+
}
793+
653794
// Test restore functionality
654795
static int test_restore(void) {
655796
char stale_wal[256];
@@ -735,6 +876,20 @@ int main(void) {
735876
return 1;
736877
}
737878

879+
if (test_shutdown_skips_backup_when_recent_backup_exists() != 0) {
880+
printf("Test failed: shutdown did not skip a redundant backup\n");
881+
return 1;
882+
}
883+
884+
if (test_shutdown_backs_up_when_scheduled_backups_disabled() != 0) {
885+
printf("Test failed: shutdown did not back up with scheduled backups disabled\n");
886+
return 1;
887+
}
888+
889+
// Restore the interval this file's own load_default_config() set, in
890+
// case any later step in this binary implicitly depends on it.
891+
load_default_config(&g_config);
892+
738893
// Corrupt the database
739894
if (corrupt_database() != 0) {
740895
printf("Test failed: Could not corrupt database\n");

0 commit comments

Comments
 (0)