carlos ratelimit: show prints the plan ceiling; help lists the verb (#358)
show adds one "plan ceiling" line after the backstop line when the plan sets any of share, rate or burst. The top-level usage gains the ratelimit line after gate. The docs test now expects the cli.md usage fence to declare its language.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+62
−1
main.go+1 −0ratelimit_cli.go+31 −0ratelimit_cli_test.go+30 −1
diff --git a/main.go b/main.go| index 1c70aa96e..5c7d8f197 100644 |
| --- a/main.go |
| +++ b/main.go |
| @@ -275,6 +275,7 @@ Your apps — ship and run software on a CARLOS deployment |
| features an app's own feature flags, stored and served by the |
| platform, never interpreted: set | list |
| gate a shared password in front of an app's hostnames |
| + ratelimit per-client limits on an app's instances |
| logs an app's merged log timeline: its own output plus platform |
| events (wakes, restarts, failures); -f follows live |
| errors error groups for an app: log lines at the set level plus |
diff --git a/ratelimit_cli.go b/ratelimit_cli.go| index 3f14c6422..23fa73daa 100644 |
| --- a/ratelimit_cli.go |
| +++ b/ratelimit_cli.go |
| @@ -55,6 +55,10 @@ The box picks a change up within seconds of the next converge tick. |
| ratelimitShowDefault = "(default)" // cli.ratelimit.show.default |
| ratelimitShowCapped = "(the plan caps this; you set {set})" // cli.ratelimit.show.capped |
| ratelimitShowCounts = "last 24 hours {shed} turned away while busy, {ceiling} over the backstop" // cli.ratelimit.show.counts |
| + ratelimitShowCeiling = "plan ceiling {parts}" // cli.ratelimit.show.ceiling |
| + ratelimitCeilShare = "share {n}" // cli.ratelimit.show.ceiling.share |
| + ratelimitCeilRate = "{n} a second" // cli.ratelimit.show.ceiling.rate |
| + ratelimitCeilBurst = "bursts of {n}" // cli.ratelimit.show.ceiling.burst |
| ratelimitSetOK = "rate limit set on" // cli.ratelimit.set.ok |
| ratelimitSetBad = "rate limit not set on" // cli.ratelimit.set.bad |
| ratelimitClearOK = "rate limit back to defaults on" // cli.ratelimit.clear.ok |
| @@ -224,6 +228,10 @@ func printRateLimit(w io.Writer, rl apiRateLimit) { |
| } |
| lines = append(lines, backstop) |
| + if c := ratelimitCeilingLine(rl.Ceiling); c != "" { |
| + lines = append(lines, c) |
| + } |
| + |
| for _, p := range eff.Paths { |
| lines = append(lines, ratelimitCopy(ratelimitShowPath, "prefix", p.Prefix, |
| "share", n(p.ClientShare), "rate", n(p.Rate), "burst", n(p.Burst))) |
| @@ -237,6 +245,29 @@ func printRateLimit(w io.Writer, rl apiRateLimit) { |
| } |
| } |
| +// ratelimitCeilingLine is the plan ceiling's one line: the numbers the plan |
| +// sets, in the order share, rate, burst, and nothing when the plan sets none |
| +// (a null ceiling, or one whose fields are all zero). |
| +func ratelimitCeilingLine(c *ratelimit.Rule) string { |
| + if c == nil { |
| + return "" |
| + } |
| + var parts []string |
| + if c.ClientShare != 0 { |
| + parts = append(parts, ratelimitCopy(ratelimitCeilShare, "n", strconv.Itoa(c.ClientShare))) |
| + } |
| + if c.Rate != 0 { |
| + parts = append(parts, ratelimitCopy(ratelimitCeilRate, "n", strconv.Itoa(c.Rate))) |
| + } |
| + if c.Burst != 0 { |
| + parts = append(parts, ratelimitCopy(ratelimitCeilBurst, "n", strconv.Itoa(c.Burst))) |
| + } |
| + if len(parts) == 0 { |
| + return "" |
| + } |
| + return ratelimitCopy(ratelimitShowCeiling, "parts", strings.Join(parts, ", ")) |
| +} |
| + |
| // pathFlags collects the repeatable -path and -remove-path values. |
| type pathFlags []string |
diff --git a/ratelimit_cli_test.go b/ratelimit_cli_test.go| index 3c41a1804..89532c1a8 100644 |
| --- a/ratelimit_cli_test.go |
| +++ b/ratelimit_cli_test.go |
| @@ -251,6 +251,7 @@ func TestRatelimitShowMarksDefaultsAndCeiling(t *testing.T) { |
| " busy at 32 requests in flight\n" + |
| " per address, busy 20 requests in flight (the plan caps this; you set 40)\n" + |
| " backstop 1000 a second, bursts of 1000 (the plan caps this; you set 5000 a second, bursts of 1000)\n" + |
| + " plan ceiling share 20, 1000 a second, bursts of 2000\n" + |
| " /api/ 8 in flight per address when busy, backstop 1000 a second, bursts of 1000\n" + |
| " last 24 hours 12 turned away while busy, 3 over the backstop\n" |
| if out.String() != want { |
| @@ -261,6 +262,34 @@ func TestRatelimitShowMarksDefaultsAndCeiling(t *testing.T) { |
| } |
| } |
| +// TestRatelimitShowPrintsPlanCeiling: the ceiling line names only the |
| +// numbers the plan actually sets, in the order share, rate, burst, and a |
| +// null or all-zero ceiling prints no line at all. |
| +func TestRatelimitShowPrintsPlanCeiling(t *testing.T) { |
| + fc := newRLConsole(t) |
| + show := func(ceiling string) string { |
| + fc.current = strings.Replace(rlShowPlain, `"ceiling":null`, `"ceiling":`+ceiling, 1) |
| + var out bytes.Buffer |
| + if failed := ratelimitShow(newTestSession(fc.ts.URL), apiAcct{Seg: "bah"}, "app", []string{"b.example.com"}, &out); failed != 0 { |
| + t.Fatalf("failed = %d", failed) |
| + } |
| + return out.String() |
| + } |
| + const line = " plan ceiling share 64, 5000 a second\n" |
| + got := show(`{"client_share":64,"rate":5000}`) |
| + if !strings.Contains(got, "backstop 300 a second, bursts of 1000 (default)\n"+line+" last 24 hours") { |
| + t.Errorf("non-clamping ceiling:\n%s\nwant the line %q after the backstop line", got, line) |
| + } |
| + if got := show(`{"burst":2000}`); !strings.Contains(got, " plan ceiling bursts of 2000\n") { |
| + t.Errorf("burst only:\n%s", got) |
| + } |
| + for _, c := range []string{`null`, `{}`, `{"client_share":0,"rate":0,"burst":0}`} { |
| + if got := show(c); strings.Contains(got, "plan ceiling") { |
| + t.Errorf("ceiling %s printed a line:\n%s", c, got) |
| + } |
| + } |
| +} |
| + |
| func TestRatelimitOldConsole(t *testing.T) { |
| fc := newRLConsole(t) |
| fc.patch = func(string) (int, string) { |
| @@ -290,7 +319,7 @@ func TestRatelimitDocsCarryTheUsage(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - if !strings.Contains(string(raw), "### carlos ratelimit\n\n```\n"+ratelimitUsage+"```\n") { |
| + if !strings.Contains(string(raw), "### carlos ratelimit\n\n```text\n"+ratelimitUsage+"```\n") { |
| t.Error("docs/site/cli.md does not carry the ratelimit usage text verbatim") |
| } |
| } |