Ядро веб-API ASP.NET: обработка проблем с клиентскими подключениями и поиск конфликтов - PullRequest
0 голосов
/ 17 октября 2018

Фон

У меня есть сервер веб-API (asp.net core v2.1), который выполняет некоторые основные операции, например, управление объектами на сервере.Это интерфейс:

[HttpPost]
[Route("create")]
public async Task<ActionResult<NewEntityResponse>> Create(CreateEntityModel model)
{
    // 1) Validate the request.
    // 2) Create a new row on the database
    // 3) Return the new entity in response.
}

Пользователь, выполняющий этот метод REST следующим образом:

POST https://example.com/create

Content-Type: application/json

{
    "firstName": "Michael", 
    "lastName": "Jorden"
}

И получает ответ, подобный этому:

Status 200

{
    "id": "123456" // The newly created entity id
}

Проблема

При отправке тысяч таких запросов в определенный момент произойдет сбой из-за сетевых подключений.При сбое соединения это может привести нас к двум различным ситуациям:

  • Сетевой вызов был завершен на пути к серверу - в этом случае сервер не знает об этом запросе.Следовательно, сущность не была создана.Пользователь просто должен отправить то же сообщение еще раз.
  • Сетевой вызов был отправлен с сервера обратно клиенту, но никогда не насыщал адресата - в этом случае запрос был выполнен полностью, но клиент неЗнаю об этом.Ожидаемое решение - отправить тот же запрос еще раз.В этом случае он создаст одну и ту же сущность дважды - и это проблема.

Запрошенное решение

Я хочу создать общее решение дляweb-api, тот «remmeber», который командует, это уже сделано.если он получил один и тот же запрос дважды, он возвращает код состояния HTTP Conflict.

Где я дошел до этого

Я подумал добавить клиенту опцию добавления уникального идентификатора в запрос,таким образом:

POST https://example.com/create?call-id=XXX

Добавьте на мой сервер новый фильтр, который проверяет, выполняется ли ключ XXX.Если да, верните Conflict.В противном случае - продолжить.

Добавить еще один серверный фильтр, который проверяет ответ метода и помечает его как «завершенный» для дальнейших проверок.

Проблема с этим решением для вызовов параллелизма.Если мой метод требует 5 секунд для возврата, и клиент снова отправил то же сообщение через 1 секунду - он создаст две сущности с одинаковыми данными.

Вопросы:

  1. Считаете ли вы, что это хороший подход для решения этой проблемы?
  2. Вы знакомы с готовыми к использованию решениями, которые делают это?
  3. Как решить мою проблему "параллелизма"?
  4. Любые другие советы будут отличными!

спасибо.

Ответы [ 2 ]

0 голосов
/ 18 октября 2018

Разве это не самое простое решение совершить действие REST? 1001 * идемпотент ?

Я имею в виду: вызов должен проверить, существует ли ресурс, и либо создать новый ресурс, если он не существует, либо вернуть существующий, если он есть?

0 голосов
/ 18 октября 2018

ОК, я просто придумаю, как все сделать правильно.Итак, я реализовал это сам и поделился с вами.

Чтобы синхронизировать все запросы между разными серверами, я использовал Redis в качестве службы кэширования.Если у вас только один сервер, вы можете использовать Dictionary<string, string>.

Этот фильтр выполняет:

  1. Перед обработкой запроса - добавьте новый пустой ключ значения в Redis.
  2. После обработки запроса сервером - сохранить ответ сервера в Redis.Эти данные будут использоваться, когда пользователь снова запросит тот же запрос.

    открытый класс ConflictsFilter: ActionFilterAttribute {const string CONFLICT_KEY_NAME = "конфликта-проверка";статическое чтение только TimeSpan EXPIRE_AFTER = TimeSpan.FromMinutes (30);

    private static bool ShouldCheck(ActionDescriptor actionDescription, IQueryCollection queries)
    {
        return queries.ContainsKey(CONFLICT_KEY_NAME);
    }
    
    private string BuildKey(string uid, string requestId)
    {
        return $"{uid}_{requestId}";
    }
    
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (ShouldCheck(context.ActionDescriptor, context.HttpContext.Request.Query))
        {
            using (var client = RedisConnectionPool.ConnectionPool.GetClient())
            {
                string key = BuildKey(context.HttpContext.User.GetId(), context.HttpContext.Request.Query[CONFLICT_KEY_NAME]);
                string existing = client.Get<string>(key);
                if (existing != null)
                {
                    var conflict = new ContentResult();
                    conflict.Content = existing;
                    conflict.ContentType = "application/json";
                    conflict.StatusCode = 409;
    
                    context.Result = conflict;
                    return;
                }
                else
                {
                    client.Set(key, string.Empty, EXPIRE_AFTER);
                }
            }
        }
    
        base.OnActionExecuting(context);
    }
    
    public override void OnResultExecuted(ResultExecutedContext context)
    {
        base.OnResultExecuted(context);
        if (ShouldCheck(context.ActionDescriptor, context.HttpContext.Request.Query) && context.HttpContext.Response.StatusCode == 200)
        {
            string key = BuildKey(context.HttpContext.User.GetId(), context.HttpContext.Request.Query[CONFLICT_KEY_NAME]);
            using (var client = RedisConnectionPool.ConnectionPool.GetClient())
            {
                var responseBody = string.Empty;
                if (context.Result is ObjectResult)
                {
                    ObjectResult result = context.Result as ObjectResult;
                    responseBody = JsonConvert.SerializeObject(result.Value);
                }
    
                if (responseBody != string.Empty)
                    client.Set(key, responseBody, EXPIRE_AFTER);
            }
        }
    }
    

    }

Код выполняется, только если существует запрос ?conflict-checker=XXX.

Этот код предоставляется вам по лицензии MIT.

Наслаждайтесь поездкой:)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...