Skip to content

Commit 97d82cf

Browse files
authored
Merge branch 'master' into zarenner/no-multiple-backup-hosts
2 parents 8211feb + bfdcc93 commit 97d82cf

File tree

4 files changed

+219
-28
lines changed

4 files changed

+219
-28
lines changed

share/github-backup-utils/ghe-backup-mssql

Lines changed: 152 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ set -e
1414
backup_dir="$GHE_SNAPSHOT_DIR/mssql"
1515
last_mssql=
1616
backup_command=
17-
take_full=
18-
take_diff=
17+
backup_type=
1918
full_expire=
2019
diff_expire=
2120
tran_expire=
@@ -53,23 +52,23 @@ find_timestamp() {
5352
echo $datetime_part
5453
}
5554

56-
ensure_same_dbs() {
55+
actions_dbs() {
5756
all_dbs=$(echo 'set -o pipefail; ghe-mssql-console -y -n -q "SET NOCOUNT ON; SELECT name FROM sys.databases"' | ghe-ssh "$GHE_HOSTNAME" /bin/bash)
58-
59-
remotes=()
6057
for db in $all_dbs; do
6158
if [[ ! "$db" =~ ^(master|tempdb|model|msdb)$ ]] && [[ "$db" =~ ^[a-zA-Z0-9_-]+$ ]]; then
62-
remotes+=("$db")
59+
echo "$db"
6360
fi
6461
done
62+
}
6563

64+
ensure_same_dbs() {
6665
locals=()
6766
while read -r file; do
6867
filename=$(basename "$file")
6968
locals+=("$filename")
7069
done < <(find "$1" \( -name "*.bak" -o -name "*.diff" -o -name "*.log" \))
7170

72-
for remote in "${remotes[@]}"; do
71+
for remote in $(actions_dbs); do
7372
remaining_locals=()
7473
for local in "${locals[@]}"; do
7574
if ! [[ "$local" == "$remote"* ]]; then
@@ -90,13 +89,67 @@ ensure_same_dbs() {
9089
fi
9190
}
9291

92+
run_query() {
93+
echo "set -o pipefail; ghe-mssql-console -y -n -q \"SET NOCOUNT ON; $1\"" | ghe-ssh "$GHE_HOSTNAME" /bin/bash | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
94+
}
95+
96+
get_latest_backup_file() {
97+
backups_dir=$1
98+
db=$2
99+
ext=$3
100+
101+
latest_full_backup=$(find "$backups_dir" -type f -name "$db*.$ext" | egrep '[0-9]{8}T[0-9]{6}' | sort | tail -n 1)
102+
latest_full_backup_file="${latest_full_backup##*/}"
103+
echo "$latest_full_backup_file"
104+
}
105+
106+
get_backup_val() {
107+
db=$1
108+
filename=$2
109+
column=$3
110+
run_query "
111+
SELECT s.$column
112+
FROM msdb.dbo.backupset s
113+
JOIN msdb.dbo.backupmediafamily f
114+
ON s.media_set_id = f.media_set_id
115+
WHERE s.database_name = '$db' AND f.physical_device_name LIKE '%$filename'"
116+
}
117+
118+
get_backup_checkpoint_lsn() {
119+
get_backup_val "$1" "$2" "checkpoint_lsn"
120+
}
121+
122+
get_backup_last_lsn() {
123+
get_backup_val "$1" "$2" "last_lsn"
124+
}
125+
126+
get_next_log_backup_starting_lsn() {
127+
db=$1
128+
# last_log_backup_lsn: The starting log sequence number of the next log backup
129+
# https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-recovery-status-transact-sql
130+
run_query "
131+
SELECT last_log_backup_lsn
132+
FROM sys.database_recovery_status drs
133+
JOIN sys.databases db on drs.database_id = db.database_id
134+
WHERE db.name = '$db'"
135+
}
136+
137+
get_next_diff_backup_base_lsn() {
138+
db=$1
139+
# differential_base_lsn: Base for differential backups. Data extents changed after this LSN will be included in a differential backup.
140+
# https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-master-files-transact-sql
141+
run_query "
142+
SELECT differential_base_lsn
143+
FROM sys.master_files mf
144+
WHERE mf.name = '$db'"
145+
}
146+
93147
last_mssql=$GHE_DATA_DIR/current/mssql
94148

95149
if [ ! -d $last_mssql ] \
96150
|| [ -z "$(find $last_mssql -type f -name '*.bak' | head -n 1)" ]; then
97151
ghe_verbose "Taking first full backup"
98-
take_full=1
99-
backup_command='ghe-export-mssql'
152+
backup_type="full"
100153
else
101154
ensure_same_dbs "$last_mssql"
102155

@@ -128,25 +181,89 @@ else
128181
ghe_verbose "current $current, full expire $full_expire, \
129182
diff expire $diff_expire, tran expire $tran_expire"
130183

131-
# Determine the type of backup to take
184+
# Determine the type of backup to take based on expiry time
132185
if [ $current -gt $full_expire ]; then
133-
ghe_verbose "Taking full backup"
134-
take_full=1
135-
backup_command='ghe-export-mssql'
186+
backup_type='full'
136187
elif [ $current -gt $diff_expire ]; then
137-
ghe_verbose "Taking diff backup"
138-
take_diff=1
139-
backup_command='ghe-export-mssql -d'
188+
backup_type='diff'
140189
elif [ $current -gt $tran_expire ]; then
141-
ghe_verbose "Taking transaction backup"
142-
backup_command='ghe-export-mssql -t'
190+
backup_type='transaction'
143191
fi
192+
193+
# Upgrade to a full backup if the diff/transaction backup might not be restorable due to other backup mechanisms interfering
194+
# with the transaction LSN chain or differential base LSN.
195+
if [ "$backup_type" == 'diff' ] || [ "$backup_type" == 'transaction' ]; then
196+
ghe_verbose "Checking for conflicting backups to ensure a $backup_type backup is sufficient"
197+
198+
for db in $(actions_dbs); do
199+
# Ensure that a diff backup will be based on the full backup file we have (rather than one another backup mechanism took)
200+
if [ "$backup_type" == 'diff' ]; then
201+
full_backup_file=$(get_latest_backup_file "$last_mssql" "$db" "bak")
202+
if [[ "$full_backup_file" == "" ]]; then
203+
ghe_verbose "Taking a full backup instead of a diff backup because for $db a full backup file wasn't found"
204+
backup_type="full"
205+
break
206+
fi
207+
208+
full_backup_file_checkpoint_lsn=$(get_backup_checkpoint_lsn "$db" "$full_backup_file")
209+
if [[ "$full_backup_file_checkpoint_lsn" = "NULL" ]] || [[ "$full_backup_file_checkpoint_lsn" == "" ]]; then
210+
ghe_verbose "Taking a full backup instead of a diff backup because for $db the checkpoint LSN for $full_backup_file couldn't be determined"
211+
backup_type="full"
212+
break
213+
fi
214+
215+
next_diff_backup_base_lsn=$(get_next_diff_backup_base_lsn "$db")
216+
if [[ "$next_diff_backup_base_lsn" = "NULL" ]] || [[ "$next_diff_backup_base_lsn" == "" ]]; then
217+
ghe_verbose "Taking a full backup instead of a $backup_type backup because for $db the base LSN for the next diff backup couldn't be determined"
218+
backup_type="full"
219+
break
220+
fi
221+
222+
# The base of the diff backup we're about to take must exactly match the checkpoint LSN of the full backup file we have
223+
if [[ "$next_diff_backup_base_lsn" -ne "$full_backup_file_checkpoint_lsn" ]]; then
224+
ghe_verbose "Taking a full backup instead of a $backup_type backup because for $db the diff would have base LSN $next_diff_backup_base_lsn yet our full backup has checkpoint LSN $full_backup_file_checkpoint_lsn"
225+
backup_type="full"
226+
break
227+
fi
228+
fi
229+
230+
# Ensure that a transaction log backup will immediately follow the previous one
231+
latest_log_backup_file=$(get_latest_backup_file "$last_mssql" "$db" "log")
232+
if [[ "$latest_log_backup_file" == "" ]]; then
233+
ghe_verbose "Taking a full backup instead of a $backup_type backup because for $db a previous transaction log backup wasn't found"
234+
backup_type="full"
235+
break
236+
fi
237+
238+
latest_log_backup_last_lsn=$(get_backup_last_lsn "$db" "$latest_log_backup_file")
239+
if [[ "$latest_log_backup_last_lsn" = "NULL" ]] || [[ "$latest_log_backup_last_lsn" == "" ]]; then
240+
ghe_verbose "Taking a full backup instead of a $backup_type backup because for $db the LSN range for $latest_log_backup_file couldn't be determined"
241+
backup_type="full"
242+
break
243+
fi
244+
245+
next_log_backup_starting_lsn=$(get_next_log_backup_starting_lsn "$db")
246+
if [[ "$next_log_backup_starting_lsn" = "NULL" ]] || [[ "$next_log_backup_starting_lsn" == "" ]]; then
247+
ghe_verbose "Taking a full backup instead of a $backup_type backup because for $db the starting LSN for the next log backup couldn't be determined"
248+
backup_type="full"
249+
break
250+
fi
251+
252+
# The starting LSN of the backup we're about to take must be equal to (or before) the last LSN from the last backup,
253+
# otherwise there'll be a gap and the logfiles won't be restorable
254+
if [[ "$next_log_backup_starting_lsn" -gt "$latest_log_backup_last_lsn" ]]; then
255+
ghe_verbose "Taking a full backup instead of a $backup_type backup because for $db a gap would exist between the last backup ending at LSN $latest_log_backup_last_lsn and next backup starting at $next_log_backup_starting_lsn"
256+
backup_type="full"
257+
break
258+
fi
259+
done
260+
fi
144261
fi
145262

146263
# Make sure root backup dir exists if this is the first run
147264
mkdir -p "$backup_dir"
148265

149-
# Create hard links to save disk space and time
266+
# Use hard links to "copy" over previous applicable backups to the new snapshot folder to save disk space and time
150267
if [ -d $last_mssql ]; then
151268
for p in $last_mssql/*
152269
do
@@ -156,15 +273,18 @@ if [ -d $last_mssql ]; then
156273
extension="${filename##*.}"
157274
transfer=
158275

159-
if [ $extension = "bak" ] && [ -z $take_full ]; then
276+
# Copy full backups unless we're taking a new full backup
277+
if [ $extension = "bak" ] && [ "$backup_type" != 'full' ]; then
160278
transfer=1
161279
fi
162280

163-
if [ $extension = "diff" ] && [ -z $take_full ] && [ -z $take_diff ]; then
281+
# Copy diff backups unless we're taking a new full or diff backup
282+
if [ $extension = "diff" ] && [ "$backup_type" != 'full' ] && [ "$backup_type" != 'diff' ]; then
164283
transfer=1
165284
fi
166285

167-
if [ $extension = "log" ] && [ -z $take_full ] && [ -z $take_diff ]; then
286+
# Copy transaction log backups unless we're taking a new full or diff backup
287+
if [ $extension = "log" ] && [ "$backup_type" != 'full' ] && [ "$backup_type" != 'diff' ]; then
168288
transfer=1
169289
fi
170290

@@ -175,7 +295,16 @@ if [ -d $last_mssql ]; then
175295
done
176296
fi
177297

178-
if [ -n "$backup_command" ]; then
298+
if [ -n "$backup_type" ]; then
299+
ghe_verbose "Taking $backup_type backup"
300+
301+
backup_command='ghe-export-mssql'
302+
if [ "$backup_type" = "diff" ]; then
303+
backup_command='ghe-export-mssql -d'
304+
elif [ "$backup_type" = "transaction" ]; then
305+
backup_command='ghe-export-mssql -t'
306+
fi
307+
179308
bm_start "$(basename $0)"
180309
ghe-ssh "$GHE_HOSTNAME" -- "$backup_command" || failures="$failures mssql"
181310
bm_end "$(basename $0)"

test/bin/ghe-mssql-console

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
11
#!/usr/bin/env bash
2-
# Returns environment variable REMOTE_DBS, the variable should be set as space-delimited string
32
# Tests use this to emulate ghe-mssql-console from a remote GitHub Enterprise Server
4-
echo $REMOTE_DBS
3+
if [[ "$*" == *"SELECT name FROM sys.databases"* ]]; then
4+
# REMOTE_DBS should be set as space-delimited string with the test database names
5+
echo "$REMOTE_DBS"
6+
elif [[ "$*" == *"last_log_backup_lsn"* ]]; then
7+
# Starting log sequence number of the next log backup.
8+
# Should match the most recent log backup last_lsn.
9+
echo "$NEXT_LOG_BACKUP_STARTING_LSN"
10+
elif [[ "$*" == *"last_lsn"* ]]; then
11+
# Last LSN of the transaction log backup file being looked up.
12+
echo "$LOG_BACKUP_FILE_LAST_LSN"
13+
elif [[ "$*" == *"differential_base_lsn"* ]]; then
14+
# Next diff backup base LSN. Should match full checkpoint LSN.
15+
echo "$DIFFERENTIAL_BASE_LSN"
16+
elif [[ "$*" == *"checkpoint_lsn"* ]]; then
17+
# Checkpoint LSN of the full backup file being looked up.
18+
echo "$FULL_BACKUP_FILE_LSN"
19+
else
20+
echo "UNKNOWN QUERY: ghe-mssql-console test stub failed on: $*"
21+
fi

test/test-ghe-backup.sh

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ begin_test "ghe-backup takes full backup upon expiration"
376376
set -e
377377
enable_actions
378378
enable_minio
379-
export REMOTE_DBS="full_mssql"
379+
setup_mssql_stubs
380380

381381
setup_mssql_backup_file "full_mssql" 11 "bak"
382382

@@ -391,7 +391,7 @@ begin_test "ghe-backup takes diff backup upon expiration"
391391
set -e
392392
enable_actions
393393
enable_minio
394-
export REMOTE_DBS="full_mssql"
394+
setup_mssql_stubs
395395

396396
setup_mssql_backup_file "full_mssql" 7 "bak"
397397

@@ -406,7 +406,7 @@ begin_test "ghe-backup takes transaction backup upon expiration"
406406
(
407407
set -e
408408
enable_actions
409-
export REMOTE_DBS="full_mssql"
409+
setup_mssql_stubs
410410

411411
setup_mssql_backup_file "full_mssql" 3 "bak"
412412

@@ -425,6 +425,7 @@ begin_test "ghe-backup warns if database names mismatched"
425425
rm -rf "$GHE_DATA_DIR/current/mssql"
426426
mkdir -p "$GHE_DATA_DIR/current/mssql"
427427

428+
setup_mssql_stubs
428429
export REMOTE_DBS="full_mssql_1 full_mssql_2 full_mssql_3"
429430

430431
add_mssql_backup_file "full_mssql_1" 3 "bak"
@@ -437,6 +438,38 @@ begin_test "ghe-backup warns if database names mismatched"
437438
)
438439
end_test
439440

441+
begin_test "ghe-backup upgrades diff backup to full if diff base mismatch"
442+
(
443+
set -e
444+
enable_actions
445+
setup_mssql_stubs
446+
export FULL_BACKUP_FILE_LSN=100
447+
export DIFFERENTIAL_BASE_LSN=101 # some other full backup interfered and moved up the diff base!
448+
449+
setup_mssql_backup_file "full_mssql" 7 "bak"
450+
451+
output=$(ghe-backup -v)
452+
echo "$output" | grep "Taking a full backup instead of a diff backup"
453+
echo "$output" | grep "Taking full backup"
454+
)
455+
end_test
456+
457+
begin_test "ghe-backup upgrades transaction backup to full if LSN chain break"
458+
(
459+
set -e
460+
enable_actions
461+
setup_mssql_stubs
462+
export LOG_BACKUP_FILE_LAST_LSN=100
463+
export NEXT_LOG_BACKUP_STARTING_LSN=101 # some other log backup interfered and stole 1 LSN!
464+
465+
setup_mssql_backup_file "full_mssql" 3 "bak"
466+
467+
output=$(ghe-backup -v)
468+
echo "$output" | grep "Taking a full backup instead of a transaction backup"
469+
echo "$output" | grep "Taking full backup"
470+
)
471+
end_test
472+
440473
begin_test "ghe-backup takes backup of Actions settings"
441474
(
442475
set -e

test/testlib.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,18 @@ setup_mssql_backup_file() {
528528
fi
529529
}
530530

531+
setup_mssql_stubs() {
532+
export REMOTE_DBS="full_mssql"
533+
534+
# Transaction log LSN checks
535+
export NEXT_LOG_BACKUP_STARTING_LSN=100
536+
export LOG_BACKUP_FILE_LAST_LSN=100
537+
538+
# Differential backup LSN checks
539+
export DIFFERENTIAL_BASE_LSN=100
540+
export FULL_BACKUP_FILE_LSN=100
541+
}
542+
531543
add_mssql_backup_file() {
532544
# $1 name: <name>@...
533545
# $2 minutes ago

0 commit comments

Comments
 (0)