<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P172-error-budget/ | updated: 2026-10-19 -->

# P172 Error budget

A 99.9% monthly target: two months that spend the same budget reach 42 users and 1,000, an hour with no requests reads as fully met, and one endpoint spends 82% of a budget the service never exceeds.

EML-P project `projects/error-budget` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/error-budget` runs it and `eml project verify projects/error-budget` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: the-error-budget-was-monthly-and-the-outage-was-not (https://efficientnewlanguage.org/cases/614-the-error-budget-was-monthly-and-the-outage-was-not/), the-slo-was-met-over-a-window-with-no-requests (https://efficientnewlanguage.org/cases/848-the-slo-was-met-over-a-window-with-no-requests/), the-error-budget-was-spent-evenly-and-one-endpoint-ate-it (https://efficientnewlanguage.org/cases/784-the-error-budget-was-spent-evenly-and-one-endpoint-ate-it/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
5

5
0
5
61
5
x
5
24

5
24
0
5
24
0.0
5
24
43.3
5
24
1.45
5
24
.5
5
24
1,5
5
24
0.1
5
2
43.2
6
```

Screen:

```text
== Error budget ==
A monthly budget of outage minutes, and what it does not count.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 0
Pick a number from 1 to 6.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> x
Pick a number from 1 to 6.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 
Cancelled.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 0
Type a whole number from 1 to 60.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 61
Type a whole number from 1 to 60.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> x
Type a whole number from 1 to 60.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 24
minutes each, to a tenth (0.1 to 43.2)> 
Cancelled.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 24
minutes each, to a tenth (0.1 to 43.2)> 0
Type minutes such as 1.4, from 0.1 to 43.2.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 24
minutes each, to a tenth (0.1 to 43.2)> 0.0
Type minutes such as 1.4, from 0.1 to 43.2.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 24
minutes each, to a tenth (0.1 to 43.2)> 43.3
Type minutes such as 1.4, from 0.1 to 43.2.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 24
minutes each, to a tenth (0.1 to 43.2)> 1.45
Type minutes such as 1.4, from 0.1 to 43.2.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 24
minutes each, to a tenth (0.1 to 43.2)> .5
Type minutes such as 1.4, from 0.1 to 43.2.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 24
minutes each, to a tenth (0.1 to 43.2)> 1,5
Type minutes such as 1.4, from 0.1 to 43.2.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 24
minutes each, to a tenth (0.1 to 43.2)> 0.1
24 outages of 0.1 minutes spend 2.4 of the 43.2 minutes, 40.8 left.
They interrupt 1,000 of the 1,000 users, 0 of them more than once.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 2
minutes each, to a tenth (0.1 to 43.2)> 43.2
2 outages of 43.2 minutes spend 86.4 minutes, 43.2 over the budget.
They interrupt 84 of the 1,000 users, 0 of them more than once.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 6
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
2
3
4
5
30
1.4
5
1
42
5
60
1
6
```

Screen:

