carlos / platform Public

plans: the deployment marker, and checked pack arithmetic

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev d055ab9cb07d334ce375ea19bbb1302b44334d42 parent f3401d1
4 files changed, +153 −5
  • internal/plans/catalog.go +21 −5
  • internal/plans/catalog_test.go +14 −0
  • internal/plans/marker.go +60 −0
  • internal/plans/marker_test.go +58 −0
diff --git a/internal/plans/catalog.go b/internal/plans/catalog.go
index cd9c2734b..a9a9592cb 100644
--- a/internal/plans/catalog.go
+++ b/internal/plans/catalog.go
@@ -14,6 +14,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "math"
"os"
"slices"
)
@@ -211,11 +212,15 @@ func (c *Catalog) Effective(plan string, packs map[string]int, sponsored *Limits
if qty < 1 || qty > pk.MaxQuantity {
return Limits{}, fmt.Errorf("%w: %q has %d, allowed 1 to %d", ErrPackQuantity, name, qty, pk.MaxQuantity)
}
- l.RunningThings += qty * pk.Adds.RunningThings
- l.DiskBytes += int64(qty) * pk.Adds.DiskBytes
- l.StorageBytes += int64(qty) * pk.Adds.StorageBytes
- l.EmailPerDay += qty * pk.Adds.EmailPerDay
- l.BandwidthBytesMonth += int64(qty) * pk.Adds.BandwidthBytesMonth
+ fits := true
+ l.RunningThings = addPack(l.RunningThings, qty, pk.Adds.RunningThings, &fits)
+ l.DiskBytes = addPack(l.DiskBytes, qty, pk.Adds.DiskBytes, &fits)
+ l.StorageBytes = addPack(l.StorageBytes, qty, pk.Adds.StorageBytes, &fits)
+ l.EmailPerDay = addPack(l.EmailPerDay, qty, pk.Adds.EmailPerDay, &fits)
+ l.BandwidthBytesMonth = addPack(l.BandwidthBytesMonth, qty, pk.Adds.BandwidthBytesMonth, &fits)
+ if !fits {
+ return Limits{}, fmt.Errorf("%w: %q overflows", ErrPackQuantity, name)
+ }
}
ce := c.Ceilings
l.RunningThings = clamp(l.RunningThings, ce.RunningThings)
@@ -261,3 +266,14 @@ func (l Limits) generous(o Limits) Limits {
}
return l
}
+
+// addPack is v + qty*add, refusing (ok = false) anything that would overflow
+// int64. Values are non-negative (validate), so only the upper bound matters;
+// int is 64-bit on every platform this builds for.
+func addPack[T int | int64](v T, qty int, add T, fits *bool) T {
+ if add != 0 && int64(qty) > (math.MaxInt64-int64(v))/int64(add) {
+ *fits = false
+ return v
+ }
+ return v + T(qty)*add
+}
diff --git a/internal/plans/catalog_test.go b/internal/plans/catalog_test.go
index ce6c86c0f..3728f2649 100644
--- a/internal/plans/catalog_test.go
+++ b/internal/plans/catalog_test.go
@@ -202,3 +202,17 @@ func TestEffectiveSponsoredZeroIsUnset(t *testing.T) {
t.Fatalf("min_schedule_seconds %d, want 60", got.MinScheduleSeconds)
}
}
+
+// Astra plans-1 finding 5: an accepted catalog with a huge pack increment
+// must never wrap to a negative limit that the ceiling cannot correct.
+func TestEffectiveRefusesOverflow(t *testing.T) {
+ c := exampleCatalog(t)
+ pk := c.Packs["storage"]
+ pk.Adds.StorageBytes = 1 << 62
+ c.Packs["storage"] = pk
+ c.Ceilings.StorageBytes = 100
+ got, err := c.Effective("personal", map[string]int{"storage": 2}, nil)
+ if !errors.Is(err, ErrPackQuantity) {
+ t.Fatalf("err %v (limits %+v), want ErrPackQuantity", err, got)
+ }
+}
diff --git a/internal/plans/marker.go b/internal/plans/marker.go
new file mode 100644
index 000000000..d39ee9e6c
--- /dev/null
+++ b/internal/plans/marker.go
@@ -0,0 +1,60 @@
+package plans
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// MarkerSchema is the deployment marker's wire version.
+const MarkerSchema = 1
+
+// Marker says whether plans are on for the deployment (plans 2). The console
+// writes it once at start; boxes read it on every pass and read no limits
+// record while it is absent or disabled, so a deployment without a catalog
+// costs a box one small read per pass and changes nothing.
+type Marker struct {
+ Schema int `json:"schema"`
+ Enabled bool `json:"enabled"`
+ CatalogID string `json:"catalog_id,omitempty"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+// MarkerKey is where the deployment marker lives.
+func MarkerKey() string { return "control/plans.json" }
+
+// GetMarker reads the marker and its etag. Absent is nil, "", nil. An unknown
+// schema is an error, so a reader keeps whatever it last knew.
+func GetMarker(ctx context.Context, s release.Store) (*Marker, string, error) {
+ raw, etag, err := s.Get(ctx, MarkerKey())
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, "", nil
+ }
+ return nil, "", err
+ }
+ var m Marker
+ if err := json.Unmarshal(raw, &m); err != nil {
+ return nil, "", fmt.Errorf("plans: %s: %w", MarkerKey(), err)
+ }
+ if m.Schema != MarkerSchema {
+ return nil, "", fmt.Errorf("plans: %s: schema %d, want %d", MarkerKey(), m.Schema, MarkerSchema)
+ }
+ return &m, etag, nil
+}
+
+// PutMarker CAS-writes the marker: ifMatch "" is create-only, otherwise
+// replace-only. Schema is stamped here.
+func PutMarker(ctx context.Context, s release.Store, m *Marker, ifMatch string) (string, error) {
+ m.Schema = MarkerSchema
+ body, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ return "", err
+ }
+ return s.Put(ctx, MarkerKey(), append(body, '\n'), ifMatch)
+}
diff --git a/internal/plans/marker_test.go b/internal/plans/marker_test.go
new file mode 100644
index 000000000..b01b1ee20
--- /dev/null
+++ b/internal/plans/marker_test.go
@@ -0,0 +1,58 @@
+package plans
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func TestMarkerKey(t *testing.T) {
+ if got := MarkerKey(); got != "control/plans.json" {
+ t.Fatalf("MarkerKey = %q", got)
+ }
+}
+
+func TestMarkerRoundTrip(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ if m, etag, err := GetMarker(ctx, s); m != nil || etag != "" || err != nil {
+ t.Fatalf("absent: %+v, %q, %v", m, etag, err)
+ }
+ want := &Marker{Enabled: true, CatalogID: "v1@0123456789ab", UpdatedAt: time.Unix(1_800_000_000, 0).UTC()}
+ if _, err := PutMarker(ctx, s, want, ""); err != nil {
+ t.Fatal(err)
+ }
+ got, etag, err := GetMarker(ctx, s)
+ if err != nil || etag == "" {
+ t.Fatalf("get: %q, %v", etag, err)
+ }
+ want.Schema = MarkerSchema
+ if *got != *want {
+ t.Fatalf("got %+v, want %+v", got, want)
+ }
+}
+
+func TestMarkerRefusesUnknownSchema(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ if _, err := s.Put(ctx, MarkerKey(), []byte(`{"schema": 2, "enabled": true}`), ""); err != nil {
+ t.Fatal(err)
+ }
+ if m, _, err := GetMarker(ctx, s); err == nil {
+ t.Fatalf("accepted schema 2: %+v", m)
+ }
+}
+
+func TestPutMarkerIsCAS(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ if _, err := PutMarker(ctx, s, &Marker{Enabled: true}, ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := PutMarker(ctx, s, &Marker{Enabled: false}, ""); !errors.Is(err, release.ErrCASConflict) {
+ t.Fatalf("second create-only put: %v", err)
+ }
+}