package platformapi import ( "bytes" "compress/gzip" "errors" "fmt" "git.rosy.net.cn/jx-callback/globals" "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("%d,config:%d: err %s", recoverableErrorRetryCount, config.MaxRecoverableRetryCount, 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 } } bodyData, err := ioutil.ReadAll(response.Body) if 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) } if bodyData != nil { return errors.New(string(bodyData)) } 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) } globals.SugarLogger.Debugf("%s", utils.Format4Output(time.Now(), false)) 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 }