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 = 10 * time.Second DefSleepSecondWhenExceedLimit = 3 * time.Second DefRandSlice = 10 DefMaxRecoverableRetryCount = 3 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(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) if ok /*&& err.Timeout()*/ && recoverableErrorRetryCount <= config.MaxRecoverableRetryCount { // 只要是网络错误都重试 recoverableErrorRetryCount++ 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,暂时在这里处理饿百的访问异常 if 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 HTTP code is:%d, url:%v, request:%v, ioutil.ReadAll failed with error:%v", trackID, response.StatusCode, request.URL, getClonedData(request.URL, savedBuf), err) } return ErrHTTPCodeIsNot200 } var ( errLevel string bodyMap map[string]interface{} ) bodyData, err := ioutil.ReadAll(response.Body) if err != nil { baseapi.SugarLogger.Errorf("AccessPlatformAPIWithRetry:%s url:%v, request:%v failed with error:%v", trackID, request.URL, getClonedData(request.URL, savedBuf), err) } else if err = utils.TryUnmarshalUseNumber(bodyData, &bodyMap); err != nil { const maxOutputLen = 200 bodyDataLen := len(bodyData) bodyData2 := bodyData if bodyDataLen > maxOutputLen { bodyData2 = bodyData2[:maxOutputLen] } baseapi.SugarLogger.Infof("AccessPlatformAPIWithRetry:%s url:%v, request:%v failed with error:%v, bodyData:%s", trackID, request.URL, getClonedData(request.URL, savedBuf), err, string(bodyData2)) } if err != nil { errLevel = ErrLevelRecoverableErr // 读取数据错误,或数据格式错误认为是偶发情况,重试 } else { errLevel, err = handleResponse(bodyMap) } baseapi.SugarLogger.Debugf("AccessPlatformAPIWithRetry:%s url:%v, response:%s", trackID, request.URL, utils.Format4Output(bodyMap, true)) 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 }