carlos / platform Public

plans: optional rate ceilings, never below the platform default; box hook (#358)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev ac8d9ba3a2f0b4d5fba14430c3aa226f7c6572d1 parent 24be8da
4 files changed, +184 −1
  • internal/host/plans_pass.go +12 −0
  • internal/host/plans_pass_ratelimit_test.go +29 −0
  • internal/plans/catalog.go +42 −1
  • internal/plans/catalog_ratelimit_test.go +101 −0
diff --git a/internal/host/plans_pass.go b/internal/host/plans_pass.go
index e45961e89..4130da030 100644
--- a/internal/host/plans_pass.go
+++ b/internal/host/plans_pass.go
@@ -19,6 +19,7 @@ import (
"time"
"github.com/carlosframework/platform/internal/plans"
+ "github.com/carlosframework/platform/internal/ratelimit"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
)
@@ -135,3 +136,14 @@ func planIdleFor(rt registry.Route) time.Duration {
}
return time.Duration(l.IdleSeconds) * time.Second
}
+
+// planRateCeilingFor is the edge's rate-limit ceiling hook: the most the
+// route's account's plan lets an instance owner set, or the zero Ceiling (no
+// ceiling) when plans are off or the account has no record.
+func planRateCeilingFor(rt registry.Route) ratelimit.Ceiling {
+ l, ok := planLimitsFor(release.RouteAccount(rt))
+ if !ok {
+ return ratelimit.Ceiling{}
+ }
+ return l.RateCeiling()
+}
diff --git a/internal/host/plans_pass_ratelimit_test.go b/internal/host/plans_pass_ratelimit_test.go
new file mode 100644
index 000000000..dc87508a3
--- /dev/null
+++ b/internal/host/plans_pass_ratelimit_test.go
@@ -0,0 +1,29 @@
+package host
+
+// Serial: it writes the package-level plans cache.
+
+import (
+ "testing"
+
+ "github.com/carlosframework/platform/internal/plans"
+ "github.com/carlosframework/platform/internal/ratelimit"
+ "github.com/carlosframework/platform/internal/registry"
+)
+
+func TestPlanRateCeilingForOffIsZero(t *testing.T) {
+ t.Cleanup(func() { planLimits.Store(nil) })
+ rt := registry.Route{Host: "a.example", Kind: "instance", Account: "bab", App: "web"}
+ planLimits.Store(nil)
+ if c := planRateCeilingFor(rt); c != (ratelimit.Ceiling{}) {
+ t.Fatalf("plans off: %+v, want the zero Ceiling", c)
+ }
+ planLimits.Store(&planLimitsSnapshot{byAccount: map[string]plans.Limits{"bab": {RatePerSecond: 400}}})
+ if c := planRateCeilingFor(rt); c != (ratelimit.Ceiling{Rate: 400}) {
+ t.Fatalf("bab: %+v, want Rate 400 only", c)
+ }
+ other := rt
+ other.Account = "bdk"
+ if c := planRateCeilingFor(other); c != (ratelimit.Ceiling{}) {
+ t.Fatalf("account with no record: %+v, want the zero Ceiling", c)
+ }
+}
diff --git a/internal/plans/catalog.go b/internal/plans/catalog.go
index a9a9592cb..e6f2c96f3 100644
--- a/internal/plans/catalog.go
+++ b/internal/plans/catalog.go
@@ -17,6 +17,8 @@ import (
"math"
"os"
"slices"
+
+ "github.com/carlosframework/platform/internal/ratelimit"
)
// Limits is one set of caps. In a plan it is the plan's caps; in a pack it
@@ -38,6 +40,20 @@ type Limits struct {
Fleets bool `json:"fleets,omitempty"`
MinScheduleSeconds int `json:"min_schedule_seconds,omitempty"`
HistoryDays int `json:"history_days,omitempty"`
+
+ // What an instance owner may set for the edge rate limit (spec
+ // 2026-10-24-edge-rate-limit-design.md §5.3). Zero is no ceiling. The
+ // per-second rate and the burst may not sit below the platform default:
+ // a plan that went lower would throttle an instance that never asked for
+ // a limit. The client share has no such floor.
+ RateClientShare int `json:"rate_client_share,omitempty"`
+ RatePerSecond int `json:"rate_per_second,omitempty"`
+ RateBurst int `json:"rate_burst,omitempty"`
+}
+
+// RateCeiling is these limits' rate fields as the edge's ceiling.
+func (l Limits) RateCeiling() ratelimit.Ceiling {
+ return ratelimit.Ceiling{ClientShare: l.RateClientShare, Rate: l.RatePerSecond, Burst: l.RateBurst}
}
// The memory tiers a plan may name. The numbers behind them belong to the
@@ -135,10 +151,16 @@ func (c *Catalog) validate() error {
if err := c.Ceilings.nonNegative(); err != nil {
return fmt.Errorf("ceilings: %w", err)
}
+ if err := c.Ceilings.rateAboveDefault(); err != nil {
+ return fmt.Errorf("ceilings: %w", err)
+ }
for name, p := range c.Plans {
if err := p.Limits.nonNegative(); err != nil {
return fmt.Errorf("plan %q: %w", name, err)
}
+ if err := p.Limits.rateAboveDefault(); err != nil {
+ return fmt.Errorf("plan %q: %w", name, err)
+ }
switch p.Limits.MemoryTier {
case "", MemorySmall, MemoryDefault:
default:
@@ -167,12 +189,25 @@ func (c *Catalog) validate() error {
func (l Limits) nonNegative() error {
if l.RunningThings < 0 || l.SidecarsPerApp < 0 || l.IdleSeconds < 0 || l.AwakeHoursMonth < 0 ||
l.DiskBytes < 0 || l.StorageBytes < 0 || l.EmailPerDay < 0 || l.BandwidthBytesMonth < 0 ||
- l.MinScheduleSeconds < 0 || l.HistoryDays < 0 {
+ l.MinScheduleSeconds < 0 || l.HistoryDays < 0 ||
+ l.RateClientShare < 0 || l.RatePerSecond < 0 || l.RateBurst < 0 {
return errors.New("limits may not be negative")
}
return nil
}
+// rateAboveDefault refuses a rate or burst set below the platform default.
+// Zero is unset and passes.
+func (l Limits) rateAboveDefault() error {
+ if l.RatePerSecond != 0 && l.RatePerSecond < ratelimit.Defaults.Rule.Rate {
+ return fmt.Errorf("rate_per_second %d is below the platform default %d", l.RatePerSecond, ratelimit.Defaults.Rule.Rate)
+ }
+ if l.RateBurst != 0 && l.RateBurst < ratelimit.Defaults.Rule.Burst {
+ return fmt.Errorf("rate_burst %d is below the platform default %d", l.RateBurst, ratelimit.Defaults.Rule.Burst)
+ }
+ return nil
+}
+
// additive is l with only the fields a pack may raise, and a ceiling clamps.
func (l Limits) additive() Limits {
return Limits{
@@ -229,6 +264,9 @@ func (c *Catalog) Effective(plan string, packs map[string]int, sponsored *Limits
l.EmailPerDay = clamp(l.EmailPerDay, ce.EmailPerDay)
l.BandwidthBytesMonth = clamp(l.BandwidthBytesMonth, ce.BandwidthBytesMonth)
l.IdleSeconds = clamp(l.IdleSeconds, ce.IdleSeconds)
+ l.RateClientShare = clamp(l.RateClientShare, ce.RateClientShare)
+ l.RatePerSecond = clamp(l.RatePerSecond, ce.RatePerSecond)
+ l.RateBurst = clamp(l.RateBurst, ce.RateBurst)
if sponsored != nil {
l = l.generous(*sponsored)
}
@@ -254,6 +292,9 @@ func (l Limits) generous(o Limits) Limits {
l.EmailPerDay = max(l.EmailPerDay, o.EmailPerDay)
l.BandwidthBytesMonth = max(l.BandwidthBytesMonth, o.BandwidthBytesMonth)
l.HistoryDays = max(l.HistoryDays, o.HistoryDays)
+ l.RateClientShare = max(l.RateClientShare, o.RateClientShare)
+ l.RatePerSecond = max(l.RatePerSecond, o.RatePerSecond)
+ l.RateBurst = max(l.RateBurst, o.RateBurst)
l.StorageSelfServe = l.StorageSelfServe || o.StorageSelfServe
l.CustomDomains = l.CustomDomains || o.CustomDomains
l.Fleets = l.Fleets || o.Fleets
diff --git a/internal/plans/catalog_ratelimit_test.go b/internal/plans/catalog_ratelimit_test.go
new file mode 100644
index 000000000..93540c91c
--- /dev/null
+++ b/internal/plans/catalog_ratelimit_test.go
@@ -0,0 +1,101 @@
+package plans
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/ratelimit"
+)
+
+func TestCatalogRefusesRateCeilingBelowDefault(t *testing.T) {
+ base := exampleBytes(t)
+ inPlan := `"running_things": 3,`
+ inCeil := `"idle_seconds": 900
+ }`
+ cases := []struct {
+ name, body, wantIn, wantField string
+ }{
+ {"plan rate", strings.Replace(base, inPlan, inPlan+` "rate_per_second": 100,`, 1), `plan "free"`, "rate_per_second"},
+ {"plan burst", strings.Replace(base, inPlan, inPlan+` "rate_burst": 10,`, 1), `plan "free"`, "rate_burst"},
+ {"ceilings rate", strings.Replace(base, inCeil, `"idle_seconds": 900, "rate_per_second": 100
+ }`, 1), "ceilings", "rate_per_second"},
+ {"ceilings burst", strings.Replace(base, inCeil, `"idle_seconds": 900, "rate_burst": 10
+ }`, 1), "ceilings", "rate_burst"},
+ {"negative share", strings.Replace(base, inPlan, inPlan+` "rate_client_share": -1,`, 1), `plan "free"`, "negative"},
+ {"negative rate", strings.Replace(base, inPlan, inPlan+` "rate_per_second": -300,`, 1), `plan "free"`, "negative"},
+ }
+ for _, tc := range cases {
+ if tc.body == base {
+ t.Fatalf("%s: replacement did not apply", tc.name)
+ }
+ _, err := ParseCatalog([]byte(tc.body))
+ if err == nil {
+ t.Errorf("%s: ParseCatalog accepted it", tc.name)
+ continue
+ }
+ if !strings.Contains(err.Error(), tc.wantIn) || !strings.Contains(err.Error(), tc.wantField) {
+ t.Errorf("%s: error %q should name %s and %s", tc.name, err, tc.wantIn, tc.wantField)
+ }
+ }
+
+ // The default itself, a share below the default and a value above all pass.
+ ok := strings.Replace(base, inPlan, inPlan+` "rate_client_share": 4, "rate_per_second": 300, "rate_burst": 5000,`, 1)
+ c, err := ParseCatalog([]byte(ok))
+ if err != nil {
+ t.Fatalf("rate_client_share 4, rate_per_second 300, rate_burst 5000: %v", err)
+ }
+ want := ratelimit.Ceiling{ClientShare: 4, Rate: 300, Burst: 5000}
+ if got := c.Plans["free"].Limits.RateCeiling(); got != want {
+ t.Fatalf("RateCeiling = %+v, want %+v", got, want)
+ }
+}
+
+func TestPackCannotAddRateFields(t *testing.T) {
+ base := exampleBytes(t)
+ for _, f := range []string{`"rate_per_second": 500`, `"rate_burst": 2000`, `"rate_client_share": 20`} {
+ body := strings.Replace(base, `{"running_things": 5}`, `{"running_things": 5, `+f+`}`, 1)
+ if body == base {
+ t.Fatalf("%s: replacement did not apply", f)
+ }
+ if _, err := ParseCatalog([]byte(body)); err == nil {
+ t.Errorf("a pack adding %s was accepted", f)
+ }
+ }
+}
+
+func TestEffectiveClampsRateFields(t *testing.T) {
+ c := exampleCatalog(t)
+ p := c.Plans["personal"]
+ p.Limits.RateClientShare = 40
+ p.Limits.RatePerSecond = 1000
+ p.Limits.RateBurst = 4000
+ c.Plans["personal"] = p
+ c.Ceilings.RateClientShare = 30
+ c.Ceilings.RatePerSecond = 500
+ c.Ceilings.RateBurst = 2000
+
+ got, err := c.Effective("personal", nil, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := (ratelimit.Ceiling{ClientShare: 30, Rate: 500, Burst: 2000}); got.RateCeiling() != want {
+ t.Fatalf("clamped = %+v, want %+v", got.RateCeiling(), want)
+ }
+
+ got, err = c.Effective("personal", nil, &Limits{RateClientShare: 60, RatePerSecond: 5000, RateBurst: 9000})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := (ratelimit.Ceiling{ClientShare: 60, Rate: 5000, Burst: 9000}); got.RateCeiling() != want {
+ t.Fatalf("sponsored = %+v, want %+v (sponsored is not clamped)", got.RateCeiling(), want)
+ }
+
+ // A sponsored zero is unset: the clamped plan value stands.
+ got, err = c.Effective("personal", nil, &Limits{RatePerSecond: 5000})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := (ratelimit.Ceiling{ClientShare: 30, Rate: 5000, Burst: 2000}); got.RateCeiling() != want {
+ t.Fatalf("partly sponsored = %+v, want %+v", got.RateCeiling(), want)
+ }
+}