初始提交:OCI 面板后端(含 GenAI 网关一期)

This commit is contained in:
Wang Defa
2026-07-09 15:31:04 +08:00
commit b9a3e97e84
168 changed files with 31794 additions and 0 deletions
+361
View File
@@ -0,0 +1,361 @@
package oci
import (
"context"
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/oracle/oci-go-sdk/v65/common"
"github.com/oracle/oci-go-sdk/v65/identity"
"github.com/oracle/oci-go-sdk/v65/limits"
)
// maxAvailabilityLookups 限制附带用量查询的条数:
// GetResourceAvailability 每条配额一次调用(OCI 无批量接口),
// 定义预筛 + 并发查询后 100 条约需 1~3 秒。
const maxAvailabilityLookups = 100
// limitsPageSize 是 limits 系列 List 接口的显式分页大小(OCI 通用上限 1000);
// 不传时服务端默认约百条一页,全量拉取要反复翻页。
const limitsPageSize = 1000
// availabilityConcurrency 是用量查询并发度;Limits 端点 TPS 不高,并发过高易撞限流。
const availabilityConcurrency = 8
// limitDefTTL 是配额定义缓存时长:定义(是否支持用量查询)几乎不随时间变化。
const limitDefTTL = 6 * time.Hour
// RegionSubscription 是租户对一个区域的订阅状态。
type RegionSubscription struct {
Key string `json:"key"`
Name string `json:"name"`
Status string `json:"status"`
IsHomeRegion bool `json:"isHomeRegion"`
}
// LimitValue 是一条服务配额;Used/Available 仅在请求用量时填充,
// 且 OCI 对部分配额不提供用量数据,此时保持为 nil。
type LimitValue struct {
Name string `json:"name"`
ScopeType string `json:"scopeType"`
AvailabilityDomain string `json:"availabilityDomain,omitempty"`
Value int64 `json:"value"`
Used *int64 `json:"used,omitempty"`
Available *int64 `json:"available,omitempty"`
}
// LimitsQuery 是一次配额查询的条件。
type LimitsQuery struct {
Region string // 目标区域,空则用凭据默认区域
Service string // 服务名,如 compute
Name string // 可选,配额名模糊过滤(大小写不敏感子串,本地过滤)
ScopeType string // 可选 GLOBAL / REGION / AD,下推给 OCI 过滤
AvailabilityDomain string // 可选,AD 全名,下推给 OCI 过滤
NonZero bool // 过滤掉上限为 0 的配额项(在用量查询前生效,不占并发名额)
WithAvailability bool // 并发附带 used/available
}
// LimitService 是配额体系支持的一个服务。
type LimitService struct {
Name string `json:"name"`
Description string `json:"description"`
}
// ListRegionSubscriptions 实现 Client。
func (c *RealClient) ListRegionSubscriptions(ctx context.Context, cred Credentials) ([]RegionSubscription, error) {
ic, err := identity.NewIdentityClientWithConfigurationProvider(provider(cred))
if err != nil {
return nil, fmt.Errorf("new identity client: %w", err)
}
applyProxy(&ic.BaseClient, cred)
resp, err := ic.ListRegionSubscriptions(ctx, identity.ListRegionSubscriptionsRequest{
TenancyId: &cred.TenancyOCID,
})
if err != nil {
return nil, fmt.Errorf("list region subscriptions: %w", err)
}
subs := make([]RegionSubscription, 0, len(resp.Items))
for _, item := range resp.Items {
subs = append(subs, RegionSubscription{
Key: deref(item.RegionKey),
Name: deref(item.RegionName),
Status: string(item.Status),
IsHomeRegion: item.IsHomeRegion != nil && *item.IsHomeRegion,
})
}
SortRegionSubscriptions(subs)
return subs, nil
}
// SortRegionSubscriptions 主区域置顶,其余按友好名(alias)字典序,
// 本地表查不到 alias 时回退区域名;实时接口与缓存读取共用,全局统一展示顺序。
func SortRegionSubscriptions(subs []RegionSubscription) {
sortKey := func(sub RegionSubscription) string {
if info, ok := RegionByKey(sub.Key); ok {
return info.Alias
}
return sub.Name
}
sort.SliceStable(subs, func(i, j int) bool {
if subs[i].IsHomeRegion != subs[j].IsHomeRegion {
return subs[i].IsHomeRegion
}
return sortKey(subs[i]) < sortKey(subs[j])
})
}
// SubscribeRegion 实现 Client:订阅新区域。OCI 要求该请求发往
// home region,且区域订阅一经创建不可取消。
func (c *RealClient) SubscribeRegion(ctx context.Context, cred Credentials, homeRegion, regionKey string) error {
ic, err := identity.NewIdentityClientWithConfigurationProvider(provider(cred))
if err != nil {
return fmt.Errorf("new identity client: %w", err)
}
applyProxy(&ic.BaseClient, cred)
if homeRegion != "" {
ic.SetRegion(normalizeRegion(homeRegion))
}
_, err = ic.CreateRegionSubscription(ctx, identity.CreateRegionSubscriptionRequest{
TenancyId: &cred.TenancyOCID,
CreateRegionSubscriptionDetails: identity.CreateRegionSubscriptionDetails{
RegionKey: &regionKey,
},
})
if err != nil {
return fmt.Errorf("create region subscription %s: %w", regionKey, err)
}
return nil
}
// limitsClient 构造 limits 服务客户端;region 为空时用凭据默认区域。
func limitsClient(cred Credentials, region string) (limits.LimitsClient, error) {
lc, err := limits.NewLimitsClientWithConfigurationProvider(provider(cred))
if err != nil {
return lc, fmt.Errorf("new limits client: %w", err)
}
applyProxy(&lc.BaseClient, cred)
if region != "" {
lc.SetRegion(normalizeRegion(region))
}
return lc, nil
}
// ListLimits 实现 Client:全量拉取配额(scopeType / AD 条件下推),
// 名称本地子串过滤,按需并发附带用量。
func (c *RealClient) ListLimits(ctx context.Context, cred Credentials, q LimitsQuery) ([]LimitValue, error) {
lc, err := limitsClient(cred, q.Region)
if err != nil {
return nil, err
}
values, err := listLimitValues(ctx, lc, cred.TenancyOCID, q)
if err != nil {
return nil, err
}
values = filterLimitsByName(values, q.Name)
if q.NonZero {
values = filterNonZero(values)
}
if !q.WithAvailability {
return values, nil
}
if len(values) > maxAvailabilityLookups {
return nil, fmt.Errorf("list limits: %d entries exceed availability lookup cap %d, narrow with name filter",
len(values), maxAvailabilityLookups)
}
supported := c.availabilitySupport(ctx, lc, cred.TenancyOCID, q.Region, q.Service)
attachAvailability(ctx, lc, cred.TenancyOCID, q.Service, values, supported)
return values, nil
}
// listLimitValues 分页取回一个服务的配额;scopeType / availabilityDomain 下推给 OCI。
// OCI 的 name 参数只支持精确匹配,子串过滤由调用方本地完成。
func listLimitValues(ctx context.Context, lc limits.LimitsClient, tenancyOCID string, q LimitsQuery) ([]LimitValue, error) {
var values []LimitValue
var page *string
for {
req := limits.ListLimitValuesRequest{
CompartmentId: &tenancyOCID,
ServiceName: &q.Service,
Limit: common.Int(limitsPageSize),
Page: page,
}
if q.ScopeType != "" {
req.ScopeType = limits.ListLimitValuesScopeTypeEnum(q.ScopeType)
}
if q.AvailabilityDomain != "" {
req.AvailabilityDomain = &q.AvailabilityDomain
}
resp, err := lc.ListLimitValues(ctx, req)
if err != nil {
return nil, fmt.Errorf("list limit values of %s: %w", q.Service, err)
}
for _, item := range resp.Items {
values = append(values, toLimitValue(item))
}
if resp.OpcNextPage == nil {
return orEmpty(values), nil
}
page = resp.OpcNextPage
}
}
func toLimitValue(item limits.LimitValueSummary) LimitValue {
var value int64
if item.Value != nil {
value = *item.Value
}
return LimitValue{
Name: deref(item.Name),
ScopeType: string(item.ScopeType),
AvailabilityDomain: deref(item.AvailabilityDomain),
Value: value,
}
}
// filterLimitsByName 按大小写不敏感的子串匹配过滤配额名。
func filterLimitsByName(values []LimitValue, name string) []LimitValue {
if name == "" {
return values
}
needle := strings.ToLower(strings.TrimSpace(name))
filtered := make([]LimitValue, 0, len(values))
for _, v := range values {
if strings.Contains(strings.ToLower(v.Name), needle) {
filtered = append(filtered, v)
}
}
return filtered
}
// filterNonZero 过滤掉上限为 0 的配额项。
func filterNonZero(values []LimitValue) []LimitValue {
filtered := make([]LimitValue, 0, len(values))
for _, v := range values {
if v.Value > 0 {
filtered = append(filtered, v)
}
}
return filtered
}
// ListLimitServices 实现 Client:分页列出配额体系支持的全部服务。
func (c *RealClient) ListLimitServices(ctx context.Context, cred Credentials, region string) ([]LimitService, error) {
lc, err := limitsClient(cred, region)
if err != nil {
return nil, err
}
var services []LimitService
var page *string
for {
resp, err := lc.ListServices(ctx, limits.ListServicesRequest{
CompartmentId: &cred.TenancyOCID,
Limit: common.Int(limitsPageSize),
Page: page,
})
if err != nil {
return nil, fmt.Errorf("list limit services: %w", err)
}
for _, item := range resp.Items {
services = append(services, LimitService{
Name: deref(item.Name),
Description: deref(item.Description),
})
}
if resp.OpcNextPage == nil {
return orEmpty(services), nil
}
page = resp.OpcNextPage
}
}
// limitDefEntry 是一份配额定义预筛结果的缓存条目。
type limitDefEntry struct {
expires time.Time
supported map[string]bool
}
// availabilitySupport 返回配额名 → 是否支持用量查询的映射(带 TTL 缓存);
// 拉取失败时返回 nil,调用方视为全部照查(保守降级)。
func (c *RealClient) availabilitySupport(ctx context.Context, lc limits.LimitsClient, tenancyOCID, region, service string) map[string]bool {
key := tenancyOCID + "|" + region + "|" + service
if v, ok := c.limitDefs.Load(key); ok {
if e := v.(limitDefEntry); time.Now().Before(e.expires) {
return e.supported
}
}
supported, err := listLimitDefinitions(ctx, lc, tenancyOCID, service)
if err != nil {
return nil
}
c.limitDefs.Store(key, limitDefEntry{expires: time.Now().Add(limitDefTTL), supported: supported})
return supported
}
// listLimitDefinitions 分页取回一个服务的配额定义,产出配额名 → 是否支持用量查询的映射;
// 字段缺失(nil)视为支持,仅明确 false 的会被预筛跳过。
func listLimitDefinitions(ctx context.Context, lc limits.LimitsClient, tenancyOCID, service string) (map[string]bool, error) {
supported := map[string]bool{}
var page *string
for {
resp, err := lc.ListLimitDefinitions(ctx, limits.ListLimitDefinitionsRequest{
CompartmentId: &tenancyOCID,
ServiceName: &service,
Limit: common.Int(limitsPageSize),
Page: page,
})
if err != nil {
return nil, fmt.Errorf("list limit definitions of %s: %w", service, err)
}
for _, item := range resp.Items {
if item.Name == nil {
continue
}
supported[*item.Name] = item.IsResourceAvailabilitySupported == nil || *item.IsResourceAvailabilitySupported
}
if resp.OpcNextPage == nil {
return supported, nil
}
page = resp.OpcNextPage
}
}
// attachAvailability 并发查询用量:预筛掉明确不支持用量查询的配额,
// 单条失败不影响整体,字段保持 nil。
func attachAvailability(ctx context.Context, lc limits.LimitsClient, tenancyOCID, service string, values []LimitValue, supported map[string]bool) {
sem := make(chan struct{}, availabilityConcurrency)
var wg sync.WaitGroup
for i := range values {
if s, ok := supported[values[i].Name]; ok && !s {
continue
}
wg.Add(1)
sem <- struct{}{}
go func(v *LimitValue) {
defer wg.Done()
defer func() { <-sem }()
fetchAvailability(ctx, lc, tenancyOCID, service, v)
}(&values[i])
}
wg.Wait()
}
// fetchAvailability 查询单条配额的用量;AD 级配额必须带 availabilityDomain。
func fetchAvailability(ctx context.Context, lc limits.LimitsClient, tenancyOCID, service string, v *LimitValue) {
req := limits.GetResourceAvailabilityRequest{
ServiceName: &service,
LimitName: &v.Name,
CompartmentId: &tenancyOCID,
}
if strings.EqualFold(v.ScopeType, "AD") {
req.AvailabilityDomain = &v.AvailabilityDomain
}
resp, err := lc.GetResourceAvailability(ctx, req)
if err != nil {
return
}
v.Used = resp.Used
v.Available = resp.Available
}