Backup planner
An order system's changes hour by hour against a one-hour recovery point objective: a nightly backup that has never failed meets it for 1 hour of 24, and every archive carries the key that encrypts it.
Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.
About
An order system's changes hour by hour, a recovery point objective of one hour, three backup schedules, and what one archive holds.
main.eml- the menu, the system, a failure at each hour, what an archive holds, and a schedule of your ownbackup.eml- the changes in each hour, the objective, the schedules, the age of the last backup before a failure and what it loses, and the archive's contents
How each part works:
- 300 changes an hour at night, 4,200 in office hours, 51,100 a day. A failure at h:00 loses everything written since the last backup before it; the objective is to lose at most one hour of work.
- As in the corpus case
the-backup-ran-nightly-and-the-change-was-hourly, a nightly backup at 02:00 that has succeeded 400 nights in a row is within the objective for 1 of the 24 failure hours, and a failure just before it loses the whole day, 51,100 changes. Every 6 hours meets it for 4 hours of 24 and loses up to 25,200; only an incremental every hour meets it for all 24. The success count says the archive was made; the schedule alone says how old it is when it is needed. - As in
the-backup-was-encrypted-and-the-key-was-in-the-backup, the archive is a snapshot of the whole host, and the key it is encrypted with is a file on that host: with 90 archives and 3 copies of each off site, the key is in 270 places, each next to the data it protects.
What is checked: menu choices 1 to 5; a first backup hour from 0 to 23 and a backup every 1 to 24 hours. An empty answer cancels.
Sessions: sessions/basic.in gives: - the system, a failure at each hour, and the archive; - your own schedules: from 02:00 every 5 hours, from 00:00 every hour, and once a day at 09:00.
sessions/bad-input.in gives: - menu choices 0 and x; - first hours: a cancel, 24 and x; then intervals with a cancel, 0, 25 and x; - from 23:00 every 7 hours, and from 03:00 every 12.
Built on the verified corpus cases the-backup-ran-nightly-and-the-change-was-hourly (a nightly backup, 400 successes, and a one-hour recovery point objective) and the-backup-was-encrypted-and-the-key-was-in-the-backup (encrypted host snapshots that hold the key material the host needs to start).
Recorded sessions
What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.
bad-input
interpreter: byte-equal== Backup planner ==
Backups that succeed every night, and what a failure still loses.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 0
Pick a number from 1 to 5.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> x
Pick a number from 1 to 5.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 4
first backup hour (0 to 23)>
Cancelled.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 4
first backup hour (0 to 23)> 24
Type a whole number from 0 to 23.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 4
first backup hour (0 to 23)> x
Type a whole number from 0 to 23.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 4
first backup hour (0 to 23)> 3
every how many hours (1 to 24)>
Cancelled.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 4
first backup hour (0 to 23)> 3
every how many hours (1 to 24)> 0
Type a whole number from 1 to 24.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 4
first backup hour (0 to 23)> 3
every how many hours (1 to 24)> 25
Type a whole number from 1 to 24.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 4
first backup hour (0 to 23)> 3
every how many hours (1 to 24)> x
Type a whole number from 1 to 24.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 4
first backup hour (0 to 23)> 23
every how many hours (1 to 24)> 7
Backups at 23:00, 06:00, 13:00 and 20:00.
This schedule: within the objective for 4 of 24 failure hours; the worst, at 20:00, loses 24,000 changes, 7 hours of work.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 4
first backup hour (0 to 23)> 3
every how many hours (1 to 24)> 12
Backups at 03:00 and 15:00.
This schedule: within the objective for 2 of 24 failure hours; the worst, at 15:00, loses 30,400 changes, 12 hours of work.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 5
Bye.
What was typed (27 lines)
0
x
4
4
24
4
x
4
3
4
3
0
4
3
25
4
3
x
4
23
7
4
3
12
5
basic
interpreter: byte-equal== Backup planner ==
Backups that succeed every night, and what a failure still loses.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 1
An order system, hour by hour: 300 changes an hour from 00:00 to 07:00, 2,000 to 09:00, 4,200 to 18:00, 1,500 to 22:00 and 600 to midnight - 51,100 a day.
The recovery point objective: lose at most 1 hour of work.
The backups: nightly at 02:00, which has succeeded 400 nights in a row; every 6 hours; and nightly with an incremental every hour.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 2
A failure at each hour, the changes lost since the last backup, and their age (ok: within the objective):
failure nightly at 02:00 every 6 hours nightly + hourly
00:00 50,500 22h 7,200 6h 600 1h ok
01:00 50,800 23h 300 1h ok 300 1h ok
02:00 51,100 24h 600 2h 300 1h ok
03:00 300 1h ok 900 3h 300 1h ok
04:00 600 2h 1,200 4h 300 1h ok
05:00 900 3h 1,500 5h 300 1h ok
06:00 1,200 4h 1,800 6h 300 1h ok
07:00 1,500 5h 300 1h ok 300 1h ok
08:00 3,500 6h 2,300 2h 2,000 1h ok
09:00 5,500 7h 4,300 3h 2,000 1h ok
10:00 9,700 8h 8,500 4h 4,200 1h ok
11:00 13,900 9h 12,700 5h 4,200 1h ok
12:00 18,100 10h 16,900 6h 4,200 1h ok
13:00 22,300 11h 4,200 1h ok 4,200 1h ok
14:00 26,500 12h 8,400 2h 4,200 1h ok
15:00 30,700 13h 12,600 3h 4,200 1h ok
16:00 34,900 14h 16,800 4h 4,200 1h ok
17:00 39,100 15h 21,000 5h 4,200 1h ok
18:00 43,300 16h 25,200 6h 4,200 1h ok
19:00 44,800 17h 1,500 1h ok 1,500 1h ok
20:00 46,300 18h 3,000 2h 1,500 1h ok
21:00 47,800 19h 4,500 3h 1,500 1h ok
22:00 49,300 20h 6,000 4h 1,500 1h ok
23:00 49,900 21h 6,600 5h 600 1h ok
nightly at 02:00: within the objective for 1 of 24 failure hours; the worst, at 02:00, loses 51,100 changes, 24 hours of work.
every 6 hours: within the objective for 4 of 24 failure hours; the worst, at 18:00, loses 25,200 changes, 6 hours of work.
nightly + hourly: every failure hour within the objective; the most a failure loses is 4,200 changes.
400 nights of success say the archive was made. How old it is when it is needed is set by the schedule alone.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 3
What one archive holds - a snapshot of the whole application host:
/srv/orders/db the order database 48,000 MB
/etc/orders/app.conf the application's settings 1 MB
/etc/orders/backup.key the key the archive is encrypted with 1 MB
90 archives are kept, each with 3 copies off site: the key is in 270 places, each next to the data it protects. Anyone who holds one archive can open it.
With /etc/orders/backup.key left out of the snapshot, a restore asks the key service for the key: no copy in any archive, and one more step in the drill.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 4
first backup hour (0 to 23)> 2
every how many hours (1 to 24)> 5
Backups at 02:00, 07:00, 12:00, 17:00 and 22:00.
This schedule: within the objective for 5 of 24 failure hours; the worst, at 17:00, loses 21,000 changes, 5 hours of work.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 4
first backup hour (0 to 23)> 0
every how many hours (1 to 24)> 1
Backups at 00:00, 01:00, 02:00, 03:00, 04:00, 05:00, 06:00, 07:00, 08:00, 09:00, 10:00, 11:00, 12:00, 13:00, 14:00, 15:00, 16:00, 17:00, 18:00, 19:00, 20:00, 21:00, 22:00 and 23:00.
This schedule: every failure hour within the objective; the most a failure loses is 4,200 changes.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 4
first backup hour (0 to 23)> 9
every how many hours (1 to 24)> 24
Backups at 09:00.
This schedule: within the objective for 1 of 24 failure hours; the worst, at 09:00, loses 51,100 changes, 24 hours of work.
1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit
choice> 5
Bye.
What was typed (13 lines)
1
2
3
4
2
5
4
0
1
4
9
24
5
Modules
The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.
main.eml(entry)
eml# P167 backup planner: an order system's changes hour by hour against a
# one-hour recovery point objective - the system, a failure at each hour
# under three schedules, what one archive holds, and a schedule of your own.
import backup
def plain(s):
0 => i
len(s) => j
while i < j and s[i] == " ":
i + 1 => i
while j > i and s[j - 1] == " ":
j - 1 => j
return s[i:j]
def pad(s, n):
s => out
while len(out) < n:
out + " " => out
return out
def lpad(s, n):
s => out
while len(out) < n:
" " + out => out
return out
def digits(t):
for c in t:
if c < "0" or c > "9":
return False
return len(t) > 0
def commas(n):
str(n) => s
"" => out
0 => k
len(s) => i
while i > 0:
if k == 3:
"," + out => out
0 => k
s[i - 1] + out => out
k + 1 => k
i - 1 => i
return out
def clock(h):
if h < 10:
return "0" + str(h) + ":00"
return str(h) + ":00"
def clocks(hours):
"" => out
0 => i
while i < len(hours):
if i > 0 and i == len(hours) - 1:
out + " and " => out
elif i > 0:
out + ", " => out
out + clock(hours[i]) => out
i + 1 => i
return out
def hours_word(n):
if n == 1:
return "1 hour"
return str(n) + " hours"
def show_system():
("An order system, hour by hour: 300 changes an hour from 00:00 to 07:00, 2,000 to 09:00, 4,200 to 18:00, 1,500 to 22:00 and 600 to midnight - " + commas(backup.per_day()) + " a day.") ^0
("The recovery point objective: lose at most " + hours_word(backup.rpo()) + " of work.") ^0
"The backups: nightly at 02:00, which has succeeded 400 nights in a row; every 6 hours; and nightly with an incremental every hour." ^0
def summary(sc):
# [failure hours within the objective, the worst hour, its loss, its age]
0 => within
0 => worst
0 => h
while h < 24:
if backup.age(sc, h) <= backup.rpo():
within + 1 => within
if backup.lost(sc, h) > backup.lost(sc, worst):
h => worst
h + 1 => h
return [within, worst, backup.lost(sc, worst), backup.age(sc, worst)]
def verdict(name, sc):
summary(sc) => s
if s[0] == 24:
return name + ": every failure hour within the objective; the most a failure loses is " + commas(s[2]) + " changes."
return name + ": within the objective for " + str(s[0]) + " of 24 failure hours; the worst, at " + clock(s[1]) + ", loses " + commas(s[2]) + " changes, " + hours_word(s[3]) + " of work."
def cell(sc, h):
backup.age(sc, h) => a
lpad(commas(backup.lost(sc, h)), 7) + lpad(str(a) + "h", 4) => out
if a <= backup.rpo():
out + " ok" => out
else:
out + " " => out
return out
def show_failures():
"A failure at each hour, the changes lost since the last backup, and their age (ok: within the objective):" ^0
backup.schedules() => scs
(" " + pad("failure", 9) + pad(scs[0][0], 19) + pad(scs[1][0], 19) + scs[2][0]) ^0
0 => h
while h < 24:
(" " + pad(clock(h), 9) + pad(cell(scs[0][1], h), 19) + pad(cell(scs[1][1], h), 19) + cell(scs[2][1], h)) ^0
h + 1 => h
for sc in scs:
verdict(sc[0], sc[1]) ^0
"400 nights of success say the archive was made. How old it is when it is needed is set by the schedule alone." ^0
def show_archive():
"What one archive holds - a snapshot of the whole application host:" ^0
for f in backup.archive():
(" " + pad(f[0], 24) + pad(f[1], 40) + lpad(commas(f[2]) + " MB", 10)) ^0
backup.kept() => k
(str(k[0]) + " archives are kept, each with " + str(k[1]) + " copies off site: the key is in " + str(k[0] * k[1]) + " places, each next to the data it protects. Anyone who holds one archive can open it.") ^0
"With /etc/orders/backup.key left out of the snapshot, a restore asks the key service for the key: no copy in any archive, and one more step in the drill." ^0
def own():
plain(input("first backup hour (0 to 23)> ")) => t
if t == "":
"Cancelled." ^0
return
if len(t) > 2 or not digits(t) or int(t) > 23:
"Type a whole number from 0 to 23." ^0
return
plain(input("every how many hours (1 to 24)> ")) => n
if n == "":
"Cancelled." ^0
return
if len(n) > 2 or not digits(n) or int(n) < 1 or int(n) > 24:
"Type a whole number from 1 to 24." ^0
return
backup.every(int(t), int(n)) => sc
("Backups at " + clocks(sc) + ".") ^0
verdict("This schedule", sc) ^0
"== Backup planner ==" ^0
"Backups that succeed every night, and what a failure still loses." ^0
True => running
while running:
"" ^0
"1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit" ^0
plain(input("choice> ")) => choice
if choice == "1":
show_system()
elif choice == "2":
show_failures()
elif choice == "3":
show_archive()
elif choice == "4":
own()
elif choice == "5":
False => running
else:
"Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import backup
def plain(s):
i = 0
j = len(s)
while i < j and s[i] == " ":
i = i + 1
while j > i and s[j - 1] == " ":
j = j - 1
return s[i:j]
def pad(s, n):
out = s
while len(out) < n:
out = out + " "
return out
def lpad(s, n):
out = s
while len(out) < n:
out = " " + out
return out
def digits(t):
for c in t:
if c < "0" or c > "9":
return False
return len(t) > 0
def commas(n):
s = str(n)
out = ""
k = 0
i = len(s)
while i > 0:
if k == 3:
out = "," + out
k = 0
out = s[i - 1] + out
k = k + 1
i = i - 1
return out
def clock(h):
if h < 10:
return "0" + str(h) + ":00"
return str(h) + ":00"
def clocks(hours):
out = ""
i = 0
while i < len(hours):
if i > 0 and i == len(hours) - 1:
out = out + " and "
elif i > 0:
out = out + ", "
out = out + clock(hours[i])
i = i + 1
return out
def hours_word(n):
if n == 1:
return "1 hour"
return str(n) + " hours"
def show_system():
print("An order system, hour by hour: 300 changes an hour from 00:00 to 07:00, 2,000 to 09:00, 4,200 to 18:00, 1,500 to 22:00 and 600 to midnight - " + commas(backup.per_day()) + " a day.")
print("The recovery point objective: lose at most " + hours_word(backup.rpo()) + " of work.")
print("The backups: nightly at 02:00, which has succeeded 400 nights in a row; every 6 hours; and nightly with an incremental every hour.")
def summary(sc):
within = 0
worst = 0
h = 0
while h < 24:
if backup.age(sc, h) <= backup.rpo():
within = within + 1
if backup.lost(sc, h) > backup.lost(sc, worst):
worst = h
h = h + 1
return [within, worst, backup.lost(sc, worst), backup.age(sc, worst)]
def verdict(name, sc):
s = summary(sc)
if s[0] == 24:
return name + ": every failure hour within the objective; the most a failure loses is " + commas(s[2]) + " changes."
return name + ": within the objective for " + str(s[0]) + " of 24 failure hours; the worst, at " + clock(s[1]) + ", loses " + commas(s[2]) + " changes, " + hours_word(s[3]) + " of work."
def cell(sc, h):
a = backup.age(sc, h)
out = lpad(commas(backup.lost(sc, h)), 7) + lpad(str(a) + "h", 4)
if a <= backup.rpo():
out = out + " ok"
else:
out = out + " "
return out
def show_failures():
print("A failure at each hour, the changes lost since the last backup, and their age (ok: within the objective):")
scs = backup.schedules()
print(" " + pad("failure", 9) + pad(scs[0][0], 19) + pad(scs[1][0], 19) + scs[2][0])
h = 0
while h < 24:
print(" " + pad(clock(h), 9) + pad(cell(scs[0][1], h), 19) + pad(cell(scs[1][1], h), 19) + cell(scs[2][1], h))
h = h + 1
for sc in scs:
print(verdict(sc[0], sc[1]))
print("400 nights of success say the archive was made. How old it is when it is needed is set by the schedule alone.")
def show_archive():
print("What one archive holds - a snapshot of the whole application host:")
for f in backup.archive():
print(" " + pad(f[0], 24) + pad(f[1], 40) + lpad(commas(f[2]) + " MB", 10))
k = backup.kept()
print(str(k[0]) + " archives are kept, each with " + str(k[1]) + " copies off site: the key is in " + str(k[0] * k[1]) + " places, each next to the data it protects. Anyone who holds one archive can open it.")
print("With /etc/orders/backup.key left out of the snapshot, a restore asks the key service for the key: no copy in any archive, and one more step in the drill.")
def own():
t = plain(input("first backup hour (0 to 23)> "))
if t == "":
print("Cancelled.")
return
if len(t) > 2 or not digits(t) or int(t) > 23:
print("Type a whole number from 0 to 23.")
return
n = plain(input("every how many hours (1 to 24)> "))
if n == "":
print("Cancelled.")
return
if len(n) > 2 or not digits(n) or int(n) < 1 or int(n) > 24:
print("Type a whole number from 1 to 24.")
return
sc = backup.every(int(t), int(n))
print("Backups at " + clocks(sc) + ".")
print(verdict("This schedule", sc))
print("== Backup planner ==")
print("Backups that succeed every night, and what a failure still loses.")
running = True
while running:
print("")
print("1) the system 2) a failure at each hour 3) what an archive holds 4) a schedule of your own 5) quit")
choice = plain(input("choice> "))
if choice == "1":
show_system()
elif choice == "2":
show_failures()
elif choice == "3":
show_archive()
elif choice == "4":
own()
elif choice == "5":
running = False
else:
print("Pick a number from 1 to 5.")
print("Bye.")
backup.eml
eml# P167 backup planner - an order system's changes hour by hour, a recovery
# point objective of one hour, and three backup schedules. As in the corpus
# case the-backup-ran-nightly-and-the-change-was-hourly, a backup that has
# succeeded every night says the artifact was made; how old it is when it is
# needed is set by the schedule alone. And as in
# the-backup-was-encrypted-and-the-key-was-in-the-backup, an archive that is a
# snapshot of the whole host carries the key that encrypts it.
def rpo():
# hours of work the business can afford to lose
return 1
def changes(h):
# orders and updates written in hour h (0 to 23)
if h < 7:
return 300
if h < 9:
return 2000
if h < 18:
return 4200
if h < 22:
return 1500
return 600
def schedules():
# [name, the hours a backup runs]
return [["nightly at 02:00", [2]],
["every 6 hours", [0, 6, 12, 18]],
["nightly + hourly", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]]]
def every(start, n):
# backups every n hours from start, the same hours each day
[] => out
0 => k
while k < 24:
(start + k) % 24 => h
False => dup
for x in out:
if x == h:
True => dup
if not dup:
out + [h] => out
k + n => k
return out
def age(hours, h):
# hours since the last backup before a failure at h:00 (1 to 24)
1 => a
while a <= 24:
(h - a + 24) % 24 => b
for x in hours:
if x == b:
return a
a + 1 => a
return 24
def lost(hours, h):
# changes written since that backup
age(hours, h) => a
0 => t
1 => k
while k <= a:
t + changes((h - k + 24) % 24) => t
k + 1 => k
return t
def per_day():
0 => t
0 => h
while h < 24:
t + changes(h) => t
h + 1 => h
return t
def archive():
# [path, what it is, size in MB] in a snapshot of the application host
return [["/srv/orders/db", "the order database", 48000],
["/etc/orders/app.conf", "the application's settings", 1],
["/etc/orders/backup.key", "the key the archive is encrypted with", 1]]
def kept():
# archives kept, and the copies of each held off site
return [90, 3]
Python projection (backup.py)
def rpo():
return 1
def changes(h):
if h < 7:
return 300
if h < 9:
return 2000
if h < 18:
return 4200
if h < 22:
return 1500
return 600
def schedules():
return [["nightly at 02:00", [2]], ["every 6 hours", [0, 6, 12, 18]], ["nightly + hourly", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]]]
def every(start, n):
out = []
k = 0
while k < 24:
h = (start + k) % 24
dup = False
for x in out:
if x == h:
dup = True
if not dup:
out = out + [h]
k = k + n
return out
def age(hours, h):
a = 1
while a <= 24:
b = (h - a + 24) % 24
for x in hours:
if x == b:
return a
a = a + 1
return 24
def lost(hours, h):
a = age(hours, h)
t = 0
k = 1
while k <= a:
t = t + changes((h - k + 24) % 24)
k = k + 1
return t
def per_day():
t = 0
h = 0
while h < 24:
t = t + changes(h)
h = h + 1
return t
def archive():
return [["/srv/orders/db", "the order database", 48000], ["/etc/orders/app.conf", "the application's settings", 1], ["/etc/orders/backup.key", "the key the archive is encrypted with", 1]]
def kept():
return [90, 3]