```text
== Error budget ==
A monthly budget of outage minutes, and what it does not count.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 1
A service with a 99.9% monthly target: 0.1% of a 30-day month is a budget of 43.2 minutes of outage.
Its 1,000 users are each active one hour a day, the same hour every day, spread evenly over the 24 hours.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 2
Two months, each inside the budget:
  month A: 1 outage, on day 10 at 14:10, 42.0 minutes
  month B: 30 outages of 1.4 minutes, one a day, at 00:10, 07:10, 14:10, 21:10, 04:10 and so on
  month  outages  minutes  budget left  users hit
  A            1     42.0          1.2         42
  B           30     42.0          1.2      1,000
The budget reads the same for both. Month A interrupted the 42 users of one hour; month B interrupted all 1,000, and 251 of them twice.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 3
A day's latency objective, hour by hour: a request over the threshold is a miss, and an hour meets the objective at 99.50% or better.
  hour    requests  misses       met
  00:00        800       2    99.75%
  01:00      1,550       3    99.81%
  02:00      1,250       3    99.76%
  03:00          0       0   100.00%
  04:00      1,700       4    99.76%
  05:00      1,400       3    99.79%
  06:00      1,100       2    99.82%
  07:00        800       2    99.75%
  08:00      1,550       3    99.81%
  09:00      1,250       3    99.76%
  10:00        950       2    99.79%
  11:00      1,700       4    99.76%
  12:00      1,400       3    99.79%
  13:00      1,100       2    99.82%
  14:00        800       2    99.75%
  15:00      1,550       3    99.81%
  16:00      1,250       3    99.76%
  17:00        950       2    99.79%
  18:00      1,700       4    99.76%
  19:00      1,400       3    99.79%
  20:00      1,100       2    99.82%
  21:00        800       2    99.75%
  22:00      1,550       3    99.81%
  23:00      1,250       3    99.76%
24 of 24 hours meet it. 03:00 reads 100.00% from 0 requests: the load balancer sent nothing to the service that hour, and nobody could reach it.
With an hour of no requests counted as no answer, and an availability check that fires on an empty hour, 03:00 pages instead.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 4
One budget for the quarter over every endpoint: at most 1.00% of requests may fail.
  endpoint      requests   errors  error rate  of the errors
  /search     24,000,000   31,000       0.13%          6.52%
  /orders     18,000,000   26,000       0.14%          5.46%
  /cart       11,000,000   14,000       0.13%          2.94%
  /login       6,100,000   13,800       0.23%          2.90%
  /export        900,000  391,000      43.44%         82.18%
  /health      1,000,000        0       0.00%          0.00%
All endpoints: 61,000,000 requests, 475,800 errors, 0.78%: inside the budget, and no freeze.
/export fails 43.44% of its requests on 1.48% of the traffic, and spends 82.18% of the budget used.
With a budget per endpoint, in proportion to its traffic, /export is the one endpoint over its own and the one frozen.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 30
minutes each, to a tenth (0.1 to 43.2)> 1.4
30 outages of 1.4 minutes spend 42.0 of the 43.2 minutes, 1.2 left.
They interrupt 1,000 of the 1,000 users, 251 of them more than once.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 1
minutes each, to a tenth (0.1 to 43.2)> 42
1 outage of 42.0 minutes spends 42.0 of the 43.2 minutes, 1.2 left.
It interrupts 42 of the 1,000 users, 0 of them more than once.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 5
outages (1 to 60)> 60
minutes each, to a tenth (0.1 to 43.2)> 1
60 outages of 1.0 minutes spend 60.0 minutes, 16.8 over the budget.
They interrupt 1,000 of the 1,000 users, 1,000 of them more than once.

1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit
choice> 6
Bye.
```

## Modules

### main.eml

