Skip to content

Commit 1c1a710

Browse files
cmwshangdwsteele
authored andcommitted
Add --set option to the expire command.
The specified backup set (i.e. the backup label provided and all of its dependent backups, if any) will be expired regardless of backup retention rules except that at least one full backup must remain in the repository.
1 parent ad33f54 commit 1c1a710

File tree

11 files changed

+847
-9
lines changed

11 files changed

+847
-9
lines changed

build/lib/pgBackRestBuild/Config/Data.pm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,10 @@ my %hConfigDefine =
702702
&CFGDEF_TYPE => CFGDEF_TYPE_STRING,
703703
&CFGDEF_COMMAND =>
704704
{
705+
&CFGCMD_EXPIRE =>
706+
{
707+
&CFGDEF_REQUIRED => false,
708+
},
705709
&CFGCMD_INFO =>
706710
{
707711
&CFGDEF_REQUIRED => false,

doc/xml/reference.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,16 @@
11301130

11311131
<text><backrest/> does backup rotation but is not concerned with when the backups were created. If two full backups are configured for retention, <backrest/> will keep two full backups no matter whether they occur two hours or two weeks apart.</text>
11321132

1133+
<option-list>
1134+
<!-- OPERATION - EXPIRE COMMAND - SET OPTION -->
1135+
<option id="set" name="Set">
1136+
<summary>Backup set to expire.</summary>
1137+
1138+
<text>The specified backup set (i.e. the backup label provided and all of its dependent backups, if any) will be expired regardless of backup retention rules except that at least one full backup must remain in the repository. <admonition type="warning">Use this option with extreme caution &amp;mdash; it will permanently remove all backups and archives not required to make a backup consistent from the <backrest/> repository for the specified backup set. This process may negate the ability to perform PITR. If <br-option>--repo-retention-full</br-option> and/or <br-option>--repo-retention-archive</br-option> options are configured, then it is recommended that you override these options by setting their values to the maximum while performing adhoc expiration.</admonition></text>
1139+
<example>20150131-153358F_20150131-153401I</example>
1140+
</option>
1141+
</option-list>
1142+
11331143
<command-example-list>
11341144
<command-example>
11351145
<text><code-block title="">

doc/xml/release.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@
2525
</release-bug-list>
2626

2727
<release-feature-list>
28+
<release-item>
29+
<release-item-contributor-list>
30+
<release-item-contributor id="cynthia.shang"/>
31+
</release-item-contributor-list>
32+
33+
<p>Add <br-option>--set</br-option> option to the <cmd>expire</cmd> command.</p>
34+
35+
<p>Allow the user to remove a specified backup regardless of retention settings.</p>
36+
</release-item>
37+
2838
<release-item>
2939
<release-item-contributor-list>
3040
<release-item-contributor id="stefan.fercot"/>

src/command/expire/expire.c

Lines changed: 141 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Expire Command
1717
#include "storage/helper.h"
1818

1919
#include <stdlib.h>
20+
#include <stdio.h>
2021

2122
/***********************************************************************************************************************************
2223
Helper functions and structures
@@ -97,6 +98,93 @@ expireBackup(InfoBackup *infoBackup, const String *backupLabel)
9798
FUNCTION_LOG_RETURN(STRING_LIST, result);
9899
}
99100

101+
/***********************************************************************************************************************************
102+
Function to expire a selected backup (and all its dependents) regardless of retention rules.
103+
***********************************************************************************************************************************/
104+
static unsigned int
105+
expireAdhocBackup(InfoBackup *infoBackup, const String *backupLabel)
106+
{
107+
FUNCTION_LOG_BEGIN(logLevelDebug);
108+
FUNCTION_LOG_PARAM(INFO_BACKUP, infoBackup);
109+
FUNCTION_LOG_PARAM(STRING, backupLabel);
110+
FUNCTION_LOG_END();
111+
112+
ASSERT(infoBackup != NULL);
113+
ASSERT(backupLabel != NULL);
114+
115+
unsigned int result = 0;
116+
117+
MEM_CONTEXT_TEMP_BEGIN()
118+
{
119+
// If the label format is invalid, then error
120+
if (!regExpMatchOne(backupRegExpP(.full = true, .differential = true, .incremental = true), backupLabel))
121+
{
122+
THROW_FMT(OptionInvalidValueError, "'%s' is not a valid backup label format", strPtr(backupLabel));
123+
}
124+
125+
// If the label is not a current backup then notify user and exit
126+
if (infoBackupDataByLabel(infoBackup, backupLabel) == NULL)
127+
{
128+
LOG_WARN_FMT(
129+
"backup %s does not exist\nHINT: run the info command and confirm the backup is listed", strPtr(backupLabel));
130+
}
131+
else
132+
{
133+
// Get a list of all full backups with most recent in position 0
134+
StringList *fullList = strLstSort(infoBackupDataLabelList(infoBackup, backupRegExpP(.full = true)), sortOrderDesc);
135+
136+
// If the requested backup to expire is the latest full backup
137+
if (strCmp(strLstGet(fullList, 0), backupLabel) == 0)
138+
{
139+
// If the latest full backup requested is the only backup or the prior full backup is not for the same db-id
140+
// then the backup requested cannot be expired
141+
if (strLstSize(fullList) == 1 || infoBackupDataByLabel(infoBackup, backupLabel)->backupPgId !=
142+
infoBackupDataByLabel(infoBackup, strLstGet(fullList, 1))->backupPgId)
143+
{
144+
THROW_FMT(
145+
BackupSetInvalidError, "full backup %s cannot be expired until another full backup has been created",
146+
strPtr(backupLabel));
147+
}
148+
}
149+
150+
// Save off what is currently the latest backup (it may be removed if it is the adhoc backup or is a dependent of the
151+
// adhoc backup
152+
const String *latestBackup = infoBackupData(infoBackup, infoBackupDataTotal(infoBackup) - 1).backupLabel;
153+
154+
// Expire the requested backup and any dependents
155+
StringList *backupExpired = expireBackup(infoBackup, backupLabel);
156+
157+
// If the latest backup was removed, then update the latest link if not a dry-run
158+
if (infoBackupDataByLabel(infoBackup, latestBackup) == NULL)
159+
{
160+
// If retention settings have been configured, then there may be holes in the archives. For example, if the archive
161+
// for db-id=1 has 01,02,03,04,05 and F1 backup has archive start-stop 02-03 and rentention-full=1
162+
// (hence retention-archive=1 and retention-archive-type=full), then when F2 backup is created and assuming its
163+
// archive start-stop=05-06 then archives 01 and 04 will be removed resulting in F1 not being able to play through
164+
// PITR, which is expected. Now adhoc expire is attempted on F2 - it will be allowed but now there will be no
165+
// backups that can be recovered through PITR until the next full backup is created. Same problem for differential
166+
// backups with retention-diff.
167+
LOG_WARN_FMT(
168+
"expiring latest backup %s - the ability to perform point-in-time-recovery (PITR) may be affected\n"
169+
"HINT: non-default settings for '%s'/'%s' (even in prior expires) can cause gaps in the WAL.",
170+
strPtr(latestBackup), cfgOptionName(cfgOptRepoRetentionArchive), cfgOptionName(cfgOptRepoRetentionArchiveType));
171+
172+
// Adhoc expire is never performed through backup command so only check to determine if dry-run has been set or not
173+
if (!cfgOptionBool(cfgOptDryRun))
174+
backupLinkLatest(infoBackupData(infoBackup, infoBackupDataTotal(infoBackup) - 1).backupLabel);
175+
}
176+
177+
result = strLstSize(backupExpired);
178+
179+
// Log the expired backup list (prepend "set:" if there were any dependents that were also expired)
180+
LOG_INFO_FMT("expire adhoc backup %s%s", (result > 1 ? "set: " : ""), strPtr(strLstJoin(backupExpired, ", ")));
181+
}
182+
}
183+
MEM_CONTEXT_TEMP_END();
184+
185+
FUNCTION_LOG_RETURN(UINT, result);
186+
}
187+
100188
/***********************************************************************************************************************************
101189
Expire differential backups
102190
***********************************************************************************************************************************/
@@ -600,24 +688,59 @@ removeExpiredArchive(InfoBackup *infoBackup)
600688
Remove expired backups from repo
601689
***********************************************************************************************************************************/
602690
static void
603-
removeExpiredBackup(InfoBackup *infoBackup)
691+
removeExpiredBackup(InfoBackup *infoBackup, const String *adhocBackupLabel)
604692
{
605693
FUNCTION_LOG_BEGIN(logLevelDebug);
606694
FUNCTION_LOG_PARAM(INFO_BACKUP, infoBackup);
695+
FUNCTION_LOG_PARAM(STRING, adhocBackupLabel);
607696
FUNCTION_LOG_END();
608697

609698
ASSERT(infoBackup != NULL);
610699

611-
// Get all the current backups in backup.info
700+
// Get all the current backups in backup.info - these will not be expired
612701
StringList *currentBackupList = strLstSort(infoBackupDataLabelList(infoBackup, NULL), sortOrderDesc);
702+
703+
// Get all the backups on disk
613704
StringList *backupList = strLstSort(
614705
storageListP(
615706
storageRepo(), STORAGE_REPO_BACKUP_STR,
616707
.expression = backupRegExpP(.full = true, .differential = true, .incremental = true)),
617708
sortOrderDesc);
618709

710+
// Initialize the index to the lastest backup on disk
711+
unsigned int backupIdx = 0;
712+
713+
// Only remove the resumable backup if there is a possibility it is a dependent of the adhoc label being expired
714+
if (adhocBackupLabel != NULL)
715+
{
716+
String *manifestFileName = strNewFmt(
717+
STORAGE_REPO_BACKUP "/%s/" BACKUP_MANIFEST_FILE, strPtr(strLstGet(backupList, backupIdx)));
718+
String *manifestCopyFileName = strNewFmt("%s" INFO_COPY_EXT, strPtr(manifestFileName));
719+
720+
// If the latest backup is resumable (has a backup.manifest.copy but no backup.manifest)
721+
if (!storageExistsP(storageRepo(), manifestFileName) && storageExistsP(storageRepo(), manifestCopyFileName))
722+
{
723+
// If the resumable backup is not related to the expired adhoc backup then don't remove it
724+
if (!strBeginsWith(strLstGet(backupList, backupIdx), strSubN(adhocBackupLabel, 0, 16)))
725+
{
726+
backupIdx = 1;
727+
}
728+
// Else it may be related to the adhoc backup so check if its ancestor still exists
729+
else
730+
{
731+
Manifest *manifestResume = manifestLoadFile(
732+
storageRepo(), manifestFileName, cipherType(cfgOptionStr(cfgOptRepoCipherType)),
733+
infoPgCipherPass(infoBackupPg(infoBackup)));
734+
735+
// If the ancestor of the resumable backup still exists in backup.info then do not remove the resumable backup
736+
if (infoBackupDataByLabel(infoBackup, manifestData(manifestResume)->backupLabelPrior) != NULL)
737+
backupIdx = 1;
738+
}
739+
}
740+
}
741+
619742
// Remove non-current backups from disk
620-
for (unsigned int backupIdx = 0; backupIdx < strLstSize(backupList); backupIdx++)
743+
for (; backupIdx < strLstSize(backupList); backupIdx++)
621744
{
622745
if (!strLstExists(currentBackupList, strLstGet(backupList, backupIdx)))
623746
{
@@ -655,8 +778,19 @@ cmdExpire(void)
655778
storageRepo(), INFO_BACKUP_PATH_FILE_STR, cipherType(cfgOptionStr(cfgOptRepoCipherType)),
656779
cfgOptionStr(cfgOptRepoCipherPass));
657780

658-
expireFullBackup(infoBackup);
659-
expireDiffBackup(infoBackup);
781+
const String *adhocBackupLabel = NULL;
782+
783+
// If the --set option is valid (i.e. expire is called on its own) and is set then attempt to expire the requested backup
784+
if (cfgOptionValid(cfgOptSet) && cfgOptionTest(cfgOptSet))
785+
{
786+
adhocBackupLabel = cfgOptionStr(cfgOptSet);
787+
expireAdhocBackup(infoBackup, adhocBackupLabel);
788+
}
789+
else
790+
{
791+
expireFullBackup(infoBackup);
792+
expireDiffBackup(infoBackup);
793+
}
660794

661795
// Store the new backup info only if the dry-run mode is disabled
662796
if (!cfgOptionValid(cfgOptDryRun) || !cfgOptionBool(cfgOptDryRun))
@@ -666,7 +800,8 @@ cmdExpire(void)
666800
cfgOptionStr(cfgOptRepoCipherPass));
667801
}
668802

669-
removeExpiredBackup(infoBackup);
803+
// Remove all files on disk that are now expired
804+
removeExpiredBackup(infoBackup, adhocBackupLabel);
670805
removeExpiredArchive(infoBackup);
671806
}
672807
MEM_CONTEXT_TEMP_END();

src/config/define.auto.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4366,12 +4366,33 @@ static ConfigDefineOptionData configDefineOptionData[] = CFGDEFDATA_OPTION_LIST
43664366

43674367
CFGDEFDATA_OPTION_COMMAND_LIST
43684368
(
4369+
CFGDEFDATA_OPTION_COMMAND(cfgDefCmdExpire)
43694370
CFGDEFDATA_OPTION_COMMAND(cfgDefCmdInfo)
43704371
CFGDEFDATA_OPTION_COMMAND(cfgDefCmdRestore)
43714372
)
43724373

43734374
CFGDEFDATA_OPTION_OPTIONAL_LIST
43744375
(
4376+
CFGDEFDATA_OPTION_OPTIONAL_COMMAND_OVERRIDE
4377+
(
4378+
CFGDEFDATA_OPTION_OPTIONAL_COMMAND(cfgDefCmdExpire)
4379+
4380+
CFGDEFDATA_OPTION_OPTIONAL_REQUIRED(false)
4381+
4382+
CFGDEFDATA_OPTION_OPTIONAL_HELP_SUMMARY("Backup set to expire.")
4383+
CFGDEFDATA_OPTION_OPTIONAL_HELP_DESCRIPTION
4384+
(
4385+
"The specified backup set (i.e. the backup label provided and all of its dependent backups, if any) will be "
4386+
"expired regardless of backup retention rules except that at least one full backup must remain in the "
4387+
"repository. \n"
4388+
"WARNING: Use this option with extreme caution -- it will permanently remove all backups and archives not "
4389+
"required to make a backup consistent from the pgBackRest repository for the specified backup set. This "
4390+
"process may negate the ability to perform PITR. If --repo-retention-full and/or --repo-retention-archive "
4391+
"options are configured, then it is recommended that you override these options by setting their values to "
4392+
"the maximum while performing adhoc expiration."
4393+
)
4394+
)
4395+
43754396
CFGDEFDATA_OPTION_OPTIONAL_COMMAND_OVERRIDE
43764397
(
43774398
CFGDEFDATA_OPTION_OPTIONAL_COMMAND(cfgDefCmdInfo)

test/define.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ unit:
607607

608608
# ----------------------------------------------------------------------------------------------------------------------------
609609
- name: expire
610-
total: 7
610+
total: 8
611611

612612
coverage:
613613
command/expire/expire: full

test/expect/real-all-001.log

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,38 @@ spool-path=[TEST_PATH]/db-master/spool
100100
archive-copy=y
101101
start-fast=y
102102

103+
diff backup - backup for adhoc expire (db-master host)
104+
> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --type=diff --stanza=db backup
105+
------------------------------------------------------------------------------------------------------------------------------------
106+
107+
+ supplemental file: [TEST_PATH]/db-master/pgbackrest.conf
108+
----------------------------------------------------------
109+
[db]
110+
pg1-path=[TEST_PATH]/db-master/db/base
111+
pg1-port=6543
112+
pg1-socket-path=[TEST_PATH]/db-master/db
113+
114+
[global]
115+
archive-async=y
116+
buffer-size=[BUFFER-SIZE]
117+
compress-level=3
118+
compress-type=none
119+
db-timeout=45
120+
lock-path=[TEST_PATH]/db-master/lock
121+
log-level-console=detail
122+
log-level-file=[LOG-LEVEL-FILE]
123+
log-level-stderr=off
124+
log-path=[TEST_PATH]/db-master/log
125+
log-subprocess=[LOG-SUBPROCESS]
126+
log-timestamp=n
127+
protocol-timeout=60
128+
repo1-path=[TEST_PATH]/db-master/repo
129+
spool-path=[TEST_PATH]/db-master/spool
130+
131+
[global:backup]
132+
archive-copy=y
133+
start-fast=y
134+
103135
stop all stanzas (db-master host)
104136
> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf stop
105137
------------------------------------------------------------------------------------------------------------------------------------
@@ -116,6 +148,10 @@ incr backup - fail on archive_mode=always (db-master host)
116148
> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --stanza=db backup
117149
------------------------------------------------------------------------------------------------------------------------------------
118150

151+
expire --set=[BACKUP-DIFF-1] (db-master host)
152+
> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --set=[BACKUP-DIFF-1] --stanza=db expire
153+
------------------------------------------------------------------------------------------------------------------------------------
154+
119155
incr backup - update during backup (db-master host)
120156
> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --stop-auto --buffer-size=[BUFFER-SIZE] --delta --stanza=db backup
121157
------------------------------------------------------------------------------------------------------------------------------------

0 commit comments

Comments
 (0)