.NET 5 WebAPI - zablokowanie endpointa, aż nie zakończy jednego zadania

0

Witam.
Mam pewien problem z aplikacją mobilną, która wysyła dokumenty do API w tle. Działa to różnie, ze względu na to jak działa Android (nie ma aplikacji na iOS). Oczywiście mam porobiony ify czy dokument już istnieje ale niestety dostaje zapytania do API w tej samej milisekundzie, co jest dziwne i niemożliwe do "zaifowania".

LOGI Z API

2021-10-11 11:32:53.1806|0|INFO|PanelOptimo.Controllers.OptimoController|[INSERTING_OBJECT] {"action":0,"customer":60061,"description":"","magazyn":1,"offlineNumber":"ZAM/375/10/2021/B22PT ","products":[]}
2021-10-11 11:32:53.1806|0|INFO|PanelOptimo.Controllers.OptimoController|[INSERTING_OBJECT] {"action":0,"customer":60061,"description":"","magazyn":1,"offlineNumber":"ZAM/375/10/2021/B22PT ","products":[]}
2021-10-11 11:32:53.1806|0|INFO|PanelOptimo.Controllers.OptimoController|[INSERTING_OBJECT] {"action":0,"customer":60061,"description":"","magazyn":1,"offlineNumber":"ZAM/375/10/2021/B22PT ","products":[]}

API zalogowało 3 razy ten sam dokument w tej samej milisekundzie. Jak to w ogóle jest możliwe? Jak zapobiec takiemu zachowaniu? Po stronie mobilki już wrzuciłem ifa na timestamp z różnicą na 10 sekund ale Android jest tak dziwny w przypadku headless events, że chyba prościej i pewniej będzie "zablokować" to po stronie API.

1

Żeby powiedzieć coś więcej o tym co się dzieje to musiałbyś udostępnić trochę kodu zarówno apki androidowej jak i może samego API (bo może to nie jest efekt tego że android strzela kilka razy jednocześnie a tego jak zaimplementowano logowanie).

Odnosząc się jednak do pytania - nie bawiłbym się w żadne blokowanie endpointów bo to nie ma sensu.
Po pierwsze kłóci się to trochę z samą ideą API, które powinno przyjmować tyle żądań ile jest do niego wysyłanych. Po drugie cała logika powinna siedzieć przecież w modelu. W samym modelu można więc wrzucać żądania do kolejki i wykonywać pojedynczo. Jako, że żądania są wykonywane w różnych wątkach można też wykorzystać prosty mechanizm synchronizacji np w postaci zwykłego locka czy innego bardziej wyszukanego rozwiązania jeśli jest potrzebne.

0

To jest stare API, które zostało wykorzystane do "nowszego" podejścia. Wcześniej nie było tego problemu, ponieważ aplikacja (PWA) była cały czas online, ponieważ było to www. W tym momencie duży nacisk został położony na pracę offline po stronie mobilki, stąd durne synchronizowanie dokumentów (zamówień) w tle. Obawiam się, że kod nic nie powie, ponieważ twierdzę, że to nie jest kwestia API, a tego jak działa Android i jego praca w tle. Przepisywanie API na "poprawne" też nie jest możliwe, ponieważ nie mam czasu, który mógłbym na to poświęcić i tak tylko dokładam więcej bałaganu to już istniejącego.

Logowanie jest zrobione za pomocą NLog
Startup.cs

LogManager.LoadConfiguration(string.Concat(Directory.GetCurrentDirectory(), "/nlog.config"));

Program.cs

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                }).ConfigureLogging(logging =>
                {
                    logging.ClearProviders();
                    logging.SetMinimumLevel(LogLevel.Debug);
                }).UseNLog();