```eml
# P172 error budget: a 99.9% monthly target - two months that spend the
# same budget, a day's latency objective with an empty hour, one quarter's
# budget over six endpoints, and outages of your own.
import budget

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 tenths(t):
    return commas(int((t - t % 10) / 10)) + "." + str(t % 10)

def per(a, b):
    # a of b as a percent to two places, half-up
    a * 20000 + b => y
    int((y - y % (2 * b)) / (2 * b)) => h
    h % 100 => r
    if r < 10:
        return str(int((h - r) / 100)) + ".0" + str(r) + "%"
    return str(int((h - r) / 100)) + "." + str(r) + "%"

def clock(h, m):
    "" => s
    if h < 10:
        "0" => s
    s + str(h) + ":" => s
    if m < 10:
        s + "0" => s
    return s + str(m)

def twice(outs):
    # users active in the hours of two or more outages
    [0] * 24 => n
    for o in outs:
        n[o[1]] + 1 => n[o[1]]
    0 => c
    0 => u
    while u < budget.users():
        if n[u % 24] >= 2:
            c + 1 => c
        u + 1 => u
    return c

def show_service():
    ("A service with a 99.9% monthly target: 0.1% of a 30-day month is a budget of " + tenths(budget.budget_tenths()) + " minutes of outage.") ^0
    ("Its " + commas(budget.users()) + " users are each active one hour a day, the same hour every day, spread evenly over the 24 hours.") ^0

def month_row(name, outs):
    budget.spent(outs) => s
    ("  " + pad(name, 7) + lpad(str(len(outs)), 7) + lpad(tenths(s), 9) + lpad(tenths(budget.budget_tenths() - s), 13) + lpad(commas(budget.hit(outs)), 11)) ^0

def show_months():
    budget.month_a() => a
    budget.spread(30, 14) => b
    "Two months, each inside the budget:" ^0
    ("  month A: 1 outage, on day " + str(a[0][0]) + " at " + clock(a[0][1], a[0][2]) + ", " + tenths(a[0][3]) + " minutes") ^0
    "  month B: 30 outages of 1.4 minutes, one a day, at 00:10, 07:10, 14:10, 21:10, 04:10 and so on" ^0
    ("  " + pad("month", 7) + lpad("outages", 7) + lpad("minutes", 9) + lpad("budget left", 13) + lpad("users hit", 11)) ^0
    month_row("A", a)
    month_row("B", b)
    ("The budget reads the same for both. Month A interrupted the " + str(budget.hit(a)) + " users of one hour; month B interrupted all " + commas(budget.hit(b)) + ", and " + str(twice(b)) + " of them twice.") ^0

def show_hours():
    budget.hourly() => hs
    "A day's latency objective, hour by hour: a request over the threshold is a miss, and an hour meets the objective at 99.50% or better." ^0
    ("  " + pad("hour", 7) + lpad("requests", 9) + lpad("misses", 8) + lpad("met", 10)) ^0
    0 => met
    for h in hs:
        "100.00%" => shown
        if h[1] > 0:
            per(h[1] - h[2], h[1]) => shown
        if h[1] == 0 or (h[1] - h[2]) * 10000 >= 9950 * h[1]:
            met + 1 => met
        ("  " + pad(clock(h[0], 0), 7) + lpad(commas(h[1]), 9) + lpad(str(h[2]), 8) + lpad(shown, 10)) ^0
    (str(met) + " of 24 hours meet it. 03:00 reads 100.00% from 0 requests: the load balancer sent nothing to the service that hour, and nobody could reach it.") ^0
    "With an hour of no requests counted as no answer, and an availability check that fires on an empty hour, 03:00 pages instead." ^0

def show_endpoints():
    budget.endpoints() => es
    0 => req
    0 => err
    for e in es:
        req + e[1] => req
        err + e[2] => err
    ("One budget for the quarter over every endpoint: at most " + per(budget.budget_rate(), 10000) + " of requests may fail.") ^0
    ("  " + pad("endpoint", 10) + lpad("requests", 12) + lpad("errors", 9) + lpad("error rate", 12) + lpad("of the errors", 15)) ^0
    [] => over
    for e in es:
        ("  " + pad(e[0], 10) + lpad(commas(e[1]), 12) + lpad(commas(e[2]), 9) + lpad(per(e[2], e[1]), 12) + lpad(per(e[2], err), 15)) ^0
        if e[2] * 10000 > budget.budget_rate() * e[1]:
            over + [e[0]] => over
    ("All endpoints: " + commas(req) + " requests, " + commas(err) + " errors, " + per(err, req) + ": inside the budget, and no freeze.") ^0
    es[4] => x
    (x[0] + " fails " + per(x[2], x[1]) + " of its requests on " + per(x[1], req) + " of the traffic, and spends " + per(x[2], err) + " of the budget used.") ^0
    if len(over) == 1:
        ("With a budget per endpoint, in proportion to its traffic, " + over[0] + " is the one endpoint over its own and the one frozen.") ^0

def own():
    plain(input("outages (1 to 60)> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    if len(t) > 2 or not digits(t) or int(t) < 1 or int(t) > 60:
        "Type a whole number from 1 to 60." ^0
        return
    int(t) => n
    plain(input("minutes each, to a tenth (0.1 to 43.2)> ")) => m
    if m == "":
        "Cancelled." ^0
        return
    0 - 1 => tt
    if digits(m) and len(m) <= 2:
        int(m) * 10 => tt
    elif len(m) >= 3 and len(m) <= 4 and m[len(m) - 2] == "." and digits(m[0:len(m) - 2]) and digits(m[len(m) - 1:]):
        int(m[0:len(m) - 2]) * 10 + int(m[len(m) - 1:]) => tt
    if tt < 1 or tt > 432:
        "Type minutes such as 1.4, from 0.1 to 43.2." ^0
        return
    budget.spread(n, tt) => outs
    budget.spent(outs) => s
    str(n) + " outages of " + tenths(tt) + " minutes spend " => lead
    if n == 1:
        "1 outage of " + tenths(tt) + " minutes spends " => lead
    if s <= budget.budget_tenths():
        (lead + tenths(s) + " of the " + tenths(budget.budget_tenths()) + " minutes, " + tenths(budget.budget_tenths() - s) + " left.") ^0
    else:
        (lead + tenths(s) + " minutes, " + tenths(s - budget.budget_tenths()) + " over the budget.") ^0
    "They interrupt " => verb
    if n == 1:
        "It interrupts " => verb
    (verb + commas(budget.hit(outs)) + " of the " + commas(budget.users()) + " users, " + commas(twice(outs)) + " of them more than once.") ^0

"== Error budget ==" ^0
"A monthly budget of outage minutes, and what it does not count." ^0
True => running
while running:
    "" ^0
    "1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_service()
    elif choice == "2":
        show_months()
    elif choice == "3":
        show_hours()
    elif choice == "4":
        show_endpoints()
    elif choice == "5":
        own()
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import budget

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 tenths(t):
    return commas(int((t - t % 10) / 10)) + "." + str(t % 10)

def per(a, b):
    y = a * 20000 + b
    h = int((y - y % (2 * b)) / (2 * b))
    r = h % 100
    if r < 10:
        return str(int((h - r) / 100)) + ".0" + str(r) + "%"
    return str(int((h - r) / 100)) + "." + str(r) + "%"

def clock(h, m):
    s = ""
    if h < 10:
        s = "0"
    s = s + str(h) + ":"
    if m < 10:
        s = s + "0"
    return s + str(m)

def twice(outs):
    n = [0] * 24
    for o in outs:
        n[o[1]] = n[o[1]] + 1
    c = 0
    u = 0
    while u < budget.users():
        if n[u % 24] >= 2:
            c = c + 1
        u = u + 1
    return c

def show_service():
    print("A service with a 99.9% monthly target: 0.1% of a 30-day month is a budget of " + tenths(budget.budget_tenths()) + " minutes of outage.")
    print("Its " + commas(budget.users()) + " users are each active one hour a day, the same hour every day, spread evenly over the 24 hours.")

def month_row(name, outs):
    s = budget.spent(outs)
    print("  " + pad(name, 7) + lpad(str(len(outs)), 7) + lpad(tenths(s), 9) + lpad(tenths(budget.budget_tenths() - s), 13) + lpad(commas(budget.hit(outs)), 11))

def show_months():
    a = budget.month_a()
    b = budget.spread(30, 14)
    print("Two months, each inside the budget:")
    print("  month A: 1 outage, on day " + str(a[0][0]) + " at " + clock(a[0][1], a[0][2]) + ", " + tenths(a[0][3]) + " minutes")
    print("  month B: 30 outages of 1.4 minutes, one a day, at 00:10, 07:10, 14:10, 21:10, 04:10 and so on")
    print("  " + pad("month", 7) + lpad("outages", 7) + lpad("minutes", 9) + lpad("budget left", 13) + lpad("users hit", 11))
    month_row("A", a)
    month_row("B", b)
    print("The budget reads the same for both. Month A interrupted the " + str(budget.hit(a)) + " users of one hour; month B interrupted all " + commas(budget.hit(b)) + ", and " + str(twice(b)) + " of them twice.")

def show_hours():
    hs = budget.hourly()
    print("A day's latency objective, hour by hour: a request over the threshold is a miss, and an hour meets the objective at 99.50% or better.")
    print("  " + pad("hour", 7) + lpad("requests", 9) + lpad("misses", 8) + lpad("met", 10))
    met = 0
    for h in hs:
        shown = "100.00%"
        if h[1] > 0:
            shown = per(h[1] - h[2], h[1])
        if h[1] == 0 or (h[1] - h[2]) * 10000 >= 9950 * h[1]:
            met = met + 1
        print("  " + pad(clock(h[0], 0), 7) + lpad(commas(h[1]), 9) + lpad(str(h[2]), 8) + lpad(shown, 10))
    print(str(met) + " of 24 hours meet it. 03:00 reads 100.00% from 0 requests: the load balancer sent nothing to the service that hour, and nobody could reach it.")
    print("With an hour of no requests counted as no answer, and an availability check that fires on an empty hour, 03:00 pages instead.")

def show_endpoints():
    es = budget.endpoints()
    req = 0
    err = 0
    for e in es:
        req = req + e[1]
        err = err + e[2]
    print("One budget for the quarter over every endpoint: at most " + per(budget.budget_rate(), 10000) + " of requests may fail.")
    print("  " + pad("endpoint", 10) + lpad("requests", 12) + lpad("errors", 9) + lpad("error rate", 12) + lpad("of the errors", 15))
    over = []
    for e in es:
        print("  " + pad(e[0], 10) + lpad(commas(e[1]), 12) + lpad(commas(e[2]), 9) + lpad(per(e[2], e[1]), 12) + lpad(per(e[2], err), 15))
        if e[2] * 10000 > budget.budget_rate() * e[1]:
            over = over + [e[0]]
    print("All endpoints: " + commas(req) + " requests, " + commas(err) + " errors, " + per(err, req) + ": inside the budget, and no freeze.")
    x = es[4]
    print(x[0] + " fails " + per(x[2], x[1]) + " of its requests on " + per(x[1], req) + " of the traffic, and spends " + per(x[2], err) + " of the budget used.")
    if len(over) == 1:
        print("With a budget per endpoint, in proportion to its traffic, " + over[0] + " is the one endpoint over its own and the one frozen.")

def own():
    t = plain(input("outages (1 to 60)> "))
    if t == "":
        print("Cancelled.")
        return
    if len(t) > 2 or not digits(t) or int(t) < 1 or int(t) > 60:
        print("Type a whole number from 1 to 60.")
        return
    n = int(t)
    m = plain(input("minutes each, to a tenth (0.1 to 43.2)> "))
    if m == "":
        print("Cancelled.")
        return
    tt = 0 - 1
    if digits(m) and len(m) <= 2:
        tt = int(m) * 10
    elif len(m) >= 3 and len(m) <= 4 and m[len(m) - 2] == "." and digits(m[0:len(m) - 2]) and digits(m[len(m) - 1:]):
        tt = int(m[0:len(m) - 2]) * 10 + int(m[len(m) - 1:])
    if tt < 1 or tt > 432:
        print("Type minutes such as 1.4, from 0.1 to 43.2.")
        return
    outs = budget.spread(n, tt)
    s = budget.spent(outs)
    lead = str(n) + " outages of " + tenths(tt) + " minutes spend "
    if n == 1:
        lead = "1 outage of " + tenths(tt) + " minutes spends "
    if s <= budget.budget_tenths():
        print(lead + tenths(s) + " of the " + tenths(budget.budget_tenths()) + " minutes, " + tenths(budget.budget_tenths() - s) + " left.")
    else:
        print(lead + tenths(s) + " minutes, " + tenths(s - budget.budget_tenths()) + " over the budget.")
    verb = "They interrupt "
    if n == 1:
        verb = "It interrupts "
    print(verb + commas(budget.hit(outs)) + " of the " + commas(budget.users()) + " users, " + commas(twice(outs)) + " of them more than once.")

print("== Error budget ==")
print("A monthly budget of outage minutes, and what it does not count.")
running = True
while running:
    print("")
    print("1) the service  2) two months  3) an empty hour  4) one budget, six endpoints  5) outages of your own  6) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_service()
    elif choice == "2":
        show_months()
    elif choice == "3":
        show_hours()
    elif choice == "4":
        show_endpoints()
    elif choice == "5":
        own()
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")
```

