From 4ffb6f2c5597c74beba54688d9fe352bb6f50fbd Mon Sep 17 00:00:00 2001 From: Winfried Plappert Date: Sun, 2 Mar 2025 08:41:12 +0000 Subject: [PATCH] new implementation of limiting repository size during backup Changed saveAndEncrypt(...) to include tracking of current size and raising an error "MaxCapacityExceeded" when the limit has been exceeded. Added CurrentRepositorySize(ctx) to report current size Added MaxCapacityExceeded() to query if limit has been execced Added IsRepositoryLimitActive() to query if size monitoring is active In addition an interface definition added to fulfill the needs of internal/archiver for accessing repository functions. The issue descrition has been updated. --- changelog/unreleased/issue-4585 | 29 +++++++++++++++ internal/repository/repository.go | 59 ++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 changelog/unreleased/issue-4585 diff --git a/changelog/unreleased/issue-4585 b/changelog/unreleased/issue-4585 new file mode 100644 index 000000000..bcea9c83d --- /dev/null +++ b/changelog/unreleased/issue-4585 @@ -0,0 +1,29 @@ +Enhancement: Limit repository size to a predefined maximum size during vackup + +Restic backup can now limit the repository size to a given maximum value. The +repository size limit can be specified as option `--max-repo-size`, with +the usual meaning for size related options. + +During backup, the current size is monitored by calculating its actual repository +size plus the size of any new blob added during the backup process. Once the +defined limit is exceeded, backup is prevented from creating new backup entries +and the 'in progress' files are finalized, without adding large amounts of new data. + +The size limit is a rough limit and cannot be taken as a precise limit, +since indexes and snapshot file have to be finalized. +Due to the highly parallel processing of a backup process +the limit can be overshot by `packfile size` multiple times. +With a current default of 16m for a packfile, +you might experience differences of multiple megabytes between the expected limit +and the actual size when the backup process terminates. + +The new packfiles are created by the current backup process and a new snapshot +will be created. + +Currently the implemenation is incomplete. There are currently two indications +that there is a incomplete backup: +field `PartialSnapshot` exist and is set to true and +the snapshot is tagged as `partial-snapshot`. + +https://github.com/restic/restic/issues/4583 +https://github.com/restic/restic/pull/5215 diff --git a/internal/repository/repository.go b/internal/repository/repository.go index aee0db103..c1243d5ef 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -51,6 +51,8 @@ type Repository struct { allocDec sync.Once enc *zstd.Encoder dec *zstd.Decoder + + MaxRepoCapReached bool } // internalRepository allows using SaveUnpacked and RemoveUnpacked with all FileTypes @@ -62,6 +64,8 @@ type Options struct { Compression CompressionMode PackSize uint NoExtraVerify bool + RepoSizeMax uint64 + repoCurSize uint64 } // CompressionMode configures if data should be compressed. @@ -391,7 +395,60 @@ func (r *Repository) saveAndEncrypt(ctx context.Context, t restic.BlobType, data panic(fmt.Sprintf("invalid type: %v", t)) } - return pm.SaveBlob(ctx, t, id, ciphertext, uncompressedLength) + length, err := pm.SaveBlob(ctx, t, id, ciphertext, uncompressedLength) + + var m sync.Mutex + + // maximum repository capacity exceeded? + m.Lock() + defer m.Unlock() + if r.opts.RepoSizeMax > 0 { + r.opts.repoCurSize += uint64(length) + if r.opts.repoCurSize > r.opts.RepoSizeMax { + r.MaxRepoCapReached = true + debug.Log("MaxCapacityExceeded") + return length, errors.New("MaxCapacityExceeded") + } + } + return length, err +} + +// CurrentRepositorySize counts the sizes of the filetypes snapshot, index and packs +func (r *Repository) CurrentRepositorySize(ctx context.Context) (uint64, error) { + curSize := uint64(0) + if r.opts.RepoSizeMax > 0 { + for _, ft := range []restic.FileType{restic.SnapshotFile, restic.IndexFile, restic.PackFile} { + err := r.List(ctx, ft, func(_ restic.ID, size int64) error { + curSize += uint64(size) + return nil + }) + if err != nil { + return 0, err + } + } + r.opts.repoCurSize = curSize + return curSize, nil + } + + return 0, errors.New("repository maximum size has not been set") +} + +// MaxCapacityExceeded reports if repository has a limit and if it is exceeded +func (r *Repository) MaxCapacityExceeded() bool { + if r.opts.RepoSizeMax == 0 { + return false + } + return r.MaxRepoCapReached +} + +func (r *Repository) IsRepositoryLimitActive() bool { + return r.opts.RepoSizeMax > 0 +} + +// CapacityChecker has to satisfy restic.Repository interface needs +type CapacityChecker interface { + MaxCapacityExceeded() bool + IsRepositoryLimitActive() bool } func (r *Repository) verifyCiphertext(buf []byte, uncompressedLength int, id restic.ID) error {