feat: enforce shared application resource quotas across namespaces

This commit is contained in:
2026-09-17 17:57:54 +08:00
parent 5c8825e4bc
commit 993786476e
43 changed files with 1606 additions and 49 deletions

View File

@@ -50,7 +50,7 @@ func Parse(value string) (Token, error) {
return Token{NamespaceID: ns, CredentialID: id, Digest: core.Digest(sha256.Sum256(secret))}, nil
}
// Permissions 是凭据的授权快照,动作与队列范围必须同时匹配。
// Permissions 是凭据的授权快照;队列动作同时核验队列范围,显式应用动作作用于整个所属应用。
type Permissions struct {
// Actions 明确授权的动作集合,首版不使用动作通配符。
Actions []string `json:"actions"`
@@ -58,7 +58,7 @@ type Permissions struct {
Queues []string `json:"queues"`
}
var supported = map[string]bool{"tasks:submit": true, "tasks:read": true, "tasks:cancel": true, "queues:read": true, "queues:manage": true, "workers:connect": true, "workers:consume": true, "schedules:read": true, "schedules:manage": true}
var supported = map[string]bool{"application:resources:read": true, "application:resources:manage": true, "tasks:submit": true, "tasks:read": true, "tasks:cancel": true, "queues:read": true, "queues:manage": true, "workers:connect": true, "workers:consume": true, "schedules:read": true, "schedules:manage": true}
func (p Permissions) Validate() error {
if len(p.Actions) == 0 || len(p.Actions) > 32 || len(p.Queues) == 0 || len(p.Queues) > 128 {

View File

@@ -0,0 +1,38 @@
package model
import (
"time"
core "git.yuntuo.hk/k2safe/orbit/model"
)
// ApplicationResources 是应用下全部命名空间和队列共享的额度与持久化用量。
// 独立于身份行,避免认证共享锁升级;不使用数据库外键。
type ApplicationResources struct {
// ApplicationID 所属应用标识,同时是主键;应用归属不可在线迁移。
ApplicationID core.ID `db:"application_id"`
// Revision 配置版本从 1 开始;用量变化不修改配置版本。
Revision uint64 `db:"revision"`
// CreatedAt 创建时间,UTC 微秒。
CreatedAt time.Time `db:"created_at"`
// UpdatedAt 最后配置变更时间,UTC 微秒;不是用量采样时间。
UpdatedAt time.Time `db:"updated_at"`
// MaxUnfinishedTasks 未终结任务上限;降额允许当前用量暂时高于上限。
MaxUnfinishedTasks uint32 `db:"max_unfinished_tasks"`
// UnfinishedTasks 等待、预占、运行和待核查的总数;重试不重复占用。
UnfinishedTasks uint64 `db:"unfinished_tasks"`
// MaxConcurrency 允许预占和运行的尝试总数;降额不撤销已有尝试。
MaxConcurrency uint32 `db:"max_concurrency"`
// ActiveAttempts 当前预占或运行的尝试数,到期恢复或结果确认时释放。
ActiveAttempts uint64 `db:"active_attempts"`
// PendingStarts 已预占但未开始的尝试数,属于 ActiveAttempts 的子集。
PendingStarts uint64 `db:"pending_starts"`
// RatePerSecond 一秒固定窗口允许的开始授权次数,不是业务 HTTP 请求限流。
RatePerSecond uint32 `db:"rate_per_second"`
// StartWindowUntil 数据库时间计算的窗口结束时刻;空表示尚未建立窗口。
StartWindowUntil *time.Time `db:"start_window_until"`
// StartsInWindow 当前窗口已确认的开始次数;回执重报不重复计数。
StartsInWindow uint32 `db:"starts_in_window"`
}
func (ApplicationResources) TableName() string { return "orbit_application_resources" }

View File

@@ -0,0 +1,79 @@
// Package resources 管理当前凭据所属应用的共享额度,应用管理动作需单独授权。
package resources
import (
"context"
"strings"
"unicode/utf8"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
)
// View 返回配置与当前用量;运行计数不参与配置版本。
type View struct {
// ApplicationID 所属应用标识,由凭据归属确定。
ApplicationID core.ID `json:"application_id"`
// Revision 仅配置变更增加的版本;JSON 使用十进制字符串。
Revision uint64 `json:"revision,string"`
// MaxUnfinishedTasks 跨命名空间的未终结任务上限,包含待核查任务。
MaxUnfinishedTasks uint32 `json:"max_unfinished_tasks"`
// UnfinishedTasks 当前未终结任务总数;自动重试不重复占用。
UnfinishedTasks uint64 `json:"unfinished_tasks,string"`
// MaxConcurrency 跨队列的预占及运行尝试总上限。
MaxConcurrency uint32 `json:"max_concurrency"`
// ActiveAttempts 当前预占与运行的尝试数。
ActiveAttempts uint64 `json:"active_attempts,string"`
// PendingStarts 已预占但尚未开始的尝试数,属于 ActiveAttempts。
PendingStarts uint64 `json:"pending_starts,string"`
// RatePerSecond 每秒固定窗口允许的开始授权次数。
RatePerSecond uint32 `json:"rate_per_second"`
}
// ConfigureInput 完整替换三个正整数上限;要求版本和审计原因。
type ConfigureInput struct {
// ExpectedRevision 管理命令预期配置版本,须保留原值重报。
ExpectedRevision uint64 `json:"expected_revision,string"`
// Reason 非空审计原因,最多 1024 个 Unicode 字符。
Reason string `json:"reason"`
// MaxUnfinishedTasks 跨命名空间的未终结任务上限,包含待核查任务。
MaxUnfinishedTasks uint32 `json:"max_unfinished_tasks"`
// MaxConcurrency 跨队列的预占及运行尝试总上限。
MaxConcurrency uint32 `json:"max_concurrency"`
// RatePerSecond 每秒固定窗口允许的开始授权次数。
RatePerSecond uint32 `json:"rate_per_second"`
}
type Result struct {
// Resources 配置成功时的原用量快照,重报不刷新。
Resources View `json:"resources"`
// Deduplicated 是否返回先前已提交的同身份回执。
Deduplicated bool `json:"deduplicated"`
}
type Store interface {
GetApplicationResources(context.Context, auth.Token) (View, error)
ConfigureApplicationResources(context.Context, auth.Token, string, ConfigureInput) (Result, error)
}
type Service struct{ store Store }
func New(store Store) *Service { return &Service{store: store} }
func (s *Service) Get(ctx context.Context, token auth.Token) (View, error) {
return s.store.GetApplicationResources(ctx, token)
}
func (s *Service) Configure(ctx context.Context, token auth.Token, key string, in ConfigureInput) (Result, error) {
if len(key) < 1 || len(key) > 128 {
return Result{}, fault.Invalid("invalid Idempotency-Key")
}
for _, c := range key {
if c < 33 || c > 126 {
return Result{}, fault.Invalid("invalid Idempotency-Key")
}
}
if in.ExpectedRevision == 0 || strings.TrimSpace(in.Reason) == "" || !utf8.ValidString(in.Reason) || utf8.RuneCountInString(in.Reason) > 1024 {
return Result{}, fault.Invalid("reason and nonzero expected_revision are required")
}
if in.MaxUnfinishedTasks < 1 || in.MaxUnfinishedTasks > 100000000 || in.MaxConcurrency < 1 || in.MaxConcurrency > 1000000 || in.RatePerSecond < 1 || in.RatePerSecond > 10000000 {
return Result{}, fault.Invalid("application limits are invalid")
}
return s.store.ConfigureApplicationResources(ctx, token, key, in)
}