### budget.eml

```eml
# P172 error budget - a service with a 99.9% monthly target, 43.2 minutes
# of budget a month. As in the corpus case
# the-error-budget-was-monthly-and-the-outage-was-not, a budget sums the
# minutes and forgets when they fell and whom they reached. As in
# the-slo-was-met-over-a-window-with-no-requests, a window with no requests
# reads as fully met. And as in
# the-error-budget-was-spent-evenly-and-one-endpoint-ate-it, one budget
# over every endpoint can be almost all one endpoint's.

def budget_tenths():
    # 0.1% of a 30-day month, in tenths of a minute
    return 432

def users():
    # each of the 1,000 users is active one hour a day, the hour u % 24
    return 1000

def month_a():
    # [day, hour, minute it starts, tenths of a minute] - one outage
    return [[10, 14, 10, 420]]

def spread(n, tenths):
    # n outages of the same length, one a day, at the hour 7 * i % 24
    [] => out
    0 => i
    while i < n:
        out + [[i % 30 + 1, (7 * i) % 24, 10, tenths]] => out
        i + 1 => i
    return out

def spent(outs):
    0 => t
    for o in outs:
        t + o[3] => t
    return t

def hit(outs):
    # users active in the hour of at least one outage
    [False] * 24 => hours
    for o in outs:
        True => hours[o[1]]
    0 => n
    0 => u
    while u < users():
        if hours[u % 24]:
            n + 1 => n
        u + 1 => u
    return n

def hourly():
    # [hour, requests, requests over the latency threshold] for one day;
    # from 03:00 to 04:00 the load balancer sent nothing to the service
    [] => out
    0 => h
    while h < 24:
        800 + 150 * ((h * 5) % 7) => r
        if h == 3:
            0 => r
        out + [[h, r, int(r / 400)]] => out
        h + 1 => h
    return out

def endpoints():
    # [endpoint, requests a quarter, errors]
    return [["/search", 24000000, 31000], ["/orders", 18000000, 26000], ["/cart", 11000000, 14000],
            ["/login", 6100000, 13800], ["/export", 900000, 391000], ["/health", 1000000, 0]]

def budget_rate():
    # errors per 10,000 requests the quarter's budget allows
    return 100
```

