|
14 | 14 | #include <sys/wait.h> |
15 | 15 | #include <sys/stat.h> |
16 | 16 | #include <limits.h> |
| 17 | +#include <dirent.h> |
17 | 18 |
|
18 | 19 | #include "database/db_core.h" |
19 | 20 | #include "database/db_backup.h" |
|
28 | 29 | #define TEST_LARGE_BACKUP_PATH "/tmp/test_db_large.sqlite.bak" |
29 | 30 | #define TEST_ABORT_DB_PATH "/tmp/test_db_abort.sqlite" |
30 | 31 | #define TEST_ABORT_BACKUP_PATH "/tmp/test_db_abort.sqlite.bak" |
| 32 | +#define TEST_SHUTDOWN_DB_PATH "/tmp/test_db_shutdown.sqlite" |
31 | 33 |
|
32 | 34 | static void remove_database_files(const char *path) { |
33 | 35 | char sidecar[256]; |
@@ -650,6 +652,145 @@ static int test_backup_aborts_during_verification_when_shutdown_requested(void) |
650 | 652 | return result; |
651 | 653 | } |
652 | 654 |
|
| 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 | + |
653 | 794 | // Test restore functionality |
654 | 795 | static int test_restore(void) { |
655 | 796 | char stale_wal[256]; |
@@ -735,6 +876,20 @@ int main(void) { |
735 | 876 | return 1; |
736 | 877 | } |
737 | 878 |
|
| 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 | + |
738 | 893 | // Corrupt the database |
739 | 894 | if (corrupt_database() != 0) { |
740 | 895 | printf("Test failed: Could not corrupt database\n"); |
|
0 commit comments