.net core Странное поведение при асинхронном ожидании (возможно, из-за нескольких запросов) - PullRequest
0 голосов
/ 24 октября 2019

У меня есть следующий вариант использования, FetchDataService (внедренный как область действия), извлекающий данные из API с использованием httpClient.
Затем служба анализирует данные и запускает await SaveAsync их в базу данных.
Все до этого момента прекрасно работает.
Затем я добавил еще одну службу: AddressEnrichmentService (внедренную как область действия), которая на этапе анализа анализирует внешнюю службу асинхронно и приносит больше данных.
Эта служба искажает весьприложение. Я получаю случайные исключения из обработчика базы данных (NHibernate), который указывает на некоторые проблемы с потоками. Я действительно не могу положить на это свой палец. несколько разных случайных исключений ... Эти исключения генерируются при запуске await SaveAsync. (Я добавлю их в конце).

public async Task Fetch(string url, UserModel userModel ){
    string res = null;
    using (var httpClient = new HttpClient()){
       res = await httpClient.GetStringAsync(url);
    }
    await databaseLogRepository.SaveAsync(new Log{Message = "The data is here..." + res});
    var profile = JsonConvert.DeserializeObject<ProfileModel >(res);

    profile.Data.Locations.ForEach(async x =>
    {
        var address = await addressEnrichmentService.EnrichAsync(x.StreetAddress + " " + x.Locality + " " + x.Country);
        userModel.Address = address;
        await databaseLogRepository.SaveAsync(new Log{Message = "enriched address"});
   });
}

Проблемный сервис:

public class AddressEnrichmentService : IAddressEnrichmentService
{
        public async Task<AddressModel> EnrichAsync(string address)
        {
            string res = null;
            using (HttpClient client = new HttpClient())
            {
                var url = "https://maps.googleapis.com/maps/api/geocode/json?key=__KEY__&address=" + HttpUtility.UrlEncode(address);
                    res = await client.GetStringAsync(url);// probably the problematic row
            }
             return JsonConvert.DeserializeObject<AddressModel>(res);
        }
 }

Использование

UserModel userModel = new UserModel();
await fetchDataService.Fetch(url, userModel);
await userRepo.SaveAsync(userModel);

Снова addressEnrichmentService.EnrichAsync все портит,Как я узнаю, что EnrichAsync все испортило?
, если я конвертирую res = await client.GetStringAsync(url); в res = client.GetStringAsync(url).GetAwaiter().GetResult();, я не получаю никакой ошибки. 100 раз из 100 попыток я не получаю ошибки. если я возвращаю его к res = await client.GetStringAsync(url);, я каждый раз получаю сообщение об ошибке.

Об исключениях, опять же, я получаю некоторые признаки того, что ошибка связана с параллелизмом, это метод сохранения:

public async Task<T> SaveAsync(T entity)
{
    using (var tr = session.BeginTransaction())
    {
        //session is injected as scoped
        await session.SaveOrUpdateAsync(entity);
        tr.Commit();
    }
    return entity;
}

MySqlException: There is already an open DataReader associated with this Connection which must be closed first.

NHibernate.HibernateException: 'Flush during cascade is dangerous'

1 Ответ

3 голосов
/ 24 октября 2019

Вот проблема:

profile.Data.Locations.ForEach(async x =>

Метод ForEach принимает Action, а не асинхронный делегат. Поэтому возвращаемое значение вашего async делегата (Task) игнорируется. В результате вы получаете метод async void, который мучает множество разработчиков до вас (поищите async void, чтобы увидеть бесконечный список связанных вопросов). Поскольку Task не ожидаются, они работают одновременно. Несколько потоков отправляют параллельные запросы в вашу базу данных, которая, очевидно, не может обрабатывать параллельные запросы. Не говоря уже о том, что эти задачи больше не могут быть выполнены. Они стали задачами «забей и забудь».

Чтобы решить проблему, замените причудливый ForEach простой ванилью foreach. Каждый Task будет должным образом ожидаться, нежелательный параллелизм не возникнет, и проблема будет решена.

foreach (var x in profile.Data.Locations)
{
    var address = await addressEnrichmentService.EnrichAsync(
        x.StreetAddress + " " + x.Locality + " " + x.Country);
    userModel.Address = address;
    await databaseLogRepository.SaveAsync(new Log{Message = "enriched address"});
}
...