====== Идемпотентность запросов ====== Можно ознакомится со статьей от Яндекс: [[https://habr.com/ru/company/yandex/blog/442762/]] ---- [[#Введение|Введение]]\\ [[#Обработка в агенте|Обработка в агенте]]\\ [[#Обработка на клиенте|Обработка на клиенте]]\\ [[#Пример МП Courier|Пример МП Courier]]\\ [[#Пример Бонусы Онлайн|Пример Бонусы Онлайн]]\\ ---- ===== Введение ===== Идемпотентность - это операция, которая при многократном вызове возвращает один и тот же результат.\\ Или метод HTTP является идемпотентным, если повторный идентичный запрос, сделанный один или несколько раз подряд, имеет один и тот же эффект, не изменяющий состояние сервера. ---- ===== Обработка в агенте ===== * Новые property: * Himstat.IdempotencyText - ключ идемпотентности, берется из заголовка запроса и устанавливается на прямую (доступно на чтение и запись); * Himstat.IdempotencyHash - хэш ключ идемпотентности, устанавливается при установке IdempotencyText (доступно только на чтение); * в модуле ErrorRes.pas новый тип Exception TIdenpotencyException. **Пример запроса:** procedure THimstatDM. ...(Srvr: TRtcDataServer); var ... begin ... try ... try Himstat := THimstat(GlobalConnectionPoolClass.LockFreeConnection(THimstat)); // Получаем IdempotencyText -> (IdempotencyHash) Himstat.IdempotencyText := Srvr.Request.Query.Text; Himstat.Do...(SessionID, s); // Выполняем команду finally GlobalConnectionPoolClass.UnlockConnection(TObject(Himstat)); end; ... except on E: TIdenpotencyException do begin ... s := Format('{"error": ' + IntToStr(ErrorIdenpotency) + ', "Msg": "%s"}', [AEncodeURL(Utf8Encode('Повторный запрос. Обновите приложение!'))]); ... end; on E: Exception do begin ... end; end; ... end; procedure THimstat.Do...(const nSessionID: string; out Str: string); var ... begin ... try ... QueryIdempotency; // Записываем в таблицу Hash и время (* *Нет таблицы IDEMPOTENCY_QUERY - выходим из проверки * - получаем старый ОТВЕТ (предыдущего АНАЛОГИЧНОГО запроса) ИЛИ получаем ОТВЕТ="" - Идет запись * - ОТВЕТ=null - пытаемся записать HASH * - ERROR Присваиваем ОТВЕТ:="" - Идет запись * - FINALY * - (1) ЕСЛИ ОТВЕТ=null - то у нас уникальный запрос записанный в таблицу * - (2) ЕСЛИ ОТВЕТ<>null - запрос не уникален - возбуждаем исключение TIdenpotencyException.Create(ОТВЕТ); *) // Выполняем действия ... UpdateIdempotency(Str); // сохраняем ОТВЕТ в таблицу except on E: TIdenpotencyException do begin Str := e.Message; if Trim(Str) = '' then raise TIdenpotencyException.Create('Нет ответа на запрос - повторяющийся запрос!'); (* *Если исключение содержит НЕ ПУСТОЕ сообщение то это ОТВЕТ и мы его отправляем повторно *Если исключение содержит ПУСТОЕ сообщение то снова возбуждаем TIdenpotencyException и отправляем как ошибку ОДНОВРЕМЕННОГО ЗАПРОСА (ErrorCode = 5) *) end; on E: Exception do begin ... DeleteIdempotency; // Если ОШИБКА произошла в теле обработки до сохранения ОТВЕТа то HASH запроса удаляется end; end; ... end; ---- ==== Клиент ==== - Организовать уникальность запроса; - Обработку код Error = 5. ---- ===== Обработка на клиенте ===== ===== Пример МП Courier ===== ... ... var PayIdempotency: string;//'Переменная сохраняет состояние текущей транзакции /запрос(dor_id, debet) + ответ(JSON)/' ... ... JSONAnswer := KassaFiscalRegistar.PrintFiscalCheck(JSONResultObj); //'Печать чека' ... except on E: Exception do begin PayIdempotency := ''; //'Если ошибка при печати - обнуляем текущую транзакцию' ... ... procedure TmoneyPayFramePanel.OnShow(curVisible: TuniFramePanel); begin PayIdempotency := '';// 'При входе обнуление предыдущей транзакции' ... ... ClnDM.GetFiscalCheck(sJSON, procedure (AJObject: TJSONObject) var CurrentPayIdempotency: string; //'Текущая транзакция' begin CurrentPayIdempotency := THashMD5.GetHashString(sJSON + '=' + AJObject.ToString); if PayIdempotency = CurrentPayIdempotency then //'Текущую сравниваем с запомненой' begin // 'Ничего не делаем если транзакция все еще ТА ЖЕ' WriteLog("TmoneyPayFramePanel.WorkOfPay PayIdempotency = CurrentPayIdempotency Abort"); end else begin //'Транзакция другая - выпонить оплату' PayIdempotency := CurrentPayIdempotency; AnswerFromWeb('GetFiscalCheck', '', 0, AJObject); end; end); ... ... ---- ===== Пример Бонусы Онлайн ===== function BonusOnlineSend(const data: string; out j: TJsonObject): Boolean; var ... Idempotency_GUID: string; // Переменная сохраняет состояние текущей транзакции ReconIdempotencyCount: integer; json: TJsonValue; JSON_O: TJsonObject; begin Result := False; Idempotency_GUID := ''; // При входе обнуление предыдущей транзакции try CreateIdempotency_GUID(Idempotency_GUID); // Текущая транзакция ReconIdempotencyCount := 0; json := nil; while True do begin if not BonusPaySend(data, ApiPath, hmPOST, res, Idempotency_GUID) then begin WriteLog('Не получили данные о бонусе'); raise Exception.Create('Бонусы онлайн. Получить бонусы невозможно!' + #13#10 + 'Отсутствует связь с сервером!'); end; ... codeApi := JSON_O.Get('error').JsonValue.Value; ... else if codeApi = '5' then // Действия при 5 коде begin if ReconIdempotencyCount < 2 then // Проверяем количество провторов begin WriteLog('errorResult = 5, повторный запрос'); Sleep(500); inc(ReconIdempotencyCount); Continue; // Пробуем еще раз end else begin ReconIdempotencyCount := 0; // Обнулим счетчик и сообщим об ошибке WriteLog(_UTFDecode(JSON_O.Get('Msg').JsonValue.Value)); raise Exception.Create('Бонусы онлайн. Получить бонусы невозможно!' + #13#10 + _UTFDecode(JSON_O.Get('Msg').JsonValue.Value)); end; end else ... Break; end; ... Result := True; finally if Assigned(json) then FreeAndNil(json); Idempotency_GUID := ''; end; end; ... function BonusPaySend(const data, URL: string; Method: THttpMethod; out json: string; Idempotency_GUID: string = ''): Boolean; var i: Integer; http: T_Response; Headers: TStringList; url_data, ContentEncoding: string; begin Result := False; i := 0; while (i < 2) and not Result do begin Headers := TStringList.Create; try if Idempotency_GUID <> '' then // идентификатор запроса Headers.Add('XIdempotency-GUID: ' + Idempotency_GUID); // Передаем в запросе Headers.Add('Accept-Encoding: deflate'); Headers.Add('Content-Encoding: deflate'); Headers.Add('Content-Type: application/json'); ... finally if Assigned(Headers) then FreeAndNil(Headers); inc(i); end; end; end; ----