Wykonywanie kilku zadań w tym samym czasie za pomocą Quartz

0

Witam.
Potrzebuje napisać usługę (usługa windows, service worker), która będzie przerabiać dane pomiędzy SQL Server, a API Allegro. W związku z tym, że ofert na Allegro jest bardzo dużo i tyczy się to kilku kont (8), to chciałbym temat ogarnąć w miarę asynchronicznie, aby jak najwięcej rzeczy przeliczyć w tym samym czasie.

Są momenty, w których jest tak dużo zmian, że timer się nakłada i nie kończy poprzedniego zadania (co 30 minut). Czas operacyjny jest czasem też wydłużany przez sekundowe oczekiwanie na odpowiedź z API Allegro, głównie przy metodzie PUT.

Aktualizowane są stany magazynowe, ceny oraz aktywność ofert.

  1. Czy jedynym, mądrym, rozwiązaniem będzie podział każdej operacji na osobną usługę?
  2. Czy za pomocą Quartz jestem w stanie to rozbić przy jednej usłudze? Może inny "timer" jest w stanie?

Na samym początku było tak, że był jeden IJob i w pętli leciałem po wszystkich kontach, mało to wydajne, bo... wiadomo. Teraz jest tak, że Quartz uruchamia dla każdego konta osobny IJob, więc troszkę wydajniej. Tym razem chciałbym pójść level wyżej i rozdzielić to na tyle wątków na ile jest to możliwe.

// pętla po kontach
foreach(AllegroAccount account in _allegroAccounts)
{
  // pobranie ofert z danego konta
  var offers = await _allegroService.GetOffers(account);

  // podział ofert na zdania
  var endedOffers = offers.Where(x => x.publication.status == "ENDED");
  var activeOffers = offers.Where(x => x.publication.status == "ACTIVE");
  var stockPriceOffers = offers.Except(endedOffers); // nie wiem czy dobrze wybrałem, chodzi o ogólny zamysł, żeby nie przerabiać nieaktywnych

  JobDataMap endedJobData = new JobDataMap();
  endedJobData.Put("jobData", endedOffers);
  
  JobDataMap activeJobData = new JobDataMap();
  activeJobData.Put("jobData", activeOffers);
  
  JobDataMap stockPriceJobData = new JobDataMap();
  stockPriceJobData.Put("jobData", stockPriceOffers);

  // stworzenie zadań
  IJobDetail job = JobBuilder.Create<EndOffersJob>().UsingJobData(endedJobData).Build();
  IJobDetail job = JobBuilder.Create<ActiveOffersJob>().UsingJobData(activeJobData).Build();
  IJobDetail job = JobBuilder.Create<StockPriceOffersJob>().UsingJobData(stockPriceJobData).Build();
}

Czy taki przykład załatwi mi sprawę asynchronicznie? Czy to można już nazwać multitaskingiem? Jak ogarnąć logi i wyjątki jeśli chciałbym wiedzieć co kiedy się zrobiło?

0

Pytanie co robisz z tym danymi, co to za dane itp. Jak masz zakres danych i wiesz, że masz obsłużyć oferty od 1 do 1000 to podzielić na Joby po zakresie - czyli jedne bierze od 0- 500 a drugi od 500 - 1000. Każde konto idzie na innym wątku to w logach powinny być inne PIDy (jak są logowane).

0

Ogólnie chodzi o to, aby na Allegro było to samo co w systemie ERP - stany magazynowe, ceny, aktywność.

Klient ma dużo towarów złożonych (bundle), z których trzeba wyciągać składniki i przeliczyć ich stan. Potrzebne jest to, aby przekazać stan złożonego do Allegro na podstawie ilości składników, gdyż złozone na magazynie są (prawie) zawsze 0, a "produkuje" się je (przyjmuje na stan) pod zamówienie. Klient chce też mieć kontrole nad ofertą i ustawiając flagę na towarze Aktywny o wartościach TAK lub NIE, który decyduje o aktywności oferty na Allegro, zwyczajnie nieaktywnych nie ma sensu tykać.

