====== Идемпотентность запросов ======
Можно ознакомится со статьей от Яндекс: [[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;
----