github.com/qiniu/go-sdk/v7@v7.27.0/storagev2/region/query.go (about) 1 package region 2 3 import ( 4 "context" 5 "crypto/md5" 6 "fmt" 7 "hash/crc64" 8 "math" 9 "net" 10 "net/http" 11 "os" 12 "path/filepath" 13 "reflect" 14 "sort" 15 "strconv" 16 "strings" 17 "sync" 18 "time" 19 20 "github.com/qiniu/go-sdk/v7/auth" 21 "github.com/qiniu/go-sdk/v7/internal/cache" 22 "github.com/qiniu/go-sdk/v7/internal/clientv2" 23 "github.com/qiniu/go-sdk/v7/internal/hostprovider" 24 "github.com/qiniu/go-sdk/v7/internal/log" 25 "github.com/qiniu/go-sdk/v7/storagev2/backoff" 26 "github.com/qiniu/go-sdk/v7/storagev2/chooser" 27 "github.com/qiniu/go-sdk/v7/storagev2/credentials" 28 "github.com/qiniu/go-sdk/v7/storagev2/resolver" 29 "github.com/qiniu/go-sdk/v7/storagev2/retrier" 30 ) 31 32 type ( 33 // BucketRegionsQuery 空间区域查询器 34 BucketRegionsQuery interface { 35 Query(accessKey, bucketName string) RegionsProvider 36 } 37 38 bucketRegionsQuery struct { 39 bucketHosts Endpoints 40 cache *cache.Cache 41 client clientv2.Client 42 useHttps bool 43 accelerateUploading bool 44 } 45 46 // BucketRegionsQueryOptions 空间区域查询器选项 47 BucketRegionsQueryOptions struct { 48 // 使用 HTTP 协议 49 UseInsecureProtocol bool 50 51 // 是否加速上传 52 AccelerateUploading bool 53 54 // 压缩周期(默认:60s) 55 CompactInterval time.Duration 56 57 // 持久化路径(默认:$TMPDIR/qiniu-golang-sdk/query_v4_01.cache.json) 58 PersistentFilePath string 59 60 // 持久化周期(默认:60s) 61 PersistentDuration time.Duration 62 63 // 单域名重试次数(默认:2) 64 RetryMax int 65 66 // 主备域名冻结时间(默认:600s),当一个域名请求失败(单个域名会被重试 RetryMax 次),会被冻结一段时间,使用备用域名进行重试,在冻结时间内,域名不能被使用,当一个操作中所有域名竣备冻结操作不在进行重试,返回最后一次操作的错误。 67 HostFreezeDuration time.Duration 68 69 // HTTP 客户端,如果不配置则使用默认的 HTTP 客户端 70 Client clientv2.Client 71 72 // 域名解析器,如果不配置则使用默认的域名解析器 73 Resolver resolver.Resolver 74 75 // 域名选择器,如果不配置则使用默认的域名选择器 76 Chooser chooser.Chooser 77 78 // 退避器,如果不配置则使用默认的退避器 79 Backoff backoff.Backoff 80 81 // 域名解析前回调函数 82 BeforeResolve func(*http.Request) 83 84 // 域名解析后回调函数 85 AfterResolve func(*http.Request, []net.IP) 86 87 // 域名解析错误回调函数 88 ResolveError func(*http.Request, error) 89 90 // 退避前回调函数 91 BeforeBackoff func(*http.Request, *retrier.RetrierOptions, time.Duration) 92 93 // 退避后回调函数 94 AfterBackoff func(*http.Request, *retrier.RetrierOptions, time.Duration) 95 96 // 请求前回调函数 97 BeforeRequest func(*http.Request, *retrier.RetrierOptions) 98 99 // 请求后回调函数 100 AfterResponse func(*http.Response, *retrier.RetrierOptions, error) 101 } 102 103 bucketRegionsProvider struct { 104 accessKey string 105 bucketName string 106 cacheKey string 107 query *bucketRegionsQuery 108 accelerateUploading bool 109 } 110 111 v4QueryCacheValue struct { 112 Regions []*Region `json:"regions"` 113 RefreshAfter time.Time `json:"refresh_after"` 114 ExpiredAt time.Time `json:"expired_at"` 115 } 116 117 v4QueryServiceHosts struct { 118 Domains []string `json:"domains"` 119 Old []string `json:"old"` 120 AccDomains []string `json:"acc_domains"` 121 } 122 123 v4QueryRegion struct { 124 RegionId string `json:"region"` 125 Ttl int64 `json:"ttl"` 126 Io v4QueryServiceHosts `json:"io"` 127 IoSrc v4QueryServiceHosts `json:"io_src"` 128 Up v4QueryServiceHosts `json:"up"` 129 Rs v4QueryServiceHosts `json:"rs"` 130 Rsf v4QueryServiceHosts `json:"rsf"` 131 Api v4QueryServiceHosts `json:"api"` 132 Uc v4QueryServiceHosts `json:"uc"` 133 } 134 135 v4QueryResponse struct { 136 Hosts []v4QueryRegion `json:"hosts"` 137 } 138 ) 139 140 const bucketRegionsQueryCacheFileName = "query_v4_01.cache.json" 141 142 var ( 143 persistentCaches map[uint64]*cache.Cache 144 persistentCachesLock sync.Mutex 145 ) 146 147 // NewBucketRegionsQuery 创建空间区域查询器 148 func NewBucketRegionsQuery(bucketHosts Endpoints, opts *BucketRegionsQueryOptions) (BucketRegionsQuery, error) { 149 if opts == nil { 150 opts = &BucketRegionsQueryOptions{} 151 } 152 retryMax := opts.RetryMax 153 if retryMax <= 0 { 154 retryMax = 2 155 } 156 compactInterval := opts.CompactInterval 157 if compactInterval == time.Duration(0) { 158 compactInterval = time.Minute 159 } 160 persistentFilePath := opts.PersistentFilePath 161 if persistentFilePath == "" { 162 persistentFilePath = filepath.Join(os.TempDir(), "qiniu-golang-sdk", bucketRegionsQueryCacheFileName) 163 } 164 persistentDuration := opts.PersistentDuration 165 if persistentDuration == time.Duration(0) { 166 persistentDuration = time.Minute 167 } 168 169 persistentCache, err := getPersistentCache(persistentFilePath, compactInterval, persistentDuration) 170 if err != nil { 171 return nil, err 172 } 173 174 return &bucketRegionsQuery{ 175 bucketHosts: bucketHosts, 176 cache: persistentCache, 177 client: makeBucketQueryClient( 178 opts.Client, nil, 179 bucketHosts, 180 !opts.UseInsecureProtocol, 181 retryMax, 182 opts.HostFreezeDuration, 183 opts.Resolver, 184 opts.Chooser, 185 opts.Backoff, 186 opts.BeforeResolve, 187 opts.AfterResolve, 188 opts.ResolveError, 189 opts.BeforeBackoff, 190 opts.AfterBackoff, 191 opts.BeforeRequest, 192 opts.AfterResponse, 193 nil, nil, nil, 194 ), 195 useHttps: !opts.UseInsecureProtocol, 196 accelerateUploading: opts.AccelerateUploading, 197 }, nil 198 } 199 200 func getPersistentCache(persistentFilePath string, compactInterval, persistentDuration time.Duration) (*cache.Cache, error) { 201 var ( 202 persistentCache *cache.Cache 203 ok bool 204 err error 205 ) 206 207 crc64Value := calcPersistentCacheCrc64(persistentFilePath, compactInterval, persistentDuration) 208 persistentCachesLock.Lock() 209 defer persistentCachesLock.Unlock() 210 211 if persistentCaches == nil { 212 persistentCaches = make(map[uint64]*cache.Cache) 213 } 214 if persistentCache, ok = persistentCaches[crc64Value]; !ok { 215 persistentCache, err = cache.NewPersistentCache( 216 reflect.TypeOf(&v4QueryCacheValue{}), 217 persistentFilePath, 218 compactInterval, 219 persistentDuration, 220 func(err error) { 221 log.Warn(fmt.Sprintf("BucketRegionsQuery persist error: %s", err)) 222 }) 223 if err != nil { 224 return nil, err 225 } 226 persistentCaches[crc64Value] = persistentCache 227 } 228 return persistentCache, nil 229 } 230 231 // Query 查询空间区域,返回 region.RegionsProvider 232 func (query *bucketRegionsQuery) Query(accessKey, bucketName string) RegionsProvider { 233 return &bucketRegionsProvider{ 234 accessKey: accessKey, 235 bucketName: bucketName, 236 query: query, 237 cacheKey: makeRegionCacheKey(accessKey, bucketName, query.accelerateUploading, query.bucketHosts), 238 accelerateUploading: query.accelerateUploading, 239 } 240 } 241 242 func (provider *bucketRegionsProvider) GetRegions(ctx context.Context) ([]*Region, error) { 243 var err error 244 cacheValue, status := provider.query.cache.Get(provider.cacheKey, func() (cache.CacheValue, error) { 245 var ret v4QueryResponse 246 url := fmt.Sprintf("%s/v4/query?ak=%s&bucket=%s", provider.query.bucketHosts.firstUrl(provider.query.useHttps), provider.accessKey, provider.bucketName) 247 if err = clientv2.DoAndDecodeJsonResponse(provider.query.client, clientv2.RequestParams{ 248 Context: ctx, 249 Method: clientv2.RequestMethodGet, 250 Url: url, 251 BufferResponse: true, 252 }, &ret); err != nil { 253 return nil, err 254 } 255 return ret.toCacheValue(provider.accelerateUploading), nil 256 }) 257 if status == cache.NoResultGot { 258 return nil, err 259 } 260 return cacheValue.(*v4QueryCacheValue).Regions, nil 261 } 262 263 func (left *v4QueryCacheValue) IsEqual(rightValue cache.CacheValue) bool { 264 if right, ok := rightValue.(*v4QueryCacheValue); ok { 265 if len(left.Regions) != len(right.Regions) { 266 return false 267 } 268 for idx := range left.Regions { 269 if !left.Regions[idx].IsEqual(right.Regions[idx]) { 270 return false 271 } 272 } 273 return true 274 } 275 return false 276 } 277 278 func (left *v4QueryCacheValue) IsValid() bool { 279 return time.Now().Before(left.ExpiredAt) 280 } 281 282 func (left *v4QueryCacheValue) ShouldRefresh() bool { 283 return time.Now().After(left.RefreshAfter) 284 } 285 286 func (response *v4QueryResponse) toCacheValue(accelerateUploading bool) *v4QueryCacheValue { 287 var ( 288 minTtl = int64(math.MaxInt64) 289 regions = make([]*Region, 0, len(response.Hosts)) 290 ) 291 for _, host := range response.Hosts { 292 regions = append(regions, host.toCacheValue(accelerateUploading)) 293 if host.Ttl < minTtl { 294 minTtl = host.Ttl 295 } 296 } 297 now := time.Now() 298 return &v4QueryCacheValue{ 299 Regions: regions, 300 RefreshAfter: now.Add(time.Duration(minTtl) * time.Second / 2), 301 ExpiredAt: now.Add(time.Duration(minTtl) * time.Second), 302 } 303 } 304 305 func (response *v4QueryRegion) toCacheValue(accelerateUploading bool) *Region { 306 region := Region{ 307 RegionID: response.RegionId, 308 Up: response.Up.toCacheValue(), 309 Io: response.Io.toCacheValue(), 310 IoSrc: response.IoSrc.toCacheValue(), 311 Rs: response.Rs.toCacheValue(), 312 Rsf: response.Rsf.toCacheValue(), 313 Api: response.Api.toCacheValue(), 314 Bucket: response.Uc.toCacheValue(), 315 } 316 if !accelerateUploading { 317 region.Up.Accelerated = nil 318 } 319 320 return ®ion 321 } 322 323 func (response *v4QueryServiceHosts) toCacheValue() Endpoints { 324 return Endpoints{ 325 Accelerated: response.AccDomains, 326 Preferred: response.Domains, 327 Alternative: response.Old, 328 } 329 } 330 331 func makeRegionCacheKey(accessKey, bucketName string, accelerateUploading bool, bucketHosts Endpoints) string { 332 enableAcceleration := uint8(0) 333 if accelerateUploading { 334 enableAcceleration = 1 335 } 336 return fmt.Sprintf("%s:%s:%d:%s", accessKey, bucketName, enableAcceleration, makeBucketHostsCacheKey(bucketHosts)) 337 } 338 339 func makeBucketHostsCacheKey(serviceHosts Endpoints) string { 340 return fmt.Sprintf("%s:%s:%s", makeHostsCacheKey(serviceHosts.Preferred), makeHostsCacheKey(serviceHosts.Alternative), makeHostsCacheKey(serviceHosts.Accelerated)) 341 } 342 343 func makeHostsCacheKey(hosts []string) string { 344 sortedHosts := append(make([]string, 0, len(hosts)), hosts...) 345 sort.StringSlice(sortedHosts).Sort() 346 return fmt.Sprintf("%x", md5.Sum([]byte(strings.Join(sortedHosts, ",")))) 347 } 348 349 func makeBucketQueryClient( 350 client clientv2.Client, 351 credentials credentials.CredentialsProvider, 352 bucketHosts Endpoints, 353 useHttps bool, 354 retryMax int, 355 hostFreezeDuration time.Duration, 356 r resolver.Resolver, 357 cs chooser.Chooser, 358 bf backoff.Backoff, 359 beforeResolve func(*http.Request), 360 afterResolve func(*http.Request, []net.IP), 361 resolveError func(*http.Request, error), 362 beforeBackoff func(*http.Request, *retrier.RetrierOptions, time.Duration), 363 afterBackoff func(*http.Request, *retrier.RetrierOptions, time.Duration), 364 beforeRequest func(*http.Request, *retrier.RetrierOptions), 365 afterResponse func(*http.Response, *retrier.RetrierOptions, error), 366 beforeSign, afterSign func(*http.Request), 367 signError func(*http.Request, error), 368 ) clientv2.Client { 369 is := []clientv2.Interceptor{ 370 clientv2.NewAntiHijackingInterceptor(), 371 clientv2.NewHostsRetryInterceptor(clientv2.HostsRetryConfig{ 372 RetryMax: bucketHosts.HostsLength(), 373 HostFreezeDuration: hostFreezeDuration, 374 HostProvider: hostprovider.NewWithHosts(bucketHosts.allUrls(useHttps)), 375 }), 376 clientv2.NewSimpleRetryInterceptor(clientv2.SimpleRetryConfig{ 377 RetryMax: retryMax, 378 Backoff: bf, 379 Resolver: r, 380 Chooser: cs, 381 BeforeResolve: beforeResolve, 382 AfterResolve: afterResolve, 383 ResolveError: resolveError, 384 BeforeBackoff: beforeBackoff, 385 AfterBackoff: afterBackoff, 386 BeforeRequest: beforeRequest, 387 AfterResponse: afterResponse, 388 }), 389 clientv2.NewBufferResponseInterceptor(), 390 } 391 if credentials != nil { 392 is = append(is, clientv2.NewAuthInterceptor(clientv2.AuthConfig{ 393 Credentials: credentials, 394 TokenType: auth.TokenQiniu, 395 BeforeSign: beforeSign, 396 AfterSign: afterSign, 397 SignError: signError, 398 })) 399 } 400 return clientv2.NewClient(client, is...) 401 } 402 403 func calcPersistentCacheCrc64(persistentFilePath string, compactInterval, persistentDuration time.Duration) uint64 { 404 bytes := make([]byte, 0, 1024) 405 bytes = strconv.AppendInt(bytes, int64(compactInterval), 36) 406 bytes = append(bytes, []byte(persistentFilePath)...) 407 bytes = append(bytes, byte(0)) 408 bytes = strconv.AppendInt(bytes, int64(persistentDuration), 36) 409 return crc64.Checksum(bytes, crc64.MakeTable(crc64.ISO)) 410 }