Thread int32 saturation clamps from tiered settlement and video task recompute into the consume/task logs under admin_info, so oversized or malformed billing inputs stay auditable. Clamp negative audio duration before token conversion and gate the saturation UI markers on admin.
118 lines
4.6 KiB
Go
118 lines
4.6 KiB
Go
package common
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
|
|
"github.com/shopspring/decimal"
|
|
)
|
|
|
|
// Quota conversions are centralized here so every billing path shares one
|
|
// saturation + logging policy. Quota columns (user/token/log) are 32-bit
|
|
// integers in the database, so an oversized product must clamp to the int32
|
|
// range instead of wrapping around and turning a charge into a credit.
|
|
const (
|
|
MaxQuota = math.MaxInt32
|
|
MinQuota = math.MinInt32
|
|
)
|
|
|
|
// Clamp kinds reported by QuotaClamp.Kind.
|
|
const (
|
|
QuotaClampOverflow = "overflow"
|
|
QuotaClampUnderflow = "underflow"
|
|
QuotaClampNaN = "nan"
|
|
)
|
|
|
|
// QuotaClamp describes a single saturation event: a quota conversion whose
|
|
// input fell outside the representable int32 range (or was NaN) and was
|
|
// therefore clamped. It is surfaced to billing callers so the event can be
|
|
// recorded on the related consume/task log for admin auditing.
|
|
type QuotaClamp struct {
|
|
Op string `json:"op"` // "QuotaFromFloat" | "QuotaRound" | "QuotaFromDecimal"
|
|
Kind string `json:"kind"` // "overflow" | "underflow" | "nan"
|
|
Original float64 `json:"original"` // best-effort pre-clamp value (decimal -> float64 approx)
|
|
Clamped int `json:"clamped"` // the saturated result actually used
|
|
}
|
|
|
|
// AuditMap renders the clamp as the marker stored under a log's
|
|
// admin_info.quota_saturation. Centralized here so every billing path (consume
|
|
// logs, task billing logs, task compensation logs) records the same shape.
|
|
func (c *QuotaClamp) AuditMap() map[string]interface{} {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
return map[string]interface{}{
|
|
"op": c.Op,
|
|
"kind": c.Kind,
|
|
"original": c.Original,
|
|
"clamped": c.Clamped,
|
|
}
|
|
}
|
|
|
|
// saturateQuota converts an already-rounded quota value to int, clamping to
|
|
// the int32 range. Whenever clamping (what would otherwise be an integer
|
|
// wraparound) or a NaN fallback is triggered it logs a warning, because in
|
|
// normal operation a single request never approaches these bounds — hitting
|
|
// them signals a bug or an abusive request. `op` names the caller. When a
|
|
// clamp occurs it returns a non-nil *QuotaClamp so callers can additionally
|
|
// record the event (e.g. on the consume log); the returned pointer is nil for
|
|
// in-range values.
|
|
func saturateQuota(value float64, op string) (int, *QuotaClamp) {
|
|
switch {
|
|
case math.IsNaN(value):
|
|
SysError(fmt.Sprintf("quota conversion (%s) received NaN, falling back to 0", op))
|
|
return 0, &QuotaClamp{Op: op, Kind: QuotaClampNaN, Original: value, Clamped: 0}
|
|
case value >= MaxQuota:
|
|
SysError(fmt.Sprintf("quota conversion (%s) overflow: %g exceeds max quota, clamped to %d", op, value, MaxQuota))
|
|
return MaxQuota, &QuotaClamp{Op: op, Kind: QuotaClampOverflow, Original: value, Clamped: MaxQuota}
|
|
case value <= MinQuota:
|
|
SysError(fmt.Sprintf("quota conversion (%s) underflow: %g below min quota, clamped to %d", op, value, MinQuota))
|
|
return MinQuota, &QuotaClamp{Op: op, Kind: QuotaClampUnderflow, Original: value, Clamped: MinQuota}
|
|
default:
|
|
return int(value), nil
|
|
}
|
|
}
|
|
|
|
// QuotaFromFloat converts a computed quota value to int, truncating toward
|
|
// zero, with saturation. Use for float products of prices, ratios, and
|
|
// user-controlled multipliers (image n, video seconds, resolution ratios).
|
|
func QuotaFromFloat(value float64) int {
|
|
quota, _ := QuotaFromFloatChecked(value)
|
|
return quota
|
|
}
|
|
|
|
// QuotaFromFloatChecked is QuotaFromFloat but also returns a non-nil
|
|
// *QuotaClamp when the value was clamped, so billing callers can audit it.
|
|
func QuotaFromFloatChecked(value float64) (int, *QuotaClamp) {
|
|
return saturateQuota(value, "QuotaFromFloat")
|
|
}
|
|
|
|
// QuotaRound converts a float64 quota value to int using half-away-from-zero
|
|
// rounding, with saturation. Every tiered billing path (pre-consume,
|
|
// settlement, breakdown validation, log fields) MUST use this to avoid +-1
|
|
// discrepancies.
|
|
func QuotaRound(value float64) int {
|
|
quota, _ := QuotaRoundChecked(value)
|
|
return quota
|
|
}
|
|
|
|
// QuotaRoundChecked is QuotaRound but also returns a non-nil *QuotaClamp when
|
|
// the value was clamped, so billing callers can audit it.
|
|
func QuotaRoundChecked(value float64) (int, *QuotaClamp) {
|
|
return saturateQuota(math.Round(value), "QuotaRound")
|
|
}
|
|
|
|
// QuotaFromDecimal converts a computed quota decimal to int with saturation.
|
|
// The decimal is rounded (half away from zero) before conversion.
|
|
func QuotaFromDecimal(d decimal.Decimal) int {
|
|
quota, _ := QuotaFromDecimalChecked(d)
|
|
return quota
|
|
}
|
|
|
|
// QuotaFromDecimalChecked is QuotaFromDecimal but also returns a non-nil
|
|
// *QuotaClamp when the value was clamped, so billing callers can audit it.
|
|
func QuotaFromDecimalChecked(d decimal.Decimal) (int, *QuotaClamp) {
|
|
f, _ := d.Round(0).Float64()
|
|
return saturateQuota(f, "QuotaFromDecimal")
|
|
}
|