Python projection of budget.eml:

```python
def budget_tenths():
    return 432

def users():
    return 1000

def month_a():
    return [[10, 14, 10, 420]]

def spread(n, tenths):
    out = []
    i = 0
    while i < n:
        out = out + [[i % 30 + 1, 7 * i % 24, 10, tenths]]
        i = i + 1
    return out

def spent(outs):
    t = 0
    for o in outs:
        t = t + o[3]
    return t

def hit(outs):
    hours = [False] * 24
    for o in outs:
        hours[o[1]] = True
    n = 0
    u = 0
    while u < users():
        if hours[u % 24]:
            n = n + 1
        u = u + 1
    return n

def hourly():
    out = []
    h = 0
    while h < 24:
        r = 800 + 150 * (h * 5 % 7)
        if h == 3:
            r = 0
        out = out + [[h, r, int(r / 400)]]
        h = h + 1
    return out

def endpoints():
    return [["/search", 24000000, 31000], ["/orders", 18000000, 26000], ["/cart", 11000000, 14000], ["/login", 6100000, 13800], ["/export", 900000, 391000], ["/health", 1000000, 0]]

def budget_rate():
    return 100
```

## README

# P172 - Error budget

A service with a 99.9% monthly target, a budget of 43.2 minutes of outage
a month, and what the budget does not count.

