Идемпотентность запросов

Введение

Идемпотентность - это операция, которая при многократном вызове возвращает один и тот же результат.
Или метод HTTP является идемпотентным, если повторный идентичный запрос, сделанный один или несколько раз подряд, имеет один и тот же эффект, не изменяющий состояние сервера.


Обработка в агенте

  • Новые property:
    • Himstat.IdempotencyText - ключ идемпотентности, берется из заголовка запроса и устанавливается на прямую (доступно на чтение и запись);
    • Himstat.IdempotencyHash - хэш ключ идемпотентности, устанавливается при установке IdempotencyText (доступно только на чтение);
  • в модуле <font color=«blue» size=«+1»>ErrorRes.pas</font> новый тип 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;


Клиент

  1. Организовать уникальность запроса;
  2. Обработку код 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;