Files
baseapi/platformapi/platformapi.go

221 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package platformapi
import (
"bytes"
"errors"
"fmt"
"io"
"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 = 20 * 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, r *bytes.Buffer) string {
if strings.Index(requestURL.String(), "uploadImg") >= 0 {
return "binary content"
}
retVal := string(r.Bytes())
if len(retVal) > maxDataSizeDontOutput {
return "request data is too large"
}
return retVal
}
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 {
savedBuf := new(bytes.Buffer)
request := handleRequest()
if request.Body != nil {
request.Body = ioutil.NopCloser(io.TeeReader(request.Body, savedBuf))
}
beginTime := time.Now()
trackInfo := request.Header.Get(KeyTrackInfo)
if trackInfo != "" {
request.Header.Del(KeyTrackInfo)
}
trackInfo += ", " + utils.GetUUID()
baseapi.SugarLogger.Debugf("begin AccessPlatformAPIWithRetry:%s do:%s url:%v", trackInfo, request.Method, request.URL)
response, err := client.Do(request)
baseapi.SugarLogger.Debugf("end AccessPlatformAPIWithRetry:%s do url:%v, request:%s", trackInfo, request.URL, getClonedData(request.URL, savedBuf))
if err != nil {
baseapi.SugarLogger.Debugf("AccessPlatformAPIWithRetry:%s client.Get return err:%v", trackInfo, err)
err, ok := err.(net.Error)
recoverableErrorRetryCount++
if ok /*&& err.Timeout()*/ && recoverableErrorRetryCount <= config.MaxRecoverableRetryCount { // 只要是网络错误都重试
continue
} else {
baseapi.SugarLogger.Errorf("AccessPlatformAPIWithRetry:%s access api url:%v, request:%v, error:%v", trackInfo, request.URL, getClonedData(request.URL, savedBuf), err)
return ErrAPIAccessFailed
}
}
usedMilliSecond := time.Now().Sub(beginTime) / time.Millisecond
if usedMilliSecond > 5000 {
baseapi.SugarLogger.Infof("AccessPlatformAPIWithRetry:%s access api too slow, url:%v, request:%v, usedMilliSecond:%d", trackInfo, request.URL, getClonedData(request.URL, savedBuf), 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.Errorf("AccessPlatformAPIWithRetry:%s HTTP code is:%d, url:%v, request:%v, response:%s", trackInfo, response.StatusCode, request.URL, getClonedData(request.URL, savedBuf), string(bodyData))
} else {
baseapi.SugarLogger.Errorf("AccessPlatformAPIWithRetry:%s ioutil.ReadAll failed, HTTP code is:%d, url:%v, request:%v, error:%v", trackInfo, response.StatusCode, request.URL, getClonedData(request.URL, savedBuf), err)
}
return ErrHTTPCodeIsNot200
}
var (
errLevel string
bodyGeneral interface{}
bodyMap map[string]interface{}
parseJSONErr error
)
bodyData, err := ioutil.ReadAll(response.Body)
if err != nil {
baseapi.SugarLogger.Errorf("AccessPlatformAPIWithRetry:%s ioutil.ReadAll failed, url:%v, request:%v, error:%v", trackInfo, request.URL, getClonedData(request.URL, savedBuf), err)
errLevel = ErrLevelRecoverableErr // 读取数据错误,或数据格式错误认为是偶发情况,重试
} else {
if err = utils.TryUnmarshalUseNumber(bodyData, &bodyGeneral); err != nil {
parseJSONErr = err
err = nil // 尝试忽略解析成json错
} else {
baseapi.SugarLogger.Debugf("AccessPlatformAPIWithRetry:%s url:%v, response:%s", trackInfo, request.URL, utils.Format4Output(bodyGeneral, true))
}
// 临时处理返回值居然不是map的情况
if bodyMap2, ok := bodyGeneral.(map[string]interface{}); ok {
bodyMap = bodyMap2
} else if bodyGeneral != nil {
bodyMap = map[string]interface{}{
KeyData: bodyGeneral,
}
}
errLevel, err = handleResponse(response, string(bodyData), bodyMap)
if err != nil && parseJSONErr != nil {
const maxOutputLen = 2000
bodyDataLen := len(bodyData)
bodyData2 := bodyData
if bodyDataLen > maxOutputLen {
bodyData2 = bodyData2[:maxOutputLen]
}
baseapi.SugarLogger.Infof("AccessPlatformAPIWithRetry:%s TryUnmarshalUseNumber failed, url:%v, request:%v, error:%v, bodyData:%s", trackInfo, request.URL, getClonedData(request.URL, savedBuf), parseJSONErr, string(bodyData2))
}
}
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
}
}
baseapi.SugarLogger.Infof("AccessPlatformAPIWithRetry:%s failed, url:%v, response:%s, error:%v", trackInfo, request.URL, utils.Format4Output(bodyMap, true), err)
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
}