- `main.eml` - the menu, the service, two months, a day with an empty
  hour, one quarter's budget over six endpoints, and outages of your own
- `budget.eml` - the budget, the users and their hours, the outages of
  each month, the day's hourly requests, and the endpoints

How each part works:

- The 1,000 users are each active one hour a day, the same hour every day,
  spread evenly over the 24 hours. An outage interrupts the users of its
  hour.
- As in the corpus case `the-error-budget-was-monthly-and-the-outage-was-not`,
  month A has one outage of 42.0 minutes and month B thirty of 1.4, a day
  apart at different hours: the budget reads 1.2 minutes left in both.
  Month A interrupted the 42 users of one hour; month B all 1,000, and 251
  of them twice.
- As in `the-slo-was-met-over-a-window-with-no-requests`, a latency
  objective met hour by hour reads 100.00% for 03:00, when the load
  balancer sent the service nothing: 0 misses of 0 requests. Counted as no
  answer, with a check that fires on an empty hour, it pages.
- As in `the-error-budget-was-spent-evenly-and-one-endpoint-ate-it`, one
  budget of 1.00% over all six endpoints reads 0.78%, inside it. /export
  fails 43.44% of its requests on 1.48% of the traffic and spends 82.18%
  of the budget used; with a budget per endpoint it is the one frozen.

What is checked: menu choices 1 to 6; 1 to 60 outages; minutes each, as a
whole number or to one decimal place, from 0.1 to 43.2. An empty answer
cancels.

Sessions: `sessions/basic.in` gives:
- the service, the two months, the day's hours and the endpoints;
- your own: 30 outages of 1.4 minutes, 1 of 42, and 60 of 1 (over the
  budget).

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- outages: a cancel, 0, 61 and x; minutes: a cancel, 0, 0.0, 43.3, 1.45,
  .5 and 1,5;
- 24 outages of 0.1 minutes, and 2 of 43.2.

Built on the verified corpus cases `the-error-budget-was-monthly-and-the-outage-was-not`
(two months that spend the same budget), `the-slo-was-met-over-a-window-with-no-requests`
(an hour with no requests read as fully met) and
`the-error-budget-was-spent-evenly-and-one-endpoint-ate-it` (one quarterly
budget over 214 endpoints).
