236 lines
7.0 KiB
Go
236 lines
7.0 KiB
Go
package platformapi
|
||
|
||
import (
|
||
"bytes"
|
||
"compress/gzip"
|
||
"errors"
|
||
"fmt"
|
||
"io/ioutil"
|
||
"math"
|
||
"net"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
|
||
"git.rosy.net.cn/baseapi"
|
||
"git.rosy.net.cn/baseapi/utils"
|
||
)
|
||
|
||
const (
|
||
DefClientTimeout = 30 * time.Second
|
||
DefMaxSleepSecondWhenExceedLimit = 62 // 超频类错误最大重试间隙(秒)
|
||
DefMaxExceedLimitRetryCount = 25 // 超频类错误最大重试次数
|
||
DefMaxRecoverableRetryCount = 1 // 可恢复类错误(一般指网络错),最大重试次数
|
||
|
||
KeyTrackInfo = "TrackInfo"
|
||
KeyData = "fakeData"
|
||
)
|
||
|
||
type APIRetryConfig struct {
|
||
MaxExceedLimitRetryCount int
|
||
MaxRecoverableRetryCount int
|
||
MaxSleepSecondWhenExceedLimit int
|
||
}
|
||
|
||
type APIConfig struct {
|
||
APIRetryConfig
|
||
ClientTimeout time.Duration
|
||
}
|
||
|
||
type AccessPlatformAPIWithRetryParam struct {
|
||
APIRetryConfig
|
||
Client *http.Client
|
||
Request *http.Request
|
||
}
|
||
|
||
var (
|
||
DefAPIConfig = APIConfig{
|
||
APIRetryConfig: APIRetryConfig{
|
||
MaxExceedLimitRetryCount: DefMaxExceedLimitRetryCount,
|
||
MaxRecoverableRetryCount: DefMaxRecoverableRetryCount,
|
||
MaxSleepSecondWhenExceedLimit: DefMaxSleepSecondWhenExceedLimit,
|
||
},
|
||
ClientTimeout: DefClientTimeout,
|
||
}
|
||
)
|
||
|
||
const (
|
||
ErrLevelSuccess = "JXC4_SUCCESS"
|
||
ErrLevelExceedLimit = "JXC4_EXCEED_LIMIT"
|
||
ErrLevelRecoverableErr = "JXC4_RECOVERABLE"
|
||
ErrLevelGeneralFail = "JXC4_GENERAL_FAIL"
|
||
ErrLevelCodeIsNotOK = "JXC4_CODE_IS_NOT_OK"
|
||
)
|
||
|
||
const (
|
||
maxDataSizeDontOutput = 200 * 1024
|
||
)
|
||
|
||
// common api access error
|
||
var (
|
||
ErrAPIAccessFailed = errors.New("access API failed")
|
||
ErrHTTPCodeIsNot200 = errors.New("HTTP code is not 200")
|
||
ErrResponseDataFormatWrong = errors.New("the data of response has wrong format")
|
||
)
|
||
|
||
// common callback response
|
||
var (
|
||
ErrStrUnescapeError = "can not unescape data:%v, error:%v"
|
||
ErrStrUnmarshalError = "can not unmarshal data:%v, error:%v"
|
||
ErrStrCallbackSignatureIsWrong = "wrong signature"
|
||
)
|
||
|
||
func getClonedData(requestURL *url.URL, data []byte) string {
|
||
if strings.Index(requestURL.String(), "uploadImg") >= 0 {
|
||
return "binary content"
|
||
}
|
||
if len(data) > maxDataSizeDontOutput {
|
||
return "request data is too large"
|
||
}
|
||
return string(data)
|
||
}
|
||
|
||
func NewDefAPIConfig() (conf *APIConfig) {
|
||
obj := DefAPIConfig
|
||
return &obj
|
||
}
|
||
|
||
func AccessPlatformAPIWithRetry(client *http.Client, handleRequest func() *http.Request, config *APIConfig, handleResponse func(response *http.Response, bodyStr string, bodyMap map[string]interface{}) (string, error)) error {
|
||
exceedLimitRetryCount := 0
|
||
recoverableErrorRetryCount := 0
|
||
for {
|
||
var savedBuf []byte
|
||
request := handleRequest()
|
||
if request.Body != nil {
|
||
savedBuf2, err2 := ioutil.ReadAll(request.Body)
|
||
if err2 == nil {
|
||
savedBuf = savedBuf2
|
||
request.Body = ioutil.NopCloser(bytes.NewReader(savedBuf))
|
||
} else {
|
||
baseapi.SugarLogger.Warnf("AccessPlatformAPIWithRetry failed with err:%v", err2)
|
||
}
|
||
}
|
||
beginTime := time.Now()
|
||
trackInfo := request.Header.Get(KeyTrackInfo)
|
||
if trackInfo != "" {
|
||
request.Header.Del(KeyTrackInfo)
|
||
}
|
||
trackInfo += ", " + utils.GetUUID()
|
||
response, err := client.Do(request)
|
||
usedMilliSecond := time.Now().Sub(beginTime) / time.Millisecond
|
||
if err != nil {
|
||
err, ok := err.(net.Error)
|
||
baseapi.SugarLogger.Infof("err %s", err)
|
||
recoverableErrorRetryCount++
|
||
if ok /*&& err.Timeout()*/ && recoverableErrorRetryCount <= config.MaxRecoverableRetryCount { // 只要是网络错误都重试
|
||
continue
|
||
} else {
|
||
return ErrAPIAccessFailed
|
||
}
|
||
}
|
||
if usedMilliSecond > 5000 {
|
||
baseapi.SugarLogger.Infof("AccessPlatformAPIWithRetry:%s access api too slow, url:%v, usedMilliSecond:%d", trackInfo, request.URL, usedMilliSecond)
|
||
}
|
||
defer response.Body.Close()
|
||
if response.StatusCode != http.StatusOK {
|
||
// todo, hardcode,各平台都出现过偶发的返回非200的错误,饿百最多,美团外卖也出过,奇怪
|
||
if true /*response.StatusCode == http.StatusBadRequest && request.URL.Hostname() == "api-be.ele.me"*/ {
|
||
recoverableErrorRetryCount++
|
||
if recoverableErrorRetryCount <= config.MaxRecoverableRetryCount {
|
||
continue
|
||
}
|
||
}
|
||
if bodyData, err := ioutil.ReadAll(response.Body); err == nil {
|
||
baseapi.SugarLogger.Debugf("AccessPlatformAPIWithRetry:%s HTTP code is:%d, url:%v, response:%s", trackInfo, response.StatusCode, request.URL, string(bodyData))
|
||
} else {
|
||
baseapi.SugarLogger.Debugf("AccessPlatformAPIWithRetry:%s ioutil.ReadAll failed, HTTP code is:%d, url:%v, error:%v", trackInfo, response.StatusCode, request.URL, err)
|
||
}
|
||
return ErrHTTPCodeIsNot200
|
||
}
|
||
var (
|
||
errLevel string
|
||
bodyGeneral interface{}
|
||
bodyMap map[string]interface{}
|
||
parseJSONErr error
|
||
)
|
||
|
||
var bodyData []byte
|
||
var getDataErr error
|
||
//解析返回的gzip数据信息
|
||
if response.Header.Get("Content-Encoding") == "gzip" {
|
||
reader, e := gzip.NewReader(response.Body)
|
||
if e != nil {
|
||
fmt.Printf("create gzip reader err: %s \n\n", e.Error())
|
||
return e
|
||
}
|
||
bodyData, getDataErr = ioutil.ReadAll(reader)
|
||
} else {
|
||
bodyData, getDataErr = ioutil.ReadAll(response.Body)
|
||
}
|
||
|
||
if getDataErr != nil {
|
||
baseapi.SugarLogger.Errorf("AccessPlatformAPIWithRetry:%s ioutil.ReadAll failed, url:%v, error:%v", trackInfo, request.URL, err)
|
||
errLevel = ErrLevelRecoverableErr // 读取数据错误,或数据格式错误认为是偶发情况,重试
|
||
} else {
|
||
if err = utils.TryUnmarshalUseNumber(bodyData, &bodyGeneral); err != nil {
|
||
parseJSONErr = err
|
||
err = nil // 尝试忽略解析成json错
|
||
}
|
||
// 临时处理返回值居然不是map的情况
|
||
if bodyMap2, ok := bodyGeneral.(map[string]interface{}); ok {
|
||
bodyMap = bodyMap2
|
||
} else {
|
||
if bodyGeneral == nil {
|
||
bodyGeneral = string(bodyData)
|
||
}
|
||
bodyMap = map[string]interface{}{
|
||
KeyData: bodyGeneral,
|
||
}
|
||
}
|
||
errLevel, err = handleResponse(response, string(bodyData), bodyMap)
|
||
if err != nil && errLevel == ErrLevelRecoverableErr && parseJSONErr != nil {
|
||
const maxOutputLen = 2000
|
||
bodyDataLen := len(bodyData)
|
||
bodyData2 := bodyData
|
||
if bodyDataLen > maxOutputLen {
|
||
bodyData2 = bodyData2[:maxOutputLen]
|
||
}
|
||
}
|
||
}
|
||
|
||
if err == nil {
|
||
return nil
|
||
} else if errLevel == ErrLevelExceedLimit {
|
||
exceedLimitRetryCount++
|
||
if exceedLimitRetryCount <= config.MaxExceedLimitRetryCount {
|
||
sleepSeconds := int(math.Exp2(float64(exceedLimitRetryCount)))
|
||
if sleepSeconds > config.MaxSleepSecondWhenExceedLimit {
|
||
sleepSeconds = config.MaxSleepSecondWhenExceedLimit
|
||
}
|
||
time.Sleep(time.Duration(sleepSeconds) * time.Second)
|
||
continue
|
||
}
|
||
} else if errLevel == ErrLevelRecoverableErr {
|
||
recoverableErrorRetryCount++
|
||
if recoverableErrorRetryCount <= config.MaxRecoverableRetryCount {
|
||
continue
|
||
}
|
||
}
|
||
return err
|
||
}
|
||
}
|
||
|
||
func RebuildError(inErr error, bzParams map[string]interface{}, watchKeys []string) (outErr error) {
|
||
if inErr != nil {
|
||
if codeErr, ok := inErr.(*utils.ErrorWithCode); ok {
|
||
for _, key := range watchKeys {
|
||
if bzParams[key] != nil {
|
||
codeErr.AddPrefixMsg(fmt.Sprintf("[%s:%v]", key, bzParams[key]))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return inErr
|
||
}
|