201 lines
7.0 KiB
Go
201 lines
7.0 KiB
Go
package platformapi
|
||
|
||
import (
|
||
"bytes"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"io/ioutil"
|
||
"net"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
|
||
"git.rosy.net.cn/baseapi"
|
||
"git.rosy.net.cn/baseapi/utils"
|
||
)
|
||
|
||
const (
|
||
DefClientTimeout = 15 * time.Second
|
||
DefSleepSecondWhenExceedLimit = 3 * time.Second
|
||
DefMaxRecoverableRetryCount = 1
|
||
DefMaxExceedLimitRetryCount = 25
|
||
)
|
||
|
||
type APIRetryConfig struct {
|
||
MaxExceedLimitRetryCount int
|
||
MaxRecoverableRetryCount int
|
||
SleepSecondWhenExceedLimit time.Duration
|
||
}
|
||
|
||
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,
|
||
SleepSecondWhenExceedLimit: DefSleepSecondWhenExceedLimit,
|
||
},
|
||
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 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()
|
||
trackID := utils.GetUUID()
|
||
baseapi.SugarLogger.Debugf("begin AccessPlatformAPIWithRetry:%s do:%s url:%v", trackID, request.Method, request.URL)
|
||
response, err := client.Do(request)
|
||
baseapi.SugarLogger.Debugf("end AccessPlatformAPIWithRetry:%s do url:%v, request:%s", trackID, request.URL, getClonedData(request.URL, savedBuf))
|
||
if err != nil {
|
||
baseapi.SugarLogger.Debugf("AccessPlatformAPIWithRetry:%s client.Get return err:%v", trackID, 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", trackID, 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", trackID, 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", trackID, 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", trackID, response.StatusCode, request.URL, getClonedData(request.URL, savedBuf), err)
|
||
}
|
||
return ErrHTTPCodeIsNot200
|
||
}
|
||
var (
|
||
errLevel string
|
||
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", trackID, request.URL, getClonedData(request.URL, savedBuf), err)
|
||
} else if err = utils.TryUnmarshalUseNumber(bodyData, &bodyMap); err != nil {
|
||
parseJSONErr = err
|
||
err = nil // 尝试忽略解析成json错
|
||
} else {
|
||
baseapi.SugarLogger.Debugf("AccessPlatformAPIWithRetry:%s url:%v, response:%s", trackID, request.URL, utils.Format4Output(bodyMap, true))
|
||
}
|
||
errLevel, err = handleResponse(response, string(bodyData), bodyMap)
|
||
|
||
if err != nil {
|
||
errLevel = ErrLevelRecoverableErr // 读取数据错误,或数据格式错误认为是偶发情况,重试
|
||
} else {
|
||
if errLevel, err = handleResponse(response, string(bodyData), bodyMap); err != nil && parseJSONErr != nil {
|
||
const maxOutputLen = 200
|
||
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", trackID, request.URL, getClonedData(request.URL, savedBuf), parseJSONErr, string(bodyData2))
|
||
}
|
||
}
|
||
|
||
if err == nil {
|
||
return nil
|
||
} else if errLevel == ErrLevelExceedLimit {
|
||
exceedLimitRetryCount++
|
||
if exceedLimitRetryCount <= config.MaxExceedLimitRetryCount {
|
||
time.Sleep(config.SleepSecondWhenExceedLimit)
|
||
continue
|
||
}
|
||
} else if errLevel == ErrLevelRecoverableErr {
|
||
recoverableErrorRetryCount++
|
||
if recoverableErrorRetryCount <= config.MaxRecoverableRetryCount {
|
||
continue
|
||
}
|
||
}
|
||
baseapi.SugarLogger.Infof("AccessPlatformAPIWithRetry:%s failed, url:%v, response:%s, error:%v", trackID, 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 {
|
||
appendErrList := []string{}
|
||
for _, key := range watchKeys {
|
||
if bzParams[key] != nil {
|
||
appendErrList = append(appendErrList, fmt.Sprintf("[%s:%v]", key, bzParams[key]))
|
||
}
|
||
}
|
||
if len(appendErrList) > 0 {
|
||
inErr = utils.NewErrorCode(strings.Join(appendErrList, ",")+", "+codeErr.ErrMsg(), codeErr.Code())
|
||
}
|
||
}
|
||
}
|
||
return inErr
|
||
}
|