From 80ace2fb9e6df41ea07a27302efb40760bb07a55 Mon Sep 17 00:00:00 2001 From: Mark Stosberg Date: Tue, 18 Oct 2016 20:50:53 -0400 Subject: [PATCH] 1.1.0: Changing default calculation of `now` for safety and improving docs. --- History.md | 10 ++++++++++ README.md | 35 ++++++++++++++++++++++++++++++++--- filters.js | 3 ++- index.js | 2 +- package.json | 2 +- test/filters.js | 18 ++++++++++++++++++ 6 files changed, 64 insertions(+), 6 deletions(-) diff --git a/History.md b/History.md index 17f504c..4606030 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,14 @@ +## [1.1.0] - 2016-10-18 + +### Behavior change + + * Default value of `now` was changed to match the timestamp of the most recent backup. This is safer, erring on the side of retaining more data. See README for details. + +### Documentation + + * Document was improved to explain in more detail how the calculations work + ## [1.0.0] - 2016-10-18 ### Incompatible Changes diff --git a/README.md b/README.md index 12da8c0..61e90e3 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,12 @@ of space used. * `months` : Number of most recent one-per-month dates to keep. Defaults to 0. * `years` : Number of most recent one-per-year dates to keep. Defaults to 0. * `hours` : Number of most recent one-per-hour dates to keep. Defaults to 0. - * `now` : The date to use as 'now' when calculating 'recent'. All "future" dates after this moment are kept. Expected to be a moment object. Defaults to the current moment. + * `now` : The date to use as 'now' when calculating 'recent'. All "future" dates after this moment are kept. Expected to be a moment object. Defaults to the timestamp of the newest backup. * `firstweekday` The first day of the week to consider when calculating weekly dates to keep. Defaults to Saturday. Valid values are 0-6 (Sunday-Saturday) You provide details of dates you want to keep and `toDelete` returns a filtered list of the dates with dates-to-keep removed. The values in the returned array are moment objects sorted from oldest to newest. + ### toKeep(datetimes, options) The same as `toDelete` above, but returning the array of dates to keep instead of the dates to delete. @@ -63,6 +64,34 @@ Once all the passes have been made, the final set to keep is the union of all th The backups to delete are simply calculated by removing the backups to keep from the total set. +### Backups kept on are based on the the date range, not necessarily the number. + +When set you `days:7`, you are expressing to keep up to 7 backups for the last 7 days. All backups older than 7 days would be deleted, no matter how few +backups you happen to have in the last 7 days. + +If there are gaps in your backups, this logic is *not* the same as "keep 7 daily backups". We make this +safer in case your backups are interrupted by calculating 'now' from the most recent backup. More on that below. + +### Backups to keep start with the current unit of time. + +If 'now' is set to the current moment and you say you want to keep `days: 3`, we will keep backups +for TODAY and the two days previous. (NOT yesterday and the two days previous). The same works for the current unit of time. + +### Calculating `now`: Backups to delete are calculated from the newest backup. + +For safety, the default value of 'now' we use is the timestamp of the newest backup. + +If you are making snapshots every day and pruning them later the same day, this produces exactly the same result as setting the default value of 'now' to the current moment when rotating daily backups. + +However, if you happen to expire your daily snapshots *before* making your daily snapshots, this default value will provide you one more backup rather than one less. Consider a plan +to set `days:7`. If `now` were set to the current date and the backup for today hadn't been generated, you would be pruning to 6 daily backups until the backup runs later in the day. +Since we default to setting `now` to the newest backup-- yesterdays-- we would retain 7 backups when pruning. When the backup ran later in the day, you would have 8 until the next +pruning happenings. + +Also consider a case where you are backing up daily and want to keep 7 daily backups. There's a problem with the backups running and backups are offline for a week. If we used the current moment to calculate how many days of backups to keep, all the daily backups would be deleted after 7 days of backups being offline. But if 'now' were set to the date of the newest backup, you would always have the 7 most recent backups. + +You can set 'now' to `moment.utc()` if you want prioritize making sure your backups are always being deleted on time and are prepared for the risk of data loss if your backup cycles don't get run but your snapshot expiration cycles do! + ## Example Say you have daily backups from every day in 1999 and today is the last day of year. You'd like to keep daily @@ -99,7 +128,7 @@ The above example is tested in the test suite. ## Time zone handling -Incoming dates are always passed to 'moment.utc()`. If your timestamps are not in UTC, you could carefully +Incoming dates are always passed to `moment.utc()`. If your timestamps are not in UTC, you could carefully check that the results are what you expect. There is no test coverage for not UTC time stamps. ## Node versions supported @@ -122,4 +151,4 @@ Simon Law and Rory Geoghegan. ## License grandfatherson is distributed under the BSD 3 clause lisence. See the -LICENSE file for more details. +[LICENSE](./LICENSE) file for more details. diff --git a/filters.js b/filters.js index bf20986..0f19219 100644 --- a/filters.js +++ b/filters.js @@ -73,7 +73,8 @@ class Filter { if(a.isAfter(b)) return 1; }); - options.now = _.defaultTo(options.now, moment.utc()); + // As a safety measure, default now to the most recent date. + options.now = _.defaultTo(options.now, _.last(sortedDateTimes)); // Always keep datetimes from the future var future = _.filter(sortedDateTimes, function (dt) { return dt.isAfter(options.now); }) diff --git a/index.js b/index.js index 64091a5..9d5cd2c 100644 --- a/index.js +++ b/index.js @@ -17,7 +17,7 @@ function toKeep (datetimes, options) { hours : 0, seconds : 0, firstweekday : SATURDAY, - now : moment.utc(), + now : undefined, // Default is defined in the filter function. No need to duplicate it here. }); diff --git a/package.json b/package.json index 9b30f98..48ef4c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grandfatherson", - "version": "1.0.0", + "version": "1.1.0", "keywords": ["backup", "rotation", "algorithm", "snapshots" ], "description": "Grandfather Father Son algorithm, useful for backup rotation", "main": "index.js", diff --git a/test/filters.js b/test/filters.js index 059cd65..8800cef 100644 --- a/test/filters.js +++ b/test/filters.js @@ -3,6 +3,7 @@ var moment = require('moment'); var chai = require('chai'); var expect = chai.expect; +var _ = require('lodash'); var filters = require('../filters'); var Seconds = filters.Seconds; var Minutes = filters.Minutes; @@ -90,6 +91,7 @@ describe('Filters', function() { expect(Seconds.filter(datetimes, {number:0, now:now})).to.eql(datetimes); }); + it('no input returns empty set', function () { expect(Seconds.filter([], {number:0, now:now})).to.eql([]); }); @@ -628,6 +630,22 @@ describe('Filters', function() { expect(Years.filter(datetimes, {number:0, now:now})).to.eql([]); }); + // If now defaulted to the current moment, all the backups would be deleted. + // By defaulting to the date of the newest backup, we would keep these "newest 3" by default instead. + it('should default "now" to the newest datetime', function () { + var datetimes = [ + moment.utc({year: 2002}), + moment.utc({year: 2003}), + moment.utc({year: 2001}) + ]; + + var result = Years.filter(datetimes, {number:3}); + + expect(result.length).to.equal(3); + expect(_.last(result).toISOString()).to.equal(moment.utc({year: 2003}).toISOString()); + }); + + it('for dates that end up with the same mask, return the oldest', function () { var filtered = Years.filter(datetimes, {number:2, now:now}); //filtered.map(function (dt) { console.log(dt.toString()); });