Dla jednego konta ofert jest prawie 4000, reszta ma około 1000, a kont jest 8. Różnicy w czasie przy pobraniu 500, a 1000 nie ma wielkiej. Allegro pozwala maksymalnie pobrać 1000, poźniej już trzeba tylko manewrować offsetem. Nie wiem czy podział wątków na zakres towarów w czymś pomoże. Najdłużej schodzi tam gdzie trzeba wysłać do API PUT i zaktualizować dane. Dodatkowo, obiekt oferty jest inny pobierając listę ofert sale/offers, a pobierając konkretną ofertę sale/offers/{id} i nie czepiałbym się tego, bo to akurat normalne, ale jeśli puszczę do API PUT z obiektem z listy ofert, to kilka ważnych parametrów (stawka vat, promocje) jest NULL i zeruje się to na Allegro. Muszę to maksymalne podzielić, aby pobierać konkretną ofertę najrzadziej.

Analizując jeszcze przykład jaki zapoproponowałeś - jeśli weźmiemy pod uwagę rozbicie tego na "co 500" na wątek, to miałoby to działać w obrębie jednego konta na raz, czy osobny wątek na konto, a wątku konta osobne wątki na "co 500" ofert?

1
[Allegro napisał(a)] https://developer.allegro.pl/tutorials/informacje-podstawowe-b21569boAI1:

Ograniczenie liczby zapytań (limity)
W usłudze Allegro REST API (produkcyjnej oraz testowej) obowiązuje główny limit nakładany na Client ID (lub Software Statement ID w przypadku DCR) - 9000 zapytań na minutę.
Gdy przekroczysz limit:
na minutę zablokujemy twój Client ID,
zwrócimy odpowiedź ze statusem: 429 Too Many Requests.
Po upływie wskazanego czasu automatycznie przywrócimy dostęp do usługi dla twojego Client ID.
Dla niektórych zasobów stosujemy dodatkowe, niższe limity liczby żądań. W takich przypadkach informacje o dodatkowym limicie znajdziesz w opisie zasobu w dokumentacji REST API Allegro.

0

W twoim przykładzie co się stanie jeśli wyłączy się serwis w trakcie synchronizacji ? Wystarczy puścić synchronizacje od nowa czy jest lipa ?

1

Raczej mi nie zależy żeby kontynuować wątek. Może być robione od zera. Klient nie ma stanów na styk, że musi być zrobione to co system zaczął.

Jeśli poprawię wydajność i będzie w stanie ogarniać to "szybko", to problemu nie będzie z wyłączaniem usługi.

2

@AdamWox: No to wydaje się prosta implementacja producer/consumer z kolejka(in-memory) zadan. Wtedy producent synchronicznie wrzuca na kolejke, a sterujesz liczbą asyncronicznych consumentów w zależności od zasobów. I wtedy nawet jak producent bedzie wolny to i tak w momencie dodania na kolejkę kosumenci się już odpalają w tle. Na necie powinno być w ch..j przykładów takich implementacji :p.
Dodatkowo możesz zrobić sobie wtedy dashboard co jest na kolejce, co robi który worker, i ew zrzucać informacje do innego obiektu błędy i błedne zadania.
I quartz odpala ci tylko producera o wyznaczonych godzinach.

0

Ok, poczytałem dokładnie dokumentację. Wcześniej tego nie widziałem, ale jest opcja Batch offer modification, w którym jest price oraz quantity. Osobno jest też kończenie i aktywowanie aukcji, ale z tego już korzystam. To może być dużo lepsze, ponieważ wystarczy przygotować listę ofert pod konkretną operację i endpoint przyjmuje listę id tych ofert. Pomijam pobieranie pełnej oferty, to co mi potrzebne (cena, ilość) mam pobierając z listy ofert.

EDIT
Okazało się, że endpointy batch ustawiają tą samą wartość dla wybranych ofert, a nie różne wartości dla konkretnej oferty. Mogę albo lecieć w pętli i każdego aktualizować za pomocą PUT, albo pogrupować wynik po cenie i/lub stanie i wykorzystać batch. Co dla przykładu - dla cen zmniejszy ilość zapytań do API z 47 na 2, a w przypadku ilości 900+ na 200+... #performance 😎

@Schadoow: omg 😅 chyba, aż tak inżynieryjnie rozwiązywać tego nie trzeba. Co masz na myśli pisząc kolejka (in-memory)? O jakiej kolejce mówisz?

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