Хочу рассказать, как мы организовали фоновое обновление данных во время запроса к REST-сервису.
Задача следующая: система хранит данные о пользователях. Cервис работает изолированно и не имеет прямого доступа к базам с этими данными. Для работы сервису необходимо иметь в своей внутренней базе имена и фамилии пользователей. Их можно получить из Identity текущего пользователя во время запроса. Требуется добавлять или обновлять имена во время каждого запроса. Желательно осуществлять это в отдельном потоке, чтобы эта работа не влияла на время выполнения основного запроса.
В базе сервиса мы храним имена и фамилии пользователей. Они нужны клиентам для информации о том, кто создал или модифицировал ресурс.
Эти данные не являются системно значимыми: если вдруг в базе отсутствуют нужные записи, ничего страшного не произойдёт. Поэтому мы не хотим регистрировать нашу фоновую работу в ASP при помощи QueueBackgroundWorkItem, чтобы затруднить перегрузку домена приложения.
Желательно решить задачу как можно проще.
Тем, кто хочет узнать о фоновых задачах в ASP.NET больше, советую почитать хорошую статью об этом.
У нас есть класс DbRefresher, осуществляющий добавление или изменение данных пользователя в методе RefreshAsync.
Наши контроллеры использует атрибут Authorize. Добавим своего наследника этого класса и переопределим метод OnAuthorization:
DbRefresher.RefreshAsync – это асинхронный метод, возвращающий объект Task, который продолжит своё выполнение в другом потоке. Выход из метода OnAuthorize осуществляется немедленно без ожидания завершения задачи. В случае аварийного завершения в лог будет добавлено сообщение об ошибке.
Вот и всё: остаётся только заменить в контроллерах атрибут Authorize на имя нашего нового атрибута. Новый атрибут возвращает управление сразу же после проверки прав текущего пользователя, после чего контроллер начинает свою работу. Обновление базы будет осуществляться параллельно с подготовкой ответа контроллером.
Тест, запускающий несколько одновременных запросов к сервису, поможет определить возможные проблемы:
Если для работы каких-то action-методов контроллера требуется, чтобы база заведомо содержала данные текущего пользователя, этот подход не годится. Придётся использовать явный вызов DbRefresher.RefreshAsync в теле метода.
Возможны проблемы во время добавления в базу нового пользователя при нескольких одновременных запросах. Если происходит попытка добавить пользователя в таблицу с уже существующим ключом, следует перехватить исключение primary key violation и прекратить работу. Тогда всю работу по обновлению данных пользователя выполнит только один поток.
Этот подход успешно работает в одном из наших сервисов в Конфёрмите более года.
Мне он представляется простым и изящным. Было бы интересно узнать мнение сообщества.
Задача следующая: система хранит данные о пользователях. Cервис работает изолированно и не имеет прямого доступа к базам с этими данными. Для работы сервису необходимо иметь в своей внутренней базе имена и фамилии пользователей. Их можно получить из Identity текущего пользователя во время запроса. Требуется добавлять или обновлять имена во время каждого запроса. Желательно осуществлять это в отдельном потоке, чтобы эта работа не влияла на время выполнения основного запроса.
Уточнение задачи
В базе сервиса мы храним имена и фамилии пользователей. Они нужны клиентам для информации о том, кто создал или модифицировал ресурс.
Эти данные не являются системно значимыми: если вдруг в базе отсутствуют нужные записи, ничего страшного не произойдёт. Поэтому мы не хотим регистрировать нашу фоновую работу в ASP при помощи QueueBackgroundWorkItem, чтобы затруднить перегрузку домена приложения.
Желательно решить задачу как можно проще.
Тем, кто хочет узнать о фоновых задачах в ASP.NET больше, советую почитать хорошую статью об этом.
Решение
У нас есть класс DbRefresher, осуществляющий добавление или изменение данных пользователя в методе RefreshAsync.
Наши контроллеры использует атрибут Authorize. Добавим своего наследника этого класса и переопределим метод OnAuthorization:
public override void OnAuthorization(HttpActionContext actionContext)
{
base.OnAuthorization(actionContext);
if (IsAuthorized(actionContext))
DbRefresher.RefreshAsync(actionContext.RequestContext.Principal)
.ContinueWith(t =>
{
LogFactory.For<AuthorizeAndRefreshUserAttribute>()
.ErrorException("Error occured", t.Exception);
}, TaskContinuationOptions.OnlyOnFaulted);
}
DbRefresher.RefreshAsync – это асинхронный метод, возвращающий объект Task, который продолжит своё выполнение в другом потоке. Выход из метода OnAuthorize осуществляется немедленно без ожидания завершения задачи. В случае аварийного завершения в лог будет добавлено сообщение об ошибке.
Вот и всё: остаётся только заменить в контроллерах атрибут Authorize на имя нашего нового атрибута. Новый атрибут возвращает управление сразу же после проверки прав текущего пользователя, после чего контроллер начинает свою работу. Обновление базы будет осуществляться параллельно с подготовкой ответа контроллером.
Тестирование
Тест, запускающий несколько одновременных запросов к сервису, поможет определить возможные проблемы:
[TestMethod]
public void ConcurrentTest()
{
const int threadCount = 10;
var tasks = new Task[threadCount];
for (int i = 0; i < threadCount; i++)
{
// DoOperations contains several CRUD operations on resources
tasks[i] = Task.Factory.StartNew(DoOperations);
}
Task.WaitAll(tasks);
}
Проблемы
Если для работы каких-то action-методов контроллера требуется, чтобы база заведомо содержала данные текущего пользователя, этот подход не годится. Придётся использовать явный вызов DbRefresher.RefreshAsync в теле метода.
Возможны проблемы во время добавления в базу нового пользователя при нескольких одновременных запросах. Если происходит попытка добавить пользователя в таблицу с уже существующим ключом, следует перехватить исключение primary key violation и прекратить работу. Тогда всю работу по обновлению данных пользователя выполнит только один поток.
Заключение
Этот подход успешно работает в одном из наших сервисов в Конфёрмите более года.
Мне он представляется простым и изящным. Было бы интересно узнать мнение сообщества.