OptimoController.cs


        [HttpPost]
        public async Task<IActionResult> InsertNewDocument([FromBody] object parameter)
        {
// ...
                ClaimsIdentity claimsIdentity = User.Identity as ClaimsIdentity;
                SysUser u = JsonConvert.DeserializeObject<SysUser>(claimsIdentity.FindFirst("user").Value);
                SysUserConfig uu = JsonConvert.DeserializeObject<SysUserConfig>(claimsIdentity.FindFirst("config").Value);

                _loggerManager.LogInformation("[INSERTING_USER] " + u.Email);
                _loggerManager.LogInformation("[INSERTING_SERIE] " + uu.UserSerie);
                _loggerManager.LogInformation("[INSERTING_OBJECT] " + JsonConvert.SerializeObject(parameter));
// ...

                    await _optimoService.SaveTraNag(trn, db);

OptimoService.cs

public async Task SaveTraNag(TraNag trn, IDbConnection db)
{
     var existing = await _sqlService.QuerySingleCompany<int>("select count(*) from CDN.TraNag where TrN_NumerObcy = @Number", new { Number = trn.TrN_NumerObcy });
     if (existing == 0)
     {
     }
}

Tak, wiem, logiki nie powinno być w kontrolerze. Nie chce tego poprawiać i w najbliższej przyszłości nie mam zamiaru. Problem polega na tym, że API nie czeka z przerobieniem danych tylko przyjmuje kolejne, co powoduje duplikacje w bazie. Te same zamówienie wpada po kilka razy, ponieważ if(existing == 0) zwraca true, bo tego dokumentu faktycznie jeszcze tam nie ma.

MOBILKA - Flutter (Dart)

Future<void> initPlatformState() async {
  await BackgroundFetch.configure(
      BackgroundFetchConfig(
          requiresBatteryNotLow: false,
          requiresCharging: false,
          requiresDeviceIdle: false,
          requiresStorageNotLow: false,
          minimumFetchInterval: 5,
          startOnBoot: true,
          stopOnTerminate: false,
          enableHeadless: true,
          forceAlarmManager: true,
          requiredNetworkType: NetworkType.ANY), (String taskId) async {
    final date = Hive.box<String>(Methods.appSettingsBox).get(Methods.synchronizeDate);
    if (DateTime.now().millisecondsSinceEpoch - lastFetchEventAt < 10000) {
      BackgroundFetch.finish(taskId);
    } else {
      if (auth.isAuthenticated() && date != null) {
        MainScreen.docController.documentSynchronization();
        lastFetchEventAt = DateTime.now().millisecondsSinceEpoch;
      }
      BackgroundFetch.finish(taskId);
    }
  }, (String taskId) async {
    BackgroundFetch.finish(taskId);
  });
}

Temat był drążony z autorem biblioteki.
Posibility to execute same job in two different threads #248

0
AdamWox napisał(a):

Tak, wiem, logiki nie powinno być w kontrolerze. Nie chce tego poprawiać i w najbliższej przyszłości nie mam zamiaru. Problem polega na tym, że API nie czeka z przerobieniem danych tylko przyjmuje kolejne, co powoduje duplikacje w bazie.

Tyle, że to nie jest problem API bo WebAPI tak właśnie działa by design. Każdy request jest wykonywany w osobnym wątku i jeśli zaimplementujesz żadnego mechanizmu synchronizacji to wiele wątków będzie mogło dowolnie wchodzić w sekcję krytyczną i robić co im się tam podoba w dowolnej kolejności.

Trzeba więc w takim razie albo te żądania zakolejkować albo użyć locka (czy czegoś innego jak trzeba). Ale wpinając mechanizm synchronizacji powinno się pamiętać o tym, że jeśli oprócz zapisu gdzieś te dane są odczytywane to trzeba je również zabezpieczyć przed byciem odczytywanym w momencie, w którym inny wątek robi zapis. Z tym, że jeśli założysz tego locka w API to ryzykujesz tym, że jeśli będzie korzystać z niego wielu klientów to wydajność spadnie bo każde żądanie będzie musiało poczekać na wykonanie tych, które już czekają.

To wygląda na problem apki androidowej więc prawdę mówiąc szukałbym rozwiazania właśnie tam i to tam upewnił się, że żądania są wykonywane sekwencyjnie a nie jednocześnie.

0

No właśnie w apce androidowej zrobiłem już wszystko co mogłem. Gość mi zasugerował co poprawić, zrobiłem to i dalej są cyrki. Problem polega na tym, że system Android to nie Windows i rządzi się kompletnie innymi prawami w kwestii operacji w tle. Nie obchodzi mnie czy kogoś zablokuje z API, ponieważ wymiana danych przebiega tylko w tle. Kontrahenci, towary są synchronizowane raz i trzymane w cache + wstępna synchronizacja dokumentów (już zapisanych). Każde kolejne zamówienie jest dodawane w tle. Zauważyłem, że jeśli mobilka ma problem z wysłaniem przez dłuższy czas (error 500, z powodu błędów w ERP) to potem leci takim hurtem, że mam 8 dokumentów z tego samego zamówienia. To samo się dzieje w stanie bezczynności. Gość odłoży urządzenie na dłuższy czas, weźmie na chwilę, aby coś sprawdzić i znowu to samo. Tak jakby Android kolejkował każde uruchomienie background_workera i potem wszystkie wywołania uruchamiał na raz jak tylko wyjdzie ze stanu idle.

Urządzeniami są tablety, które internet mają po WIFI z telefonu. Przedstawiciel jedzie do klienta i zapisuje zamówienie na tablecie (cache). Jeśli klient potwierdza zamówienie (słownie), to przedstawiciel klika w opcje "Przekaż do realizacji" i taki dokument przy następnym interwale leci do API i robi się z niego dokument w systemie ERP (Comarch Optima).

Ten system jest bardzo "wadliwy" z powodu swojej pracy offline'owej ale taki był wymóg klienta, ponieważ jak trafi się przedstawicielowi klient w bunkrze to nie zrobi zamówienia.

Tak jak napisałem wcześniej. Nie interesuje mnie czy "zablokuje" API. Chce zablokować. Zapis dokumentu nie trwa dłużej niż 10 sekund, zamówienie nie musi wpaść od razu. Po prostu musi wpaść, najlepiej raz.

W przypadku lock niestety chyba nie mogę zastosować, ponieważ nie mogę wtedy korzystać z await, a to pierwsze mi do głowy przyszło.

0

async/await nie służy do synchronizacji wątków. Skoro te API nie jest wcale obciążone to może tego nawet nie potrzebujesz i możesz to zaorać i uzyć zwyczajnego locka. Jeśli uprzesz się na upieranie async/await to mozesz zabezpieczyć sekcję krytyczną semaforem ale moim zdaniem nie ma to najmniejszego sensu.

0

Nie chodzi o synchronizacje wątków z async/await. Tam jest sporo warunków z jaką serią ma się dokument z zamówieniem wystawić. Pobieram z bazy odpowiednie informacje asynchronicznie. C# nie pozwala na

lock(obj)
{
   var customers = await db.QueryAsync<Customer>("select * from CDN.Kontrahenci");
}

Skorzystam z synchronicznego db.Query i przetestuje czy coś to pomoże.

PS.
Ja sobie zdaje sprawę z tego jak działa API i wiem, że mam potrzebę na głupie rozwiązanie ale ja nie jestem w stanie kompletnie zasymulować takiego zachowania na moim urządzeniu. Robiłem to już na wszystkie sposoby. To dzieje się tylko u nich z powodów, które nie są mi znane. Nawet gość od biblioteki kazał mi timestamp ustawić na wywołaniu workera, bo w Androidzie jest coś takiego jak Doze Mode i żeby się nie włączał w tym samym czasie to trzeba go "zaifować".

0

Ja bym na szybko bazował na tym https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0&tabs=visual-studio czyli background service jako kolejka, o ile usługa jest w jednej instancji, synchronizacja operacji będzie prosta.

0

Background Worker w ASP .NET Core ma to do siebie, że przestaje działać w momencie jak IIS jest w trybie bezczynności. I uprzedzam od razu sugestię - nie, nie mogę tego hostować czymś innym, bo mi klient nie pozwala grzebać, a z dockerami nie mam doświadczenia.

1

Troszkę to głupie, ale nie możesz sam do siebie zrobić requestu http? Chociaż lepiej byłoby już zainstalować to jako serwis jak ma robić background joby, albo docker, bo to nie jest trudne w .netcore

0

Powiedziałbym nawet, że to bardzo głupie ale ja potrzebuje rozwiązania na już bez straty czasu na pisanie kolejnego oprogramowania. Zwyczajnie nie warte to jest tej ceny. Na dockera nie mam doświadczenia, a już tym bardziej czasu na naukę choć prędzej czy później będzie trzeba się nauczyć, bo widzę, że to coraz popularniejsze się robi.

Kolejkowanie na bank wróci do mnie i to ze zdwojoną siłą. Na tę chwilę tych przedstawicieli jest 3, a domyślnie ma być kilkanaście, więc jak im w API zrobię lock(obj) to dokumenty z 3 dni wstecz będą wpadać. Domyślam się, że mówiąc "kolejkowanie" macie na myśli jakieś "króliki", a nie zwyczajnie wrzucić wpis do bazy i serwisem pobierać co jakiś czas?

0

Kolejkowanie? HangFire

0
AdamWox napisał(a):

Kolejkowanie na bank wróci do mnie i to ze zdwojoną siłą. Na tę chwilę tych przedstawicieli jest 3, a domyślnie ma być kilkanaście, więc jak im w API zrobię lock(obj) to dokumenty z 3 dni wstecz będą wpadać. Domyślam się, że mówiąc "kolejkowanie" macie na myśli jakieś "króliki", a nie zwyczajnie wrzucić wpis do bazy i serwisem pobierać co jakiś czas?

Nie, przynajmniej nie ja. Jako kolejkowanie miałem na myśli jakikolwiek mechanizm do którego można wrzucić żądania i je obrabiać po kolei. Możesz to robić przez rabbita, kafkę, użyć bazy czy jakiejś prostej kolekcji/mechanizmu w pamięci z opcją persystencji żeby nie utracić danych.
Tyle, że wtedy zmieni się kompletnie zachowanie systemu bo zamiast akcji "wyślij dokument -> otrzymaj wynik przetwarzania" przejdziesz na "wyślij dokument -> otrzymaj potwierdzenie przyjęcia". Będzie to całkowicie asynchroniczne i całą obsługę tego jak potem dociągnąć sobie te dane będziesz musiał dopisać.

0

Czytam tutaj wszystkie posty i zastanawiam się, dlaczego nikt nie zaproponował po prostu transakcji w bazie danych?

0

Bo to nie jest moja baza i muszę to robić bibliotekami producenta systemu ERP, w tym przypadku Comarch.

0

@AdamWox: w jaki sposób skoro w sqlservice wykonujesz zapytanie sql?

0

Wykonuje nim zapytania o informacje z którymi obiekty COM Comarchu sobie nie radzą. Niestandardowe życzenia klienta ogarniam czystym SQL ale tylko pobieranie danych. Nie zapisuje niczego do bazy SQLem.

2

Czy dobrze rozumiem że problemem który chcesz rozwiązać to uniemożliwienie dodania tego samego dokumentu więcej niż raz?

Jeśli tak to zastosuj jakąś transakcję, nawet w bardzo prostej formie. Jakiś pessimistic lock z wykorzystaniem bazy danych, i tylko na id dokumentu. Co za tym idzie pierwszy request doda transakcyjnie te ID do bazy, a każdy następny request próbując dodać nie da rady, więc będzie wiedział że dany dokument został już obsłużony* i nie ma nic więcej do roboty. Powtarzam raz jeszcze że chodzi tu o transakcję samej informacji o dokumencie, w nie dokumencie samym w sobie. Pozwoli to uniknąć zbyt długiego blokowania requestów. Zwykły wpis w bazie dla każdego dokumentu również by się nadał, ale jeśli faktycznie masz przypadki gdzie lecą requesty dla tego samego dokumentu w tej samej lub bliskiej milisekundzie to raczej bez locka się nie obejdzie.

*Dokument którego ID został już dodany do bazy będzie następnie zapisany, i operacja oczywiście może się nie powieść. Zależnie od konkretnych wymaganiach biznesowych, być może będziesz musiał w takim przypadku jakoś to obsłużyć i zdjąć locka.

1
  1. Możesz blokować API np. przy użyciu semaphore slim ale w przypadku natłoku dokumentów to jest słabo skalowalne rozwiązanie.
  2. Transakcja w DB wydaję się tu czymś naturalnym. Aczkolwiek jest to pewnego rodzaju oddelegowanie tego problemu na poziom bazy. Może być ok, może nie.
  3. Kolejkowanie dokumentów po stronie API. Jeśli nie chcesz dokładać nic do infrastruktury (Rabbit MQ) możesz spróbować z podejściem in-memory z użyciem ConcurrentQueue<T> lub BlockingCollection<T>. Tylko tutaj trzeba się liczyć z utratą dokumentów, jeśli proces Ci się wywali, a coś w kolejce będzie jeszcze czekać. Popatrz na NService Bus i SQL Server Transport https://docs.particular.net/transports/sql/ który wykorzysta istniejący MS SQL (który masz pewnie pod ręką ze względu na Optimę/CDN)
0

Jeszcze do głowy przyszło mi bardzo proste rozwiązanie jeśli jedynym problemem są próby dodania tych samych dokumentów w tym samym czasie (a nie to że ten sam dokument może być znów dodany w innym czasie, np. po kilku godzinach lub dniach). Do tego powinien wystarczyć jakiś "lock" w pamięci, np. przy użyciu concurrent dictionary. Działo by to w zasadzie tak samo jak przy użyciu bazy danych, tj. próba dodania id dokumentu, a jeśli już istnieje to odpowiednie zasygnalizowanie błędu. Przy użyciu concurrent dictionary to kwestia wywołania TryGetValue przekazując nową wartość jako parametr, która zostanie dodana jeśli wartość dla danego klucza nie istnieje. W przeciwnym razie metoda zwróci true i będziesz wiedział że nie należy znów obsłużyć tego dokumentu.

Oczywiście to będzie działało do następnego restartu Twojego API, bo po restarcie dictionary będzie puste. Zakłada to również użycie jednego procesu, a nie np. wielu instancji web API schowanych za load balancerem.

0

Na tę chwilę lock(obj) działa. Czekam, aż reszta przedstawicieli dołączy do systemu, bo aktualni trzej nie robią na tyle ruchu, aby było widać, że coś "wolno" wpada do ERP. Dobrze jest mieć kilka alternatyw na przyszłość. Czuje, że ten problem wróci do mnie i będę musiał, którąś przez was napisanych wdrożyć.

1 użytkowników online, w tym zalogowanych: 0, gości: 1