Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/reservations/armreservations v1.1.0
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261012130811-9dae64b216d4
github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261012130811-9dae64b216d4
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20261012130811-9dae64b216d4
github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20261012130811-9dae64b216d4
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261012205319-ad67030387e4
github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261012205319-ad67030387e4
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20261012205319-ad67030387e4
github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20261012205319-ad67030387e4
github.com/aws/aws-lambda-go v1.47.0
github.com/aws/aws-sdk-go-v2/service/kms v1.50.4
github.com/aws/aws-sdk-go-v2/service/lambda v1.89.0
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,14 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261012130811-9dae64b216d4 h1:4C/4ZLemkgC45z47LtCjICglnfcTBzP2KUAq/gZEzZQ=
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261012130811-9dae64b216d4/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8=
github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261012130811-9dae64b216d4 h1:fnEv+2eCL0ObXn1nAFlBa2kZy1A0/nhc7LxYeDFCCkQ=
github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261012130811-9dae64b216d4/go.mod h1:C0ildZeRds0aRvDZp49MLY0kFWh/4FiM4lPWWWd3HFA=
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20261012130811-9dae64b216d4 h1:HljKKZOmfam4C3T5109DMeUQuyxVHRohroA4j/OU5oY=
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20261012130811-9dae64b216d4/go.mod h1:nM3Zqd5N2mMIsNoTtnKF6RFyM1E3HVjAGIsXVx9b3bY=
github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20261012130811-9dae64b216d4 h1:o9hnEg+Ro/RynW8ZWDVMC0E2yWt08izWeFbrCsZZx/c=
github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20261012130811-9dae64b216d4/go.mod h1:uJ7UpLaUqu1fpfCEECuqjLXV+As2tibtPCapPF3AZys=
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261012205319-ad67030387e4 h1:cXoxVl0G696HrsXGc0t2jKH5iHUQe+i9pL/q4dnFPSc=
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261012205319-ad67030387e4/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8=
github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261012205319-ad67030387e4 h1:PCPhtB3yJsLdg6zitDgNxgntHGq3unUn0NoyV1u2PwY=
github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261012205319-ad67030387e4/go.mod h1:fiu6WQmUGRdViiOf45QV235Cowq8j0yn8ahGWCGEFn8=
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20261012205319-ad67030387e4 h1:dbkTxvqS1xWTvCSKulDWL0UIuQSZ1JTBRw14OW/U+wU=
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20261012205319-ad67030387e4/go.mod h1:8vqO2ZlDzjX/Z4bESZVp/IenhTifkySeF0VJwQKihLA=
github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20261012205319-ad67030387e4 h1:T3woCHq7rBuExSd0+BI1BYfF0Pd5AMgpngnc8Bqepd0=
github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20261012205319-ad67030387e4/go.mod h1:J4H0b34EwQQ4p9cM3amsIU9tFmbzecS8E00ybjVp56g=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI=
Expand Down
106 changes: 78 additions & 28 deletions internal/server/handler_ladder.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ func (app *Application) processOneLadderConfig(
// here: they are executed purchases already reflected in the engine's
// ExistingUSDPerHour, so counting them again would double-subtract.
//
// Units: tranche amounts are stored exactly as Allocate emits them, which is
// ON-DEMAND-EQUIVALENT $/hr until Allocate converts to commitment dollars
// (go#349). Do not convert here.
//
// Persistence is APPEND-ONLY (see persistLadderRun): every run inserts its new
// scheduled tranches and NEVER supersedes existing ones. Netting already
// subtracted the config's existing scheduled tranches from the gap, so each run
Expand Down Expand Up @@ -364,7 +368,14 @@ func (app *Application) executeLadderRun(
}

// Assemble LadderPlan (Q5: interleaves purchases AND reshapes in one JSON).
plan := assembleLadderPlan(scope, now, allocResult, layerStates, baseline, engineCfg.TargetCoveragePct, term, paymentOpt)
// The plan is display only: its purchase amounts are the immediate tranches
// that persistLadderRun writes, so it never authorizes more than the rows
// that netting sees.
immediate, err := immediateTrancheAmounts(trancheResult.Tranches, now)
if err != nil {
return err
}
plan := assembleLadderPlan(scope, now, allocResult, immediate, layerStates, baseline, engineCfg.TargetCoveragePct, term, paymentOpt)

// Validate and persist the run row + tranche audit rows. L5 append-only:
// SaveLadderRunWithTranches inserts the run and its NEW scheduled tranches
Expand Down Expand Up @@ -403,27 +414,30 @@ func (app *Application) persistLadderRun(
modeStr := dbCfg.Mode
cadenceStr := dbCfg.Cadence
runRow := &config.LadderRunDB{
ID: runID,
ConfigID: &cfgID,
StartedAt: now,
Status: pkgladder.RunStatusPlanned,
Mode: &modeStr,
Cadence: &cadenceStr,
Plan: planJSON,
BaselineUSDHr: baseline.LowWaterUSDPerHour,
TargetUSDHr: ratToFloat64Ptr(plan.TargetUSDPerHour),
ExistingUSDHr: ratToFloat64Ptr(plan.ExistingUSDPerHour),
GapUSDHr: ratToFloat64Ptr(plan.GapUSDPerHour),
TotalHourlyCommit: allocTotalHourlyCommit(allocResult.Allocations),
}

// Build the tranche audit rows (status=scheduled; no firing in PR-2). The
// run ID is known up front (runRow.ID), so tranches can reference it before
// the insert.
ID: runID,
ConfigID: &cfgID,
StartedAt: now,
Status: pkgladder.RunStatusPlanned,
Mode: &modeStr,
Cadence: &cadenceStr,
Plan: planJSON,
BaselineUSDHr: baseline.LowWaterUSDPerHour,
TargetUSDHr: ratToFloat64Ptr(plan.TargetUSDPerHour),
ExistingUSDHr: ratToFloat64Ptr(plan.ExistingUSDPerHour),
GapUSDHr: ratToFloat64Ptr(plan.GapUSDPerHour),
}

// Build the tranche rows (status=scheduled; nothing fires them yet). EVERY
// ramp step is a row, including the immediate one (scheduled_date = run
// start), so in-flight netting sees it. The run ID is known up front
// (runRow.ID), so tranches can reference it before the insert.
trancheRows, err := buildTrancheDBRows(trancheResult.Tranches, runRow.ID, &cfgID)
if err != nil {
return fmt.Errorf("buildTrancheDBRows: %w", err)
}
// total_hourly_commit is what this run persists, not the allocation gap, so
// it always equals the sum of the rows netting will read back.
runRow.TotalHourlyCommit = sumTrancheRowAmounts(trancheRows)

// L5 APPEND-ONLY: always persist via the plain path, which INSERTS the new
// scheduled tranches and never supersedes upcoming ones (it only cancels
Expand Down Expand Up @@ -530,7 +544,7 @@ func ladderConfigToEngine(dbCfg *config.LadderConfigDB, accountID string) (pkgla

// assembleLadderPlan builds a LadderPlan from the Allocate result and the
// collected read-side data. Q5: Actions interleaves both purchases
// (Allocations) and Reshapes in one authoritative plan JSON. Term and
// (immediate ramp steps only) and Reshapes in one display-only plan JSON. Term and
// PaymentOption are stamped on every ActionPurchase entry; they are required
// by PlannedAction.Validate for purchase actions.
//
Expand All @@ -548,19 +562,28 @@ func assembleLadderPlan(
scope pkgladder.Scope,
now time.Time,
allocResult *pkgladder.AllocateResult,
immediate map[pkgladder.LayerType]*big.Rat,
layerStates map[pkgladder.LayerType]pkgladder.LayerState,
baseline pkgladder.UsageBaseline,
targetCoveragePct float64,
term pkgladder.Term,
paymentOpt pkgladder.PaymentOption,
) *pkgladder.LadderPlan {
actions := make([]pkgladder.PlannedAction, 0, len(allocResult.Allocations)+len(allocResult.Reshapes)+len(allocResult.Holds))
// Purchase allocations come first so approval-email ordering is stable.
// Purchase actions come first so approval-email ordering is stable. Each
// carries only the immediate ramp step for its layer, not the allocation
// gap: the later steps are persisted tranches, and a layer whose ramp has no
// immediate step has no purchase action. The plan is display only; the sole
// purchase path is tranche -> purchase_executions (not built yet).
for _, alloc := range allocResult.Allocations {
amount := immediate[alloc.Layer]
if amount == nil {
continue
}
actions = append(actions, pkgladder.PlannedAction{
Action: pkgladder.ActionPurchase,
Layer: alloc.Layer,
AmountUSDPerHour: alloc.GapUSDPerHour,
AmountUSDPerHour: amount,
Term: term,
PaymentOption: paymentOpt,
Rationale: alloc.Rationale,
Expand Down Expand Up @@ -642,13 +665,40 @@ func sumAllocationGaps(allocs []pkgladder.Allocation) *big.Rat {
return sum
}

// allocTotalHourlyCommit sums the GapUSDPerHour across all allocations,
// converting to float64. This is the non-nullable total_hourly_commit stored
// in the run row; it starts at 0 for a no-op (Hold-only) plan, which is the
// correct semantic (zero new commitment, not "not computed").
func allocTotalHourlyCommit(allocs []pkgladder.Allocation) float64 {
f, _ := sumAllocationGaps(allocs).Float64()
return f
// sumTrancheRowAmounts sums the amounts of the tranche rows a run persists.
// This is the non-nullable total_hourly_commit stored in the run row; it is 0
// for a no-op (Hold-only) plan, which is the correct semantic (zero new
// commitment, not "not computed"). Units are whatever Allocate emits:
// on-demand-equivalent $/hr until the commitment-dollar conversion lands
// (go#349); this code stores amounts unchanged.
func sumTrancheRowAmounts(rows []config.LadderTrancheDB) float64 {
var sum float64
for i := range rows {
sum += rows[i].AmountUSDHr
}
return sum
}

// immediateTrancheAmounts sums, per layer, the tranches that fire at the run
// start (FireAfter <= now: the AfterDays==0 ramp step). It fails loud on a
// malformed amount, like buildTrancheDBRows.
func immediateTrancheAmounts(tranches []pkgladder.Tranche, now time.Time) (map[pkgladder.LayerType]*big.Rat, error) {
sums := make(map[pkgladder.LayerType]*big.Rat)
for i := range tranches {
tr := &tranches[i]
if tr.FireAfter.After(now) {
continue
}
r, ok := new(big.Rat).SetString(tr.AmountUSDPerHour)
if !ok {
return nil, fmt.Errorf("tranche %s: malformed amount_usd_per_hour %q (not a parseable rational)", tr.ID, tr.AmountUSDPerHour)
}
if sums[tr.Layer] == nil {
sums[tr.Layer] = new(big.Rat)
}
sums[tr.Layer].Add(sums[tr.Layer], r)
}
return sums, nil
}

// buildTrancheDBRows converts pkg/ladder Tranche rows to config.LadderTrancheDB
Expand Down
Loading
Loading