Kolejkowanie wykonywania zadania przez kilka oddzielnych usług

0

Witam.
Mam problem w kwestii usługi Windows, która już ma tak dużo rzeczy do zrobienia, że w konkretnym interwale nie wyrabia i zaczyna się zapętlać, czasem robi pewne rzeczy podwójnie. Niestety asynchroniczność i tego typu bajery odpadają, ponieważ to są operacje na Comarch Optima, a ona niestety nie ogarnia. Operacje na obiektach COM Optimy są wolne, zamówień jest ponad 1000 dziennie i czasem się robi z tego powodu taki bałagan, że trudno to posprzątać.

Wpadłem na pomysł, aby rozdzielić to na kilka usług i każda z nich będzie miała swoje zadanie do wykonania. Ale... Żeby kolejna usługa mogła coś robić to poprzednia musi skończyć swoje działanie. Przykład:

Etap 1. Usługa sprawdza zamówienia (RO) i odpytuje Allegro o ilość paczek i z jakiego konta (Allegro) jest zamówienie i dopisuje odpowiednie informacje.
Etap 2. Usługa generuje fakturę/paragon do w pełni zrealizowanego dokumentu WZ
Etap 3. Usługa sprawdza zamówienia (RO) czy towar jest na stanie i generuje WZ + generowanie kompletacji (PWP) dla towarów złożonych
Etap 4. Tracking - dopisywanie statusu przesyłki (odpytywanie o status przesyłki DPD lub INPOST)

Pomiędzy tymi etapami są jeszcze statusy magazynowe (pobrane, przygotowywane, gotowe do wysyłki, wysłane). Faktura/paragon oraz tracking wykonywane są dla statusu "wysłane".

  1. Czy jedynym rozwiązaniem jest zrobić jakiś wpis w bazie, który będzie decydował o kolejność wykonywania i na podstawie tego wpisu kolejna usługa będzie wiedziała, że ma teraz pracować?
  2. Czy rozdzielanie tego jest dobrym pomysłem?
  3. Lepiej będzie ustalić jakiś interwał uruchamiania usług czy nasłuchiwać konkretne pole w bazie na zmiany i wtedy uruchamiać?

W planach jest jeszcze dołożyć kolejny etap, który będzie sprawdzał stany magazynowe oraz ceny i aktualizował je na Allegro. Potrzebne jest to głównie do towarów złożonych, których stan magazynowy jest oparty o ilość składników jakie tworzą dany produkt (towar złożony), a takie produkty są kompletowane na bieżąco pod konkretne zamówienie (Etap 3).

2

Jeśli chcesz to robić w jednym procesie to najłatwiej będzie wydzielić każde zadanie do oddzielnego, asynchronicznego modułu i mieć jedno miejsce gdzie będzie to po kolei wywływał- coś takiego:

var resultOne = await doSomethingOne();
var resultTwo = await doSomethingTwo();

Najlepiej zastosuj do tego wzorzec mediator np. przy użyciu biblioteki MediatR, i miej każde zadanie zdefiniowane jako oddzielny request i odpowiadający mu request handler.

Ewentualnie możesz pobawić się w notyfikacje/zdarzenia, i uruchamianie całego łańcucha akcji w kolejności, po zakończeniu poprzedniej.

Jeśli chcesz to robić w oddzielnych procesach to użyj do tego jakiejś kolejki (RabbitMQ, Kafka, cokolwiek), na zakończenie każdego procesu zasygnalizuj to eventem który następnie będzie obsługiwany w wyniku czego rozpoczniesz kolejny proces, później znów event na zakończenie i tak do końca aż wszystko zostanie zrobione.

0

Tak jak napisałem wyżej. Nie mogę użyć asynchroniczności, bo optima się nie zaloguje w drugim wątku jeśli poprzedni się nie zakończy. A takie "czekanie", kolejkowanie mam właśnie teraz i nie ogarnia niestety.

Wszelkiego rodzaju "króliki" i inne tego typu rzeczy wymagają instalacji dodatkowych głupot do poprawnego działania. Nie mam z tym doświadczenia, a za czas poświęcony na ogarnięcie tego nikt nam nie zapłaci.

Padła jeszcze propozycja usługi, która uruchamiała by odpowiednie usługi w określonej kolejności ale to chyba za bardzo przeinżynierowane 🤔

0

Chyba nie rozumiem na czym polega tak naprawdę problem. Skoro to co używasz nie "ogarnia" asynchronicznosci to tym bardziej wydaje mi się że będziesz miał problem jeśli chciałbyś odpalać to w oddzielnych procesach. Wtedy przecież stracisz cały kontekst, to jest stan Twojej operacji.

0

Hej

A nie możesz miec dodatkowej tabeli czy kolumny status i po rozdzieleniu tego procesu na mniejsze każdy z tych procesów sprawdzało by swoje statusy.. nazwij je jak te etapy.

0

Etapy które opisałem wyżej są robione w pewnym interwale, np. co 30 minut (korzystam z Quartz.NET), jeden po drugim. Zajmuje się tym jedna usługa. Ilość dokumentów do przerobienia i "zawrotna" prędkość wykonywania operacji przez Optimę powoduje, że nie skończy robienia wszystkich etapów w ciągu tych 30 minut i znowu robi wszystkie etapy. Przez takie udziwnienie i zazębianie się interwałów miałem sytuację, w której kompletacja (dokument, który tworzy produkt ze składników na podstawie receptury) tworzyła się dwa albo trzy razy. Oprócz tego to próbował zatwierdzać dokumenty po trzy razy co generowało masę błędów, ponieważ ten dokument był już zatwierdzony.

Wydłużenie interwału też parę razy narobiło jakiś głupot, bo usługa nie chodziła przez tydzień z powodu remanentu i nazbierało się tak dużo, że nawet dwie godziny nie pomogły. Zresztą, jaki sens na robienie tego co dwie godziny? Myślę, że system powinien na bieżąco obrabiać dane, aby nie było przestojów w pakowaniu i wysyłce.

Co do asynchroniczności. Jedyne co jest async to metoda realizacji IJob biblioteki Quartz.NET, bo taki jest ich wymóg, a wszelkie metody (etapy) obrabiania danych są synchroniczne, ponieważ:

W obrębie jednego procesu nie można równocześnie logować się kilka razy. Takie operacje muszą być wykonywane synchronicznie. Jeśli jeden wątek się zaloguje, to inny wątek nie może się zalogować, musi czekać aż pierwszy wątek wykona wszystkie operacje i się wyloguje.

Myślę, że głównym problemem jest tutaj Optima, ponieważ obojętnie jak dobrze chciałbym to zrobić to mnie blokują ich zasady i biblioteki, a muszę z nich korzystać, aby dobrze wszystko wprowadzić do bazy.

Jedyne co mi na teraz przychodzi do głowy to podzielić aktualną usługę na kilka usług i uruchamiać je w określonej kolejności usługą główną (zarządzającą). Coś na zasadzie Process.Start() i WaitForExit(), następna usługa Start(), WaitForExit() itd.

@sight
Nie wiem czy dobrze rozumiem. Możesz rozwinąć myśl?

0

A czy musisz to robić w oparciu o samą Optimę? Jakiś czas temu wdrażaliśmy w firmie integrację ERP (nie Optima) z programem serwisowym oraz z aplikacją do ofertowania. Technicznie to była integracja dwóch istniejących aplikacji (serwisowy i ERP) oraz napisanie od zera ofertownika.

Poniewaz koleś, który się tym zajmował znał bardzo dobrze sposób działania ERP'a, to po prostu stworzył swoja aplikację, która działała na bazie ERP, ale nie robiła tego przez jakieś API czy inny mechanizm oferowany przez system ERP, tylko samodzielnie działał na bazie. Wiem, że jest to ryzykowne i wymaga dużo pracy, ale z drugiej strony to wtedy praktycznie nic Cie nie ogranicza - chociażby nie musisz czekać na te 30-minutowe interwały, bo synchronizację (czy inne przetwarzanie) robisz na żadanie/według własnych potrzeb i na swoich warunkach.

0

W przypadku Optimy to tutaj jest dużo więcej roboty względem tego co już mają w swoim API. Na przykład przekształcanie dokumentów:

SerwisHaMag Serwis = (SerwisHaMag)Sesja.CreateObject("Cdn.SerwisHaMag", null);
Serwis.AgregujDokumenty2(308, rRs, magId, 306, out int klasaDok, out int rodzajDok, out int wydanieID);

To jest okrojona wersja. Oczywiście wyżej jest jeszcze logowanie do Optimy i tworzenie sesji ale koniec końców mam wszystko na tacy. Jedna metoda, która wszystko robi automatycznie i przekształca zamówienie do WZ. Jakbym miał to pisać to niestety sam bym sobie prędzej w łeb strzelił niż klient by zapłacił.

Optima kiedyś może i była dobrze zaprojektowana, ale dzisiaj obiekty COM, NET Framework 3.5 to era dinozaurów i na aktualne czasy stwarza to wiele problemów, a klienta to nie obchodzi, bo "ma działać skoro płaci".

1

Generalnie temat o którym piszesz jest mi bliski ponieważ w podobnej logistycznej branży robię. Mamy różne środowiska prazy/integracja tak jak w Twoim przypadku z web api kuriera.. ale zawsze model jest taki ze każde zamówienie ma etapy.. najpierw wpada zamówienie.. potem wypuszczane jest to do realizacji.. potem zwożone.. potem kontrolowane.. pakowane i na końcu załadunek. Cały system składa sie z mniejszych programów i usług i każdy z nich wiec właśnie po statusie danego zamówienia co ma z nim i czy może cos zrobić. Tu dobrze kolega zapytał.. jak dużo Twoje rozwiązane jest zintegrowane z Optimą.. bo jak wiem te aplikacje jest sa za super napisane. Ja my na pewno Twój proces podzielił na małe etapy.. potem łatwiej cos rozbudować lub znaleźć błąd. Dla przykładu robisz sobie pierwsza usługę która sprawdza tylko czy mamy nowe zamówienia a jak mamy to zapisuje to w bazie ze statusem 1. Kolejna jak widzi status 1 odpytuje Allegro o ilość paczek i jak ma odpowiedz a bierzemy pod uwage np brak Internetu to dopisuje do bazy dane i zmienia status 1 -> 2. Do tego robisz sobie index w bazie na tej kolumny status. Kolejna 3 usługa czyli już nasz status 3 to Usługa generuje fakturę/paragon.. nie wiem czy faktury w formacie pdf czy od razu sie drukują czy sa dostępne w aplikacji ale jak to się wykona to zmienia na status 4..

1

Do Twojego problemu idealnie pasuję kolejkowanie MQ rabbit itp. ale jak nie chcesz używać to może po prostu lockować obiekt wykonania zadań i potem jak się wszystko skończy robić release i wtedy odpalać nowy interwał w usłudze windowsa?

Próbowałeś to?

https://docs.microsoft.com/pl-pl/dotnet/csharp/language-reference/keywords/lock-statement

0

Co do kolejek to dodam jeszcze że zależnie od tego na jakim serwerze to masz uruchomione to możesz mieć jeszcze możliwość użycia MSMQ. O ile dobrze pamiętam musi tylko być włączony w ustawieniach Windowsa, tak więc będziesz miał możliwość podpięcia architektury opartej o wiadomości (messaging) bez instalowania dodatkowych/zewnętrznych narzędzi.

0

Nie wiem dlaczego, może to moja niewiedza na temat "królika" i tego typu zabawek ale jakoś mi to tutaj nie pasuje. U mnie nie ma żadnej komunikacji, nie ma żadnej takiej potrzeby (jak na ten moment). Włączam usługę, a ona robi co ma robić, co jakiś, określony czas. W tym co robi jest tak dużo operacji na Optimie, że czasem nie wyrabia i się dzieją bardzo dziwne rzeczy. Może kod wam pokaże jak bardzo to jest brzydkie i popsute.

Wywołanie zadania za pomocą Quartz.NET

    public class MainJob : IJob
    {
        public async Task Execute(IJobExecutionContext context)
        {
            try
            {
                LOG.Info("JOB!");
                AppConfig _appConfig = Methods.DeserializeJson<AppConfig>(AllegroService.appConfigPath);
                await DocumentsService.GetDocumentsReadyToPack(_appConfig);
            }
            catch(Exception ex)
            {
                LOG.Error(ex);
            }
        }
    }

Okrojony kod DocumentsService.GetDocumetsReadyToPack()

public static async Task GetDocumentsReadyToPack(AppConfig ac) {
  using(IDbConnection db = new SqlConnection(SQLConnectionString(ac.SQLConfig))) {
    AdoSession session = OptimaService.Login(ac);

    if (ac.RobAllegro) {
      var aqq = await OptimaService.GetDocuments(ac, db);

      foreach(Document document in aqq) {
        // Pierwszy etap - sprawdza konto Allegro i ilość paczek.
      }
    }

    if (ac.RobFAPA) {
      var documentsToFinish = await db.QueryAsync<TraNag>(WyslaneRezerwacjeOdbiorcyQuery, new {
        WarehouseAttributeName = ac.WarehouseStatusAttributeName
      });

      if (documentsToFinish.Count() > 0) {
        LOG.Info("DOKUMENTY_WYSŁANE: " + documentsToFinish.Count());
        foreach(TraNag documentToFinish in documentsToFinish) {
          // Drugi etap - generuje fakturę lub paragon do zamówień zrealizowanych i wysłanych.
        }
      }
    }

    if (ac.RobWZ) {
      IEnumerable<TraNag> documents = await GetAllRezerwacjeOdbiorcy(ac, db);

      foreach(TraNag trn in documents) {
        /*
          Trzeci etap 
          1. wyciąga zamówienia niezrealizowane, 
          2. sprawdza stany magazynowe poszczególnych pozycji na zamówieniu
          3. dla towarów złożonych tworzy PWP (kompletację)
          4. zamyka zamówienie i generuje WZ
        */
      }
    }

    if (ac.RobTracking) {
      IEnumerable<DocumentTracking> trackingNumbers = await db.QueryAsync<DocumentTracking>(DocumentsForTrackingQuery, new {
        WarehouseAttributeName = ac.WarehouseStatusAttributeName
      });

      LOG.Info("TRACKING_COUNT: " + trackingNumbers.Count().ToString());

      foreach(DocumentTracking dt in trackingNumbers) {
        // Czwarty etap - odpytuje API InPostu lub DPD o status przesyłki i wpisuje treść statusu w odpowiednim miejscu na zamówieniu.
      }
    }

    // Tutaj w planach jest piąty etap - aktualizacja cen i stanów magazynowych na Allegro na podstawie stanów w Optimie.

    OptimaService._LoginObject.Logout();
  }
}

Etap trzeci jest najbardziej awaryjny i najwięcej problemów generuje. W tym etapie się dzieje najwięcej i jest to jeden z najważniejszych etapów, zaraz po etapie pierwszym. Do pewnego czasu system wyrabiał się w normie, jak klient miał 300 - 500 zamówień dziennie. Lekko szło co 30 minut i nie miał ani jednej czkawki. Sprzedaż zakwitła i zaczęły się cyrki. Później remanent i w tydzień zrobiło się tak dużo zamówień, że już kompletnie zwariował. Nie chce tracić czasu na bawienie się w detektywa, a wydłużanie czasu obróbki danych jest kompletnie nie logiczne, ponieważ my nie pilnujemy ilości zamówień i nie wiemy kiedy system wyrabia, a kiedy nie.

PS.
Nazwy niektórych obiektów, klas, zmiennych nie zostały zmienione. Domyślnie (pierwsza wersja) miała tylko pierwszy etap, a reszta była robiona ręcznie na programie okienkowym, który weryfikował co jest pakowane, generował odpowiednie dokumenty i "wzywał" kuriera z drukowaniem listu przewozowego.

0

Mosquito zamiast Rabbita. Jest bezobslugowym serwisem Windows. Prosty w obsłudze.
Nie wiem czy api Optimy może się zalogować w osobnych serwisach Windows roznymi uzytkownilami bo api xl chyba nie umie.

2

Można użyć Hangfire.
Tworzymy sobie jednego workera który będzie wykonywać zadania w tle. Do recurring jobs dodajesz sobie wszystkie swoje zadania, konfigurujesz je tak żeby uruchamiały się w danym workerze i jeśli jedno zadanie będzie się chciało uruchomić w tym samym czasie co inne zostanie zakolejkowane i uruchomione dopiero po zakończeniu poprzedniego.
Jeśli twoje zadanie nie będzie wymagało Optimy możesz je dodać do nowego workera.

  • Otrzymujesz fajny dashboard do zarządzania tymi zadaniami :)
0

Hangfire odpada albo ja coś źle robię:

  1. Robi w losowej kolejności co w moim przypadku jest nie do przyjęcia.
  2. Uruchamia wszystkie zadania, gdzie to kolejkowanie.
  3. Częste wyłączanie i włączanie usługi podczas gdy jakieś zadanie trwa, generuje totalny chaos w kolejności i wykonywania zadań.

Nie testuje tego na faktycznym wykonywaniu "moich" zadań i operacji na Optimie. Zrobiłem prowizorkę.

RecurringJob.AddOrUpdate("Job1", () => MainJobs.TestJob(2000, "Job1"), Cron.Minutely);
RecurringJob.AddOrUpdate("Job2", () => MainJobs.TestJob(1000 * 60, "Job2"), Cron.Minutely);
RecurringJob.AddOrUpdate("Job3", () => MainJobs.TestJob(5000, "Job3"), Cron.Minutely);
public static void TestJob(int miliseconds, string jobName)
{
   Console.WriteLine(jobName + " started");
   Task.Delay(miliseconds).Wait();
   Console.WriteLine(jobName + " finished");
}

Na start usługi mam

Job2 started
Job3 started
Job3 started
Job2 started
Job1 started
Job1 finished
Job3 finished
Job3 finished

Po zatrzymaniu usługi (po 5 sekundach) i uruchomieniu ponownie:

Job2 started
Job2 started
Job1 started
Job2 started
Job3 started
Job3 started
Job1 finished
Job3 finished
Job3 finished

To działa jeszcze gorzej niż to z czym mam problem.

@jacek.placek
Ja nie rozumiem w jaki sposób tego typu biblioteki jak RabbitMQ, Mosquitto czy inne MSMQ ma rozwiązać mój problem. Ja nie mam żadnej komunikacji pomiędzy aplikacjami. Ja nie potrzebuje informować niczego innego o stanie. Może mi ktoś to rozrysować na przykładzie jak niby ma mi to pomóc? Tego typu biblioteki w tym wątku przeważają więc rozumiem, że to jest najlepsze rozwiązanie ale nie rozumiem w jaki sposób miałby być rozwiązane.

0

Jeśli możesz uruchomić 2 lub więcej serwisów i każdy z nich może gadać z Optimaą to każdy serwis robi 1 zadanie i wysyła do kolejki info, żeby zrobić następne zdanie. Każdy serwis robi swoje. Masz kilka niezależnych aplikacji (winsevice), jednowątkowych, które uruchamiają swoje zadania po zakończeniu poprzednich zadań. I potem uruchamiają następne zadania wysyłając info do kolejki A jakiś kolejny serwis przechwytuje to z kolejki itd.
W miarę łatwe skalowanie, migracje itp ale chyba Ci to niepotrzebne.

0

Czy faktycznie są potrzebne do tego jakieś biblioteki?

W konfiguracji będę trzymał listę nazw usług w kolejności w jakiej chce je wykonać

List<string> serviceNames = new List<string>()
{
   "Service1",
   "Service3",
   "Service2"
}

W pętli uruchomię usługę i poczekam na jej zakończenie

List<ServiceController> serviceControllers = serviceNames.Select(x => new ServiceController(x));

if(serviceControllers.All(x => x.Status == ServiceControllerStatus.Stopped)
{
   foreach(ServiceController sc in serviceControllers)
   {
      sc.Start();
      sc.WaitForStatus(ServiceControllerStatus.Stopped);
      if(sc.Status == ServiceControllerStatus.Stopped)
          continue;
   }
}

A każda pojedyncza usługa miała by swoje zadanie i na koniec sama siebie by zakończyła

protected override void OnStart(string[] args)
{
    // tutaj sobie usługa coś robi...
    Stop();
}

W tym rozwiązaniu są dwa problemy:

  1. Instalacja usług - każda nowa usługa musi być ręcznie zainstalowana. Literówka w nazwie usługi powoduje wyjątki (wiadomo). Brak zainstalowanej usługi również wyjątki.
  2. Zatrzymanie głównej usługi może nie zatrzymać usług pośrednich jeśli ich status to StartPending. Zwyczajnie metoda Stop() na nich nie działa.

Na te chwilę punkt drugi został zaobserwowany robią prowizorkę typu Thread.Sleep() albo Task.Delay().Wait(). Nie było testowane na faktycznych operacjach na Optimie jeszcze.

0

No ale tu wszystko czeka na siebie nawzajem.
Nie ściągniesz kolejnego zamówienia jeśli nie zakończy się całe przetwarzanie pierwszego. WZ, FV, PWP to pewnie w sumie z kilkadziesiąt sekund.
W Mosquitto + serwisy jeśli zadanie będą różne czasowo to łatwo możesz mieć 1 serwis do ETAP1, 3 serwisy do ETAP2, 2 serwisy do ETAP3 itp. Łatwo dodawać kolejne lub odejmować bez kombinowania z nazwami.
Każdy serwis może nasłuchiwać konkretnej konkretnego ETAPU w Mosquitto czy Rabbit. Instalacja Mosquitto to tylko odpalenia instalatora (jeśli nie potrzebujesz dodatkowej konfiguracji zabezpieczeń, persystencji itp). Takie trochę mikro serwisy (raczej makroserwisy). Każdy serwis jest osobna aplikacją jednowątkową więc Optima powinna to przełknąć.

Trzeba by włączyć zapisywanie danych w Mosquitto, jest jakieś info w necie.

[edit]
Napisałeś: "ma tak dużo rzeczy do zrobienia, że w konkretnym interwale nie wyrabia". No to Mosquitto daje "asynchroniczność jednowątkową" :).
A nawet, jeśli to będzie do przełknięcia, wydelegowanie serwisów na inne maszyny (PC pracowników?).

0

Niektóre zadania są niezależne od innych albo nie potrzebują Optimy. Można by dorzucić parametr przy nazwie usługi, który informowałby główną usługę o pracy w tym samym czasie.

Klasa zawierająca informacje o usłudze:

    public class ServiceDef
    {
        public string Name { get; set; }
        public bool IsSameTime { get; set; }
    }

Ściągamy usługi do kolejkowania oraz usługi poza kolejką

List<ServiceController> serviceControllers = _appConfig.Services.Where(x => !x.IsSameTime).Select(x => new ServiceController(x.Name)).ToList();
List<ServiceController> sameTimeServiceControllers = _appConfig.Services.Where(x => x.IsSameTime).Select(x => new ServiceController(x.Name)).ToList();

Uruchamiamy wszystkie poza kolejką, a resztę kolejkujemy tak jak pisałem wyżej.

Teraz pytanie... Jakie korzyści daje mi Mosquitto jeśli nie zależy mi na asynchroniczności, ponieważ w tej kwestii blokuje mnie Optima? Równie dobrze mogę włączyć 5 usług co 15 minut i będą robić "puste przebiegi" jeśli inne usługi nie zrealizowały swojego zadania, ponieważ zapytanie SQL o konkretne dane nie zwróci wyniku.

Ja się oczywiście nie upieram. Zwyczajnie nie mam czasu (ochoty) uczyć się nowych rzeczy, zaś z drugiej strony mogę mieć coraz więcej takich projektów i chciałbym to robić dobrze.

0

W hangfire możesz sobie zrobić zadanie "matkę" po zakończeniu swojej pracy uruchamia kolejne i tak dalej (pisałeś, że zadania wykonujesz jedno po drugim więc to powinno działać) ale wtedy masz problem, że jeśli masz 4 zadania a zadanie 2 długo trwa to będziesz mieć takie coś:
Przebieg 1: Zadanie 1
Przebieg 1: Zadanie 2
Przebieg 2: Zadanie 1 (uruchamia się nowy przebieg zadania)
Przebieg 2: Zadanie 2
Przebieg 1: Zadanie 3
Przebieg 2: Zadanie 3
...

Możesz to rozwiązać tworzą sobie sprawdzenie, że jeśli zadanie danego typu jest już uruchomione to każde nowe zadanie oznacz jako wykonane (oczywiście zadania muszą wtedy korzystać z tych samych danych)

--

Możesz też w zadaniu matka od razu uruchomić 4 zadania i powiedzieć które ma się uruchomić po którym

BackgroundJob.ContinueJobWith(
    jobId,
    () => Console.WriteLine("Continuation!"));
0

No i właśnie do tego nie mogę dopuścić. Ustalam kolejność zadań, etapów np. zadanie 1 -> zadanie 3 -> zadanie 2. Póki pierwsze się nie skończy nie może zacząć się drugie, nawet jeżeli będzie trwało bardzo długo. A w kwestii nowego przebiegu zadania to to jest problem, który aktualnie próbuje rozwiązać, Właśnie z powodu bardzo długiego trwania jednego z etapów nie powinny uruchomić się etapy po nim i nie powinno dojść do nowego przebiegu.

1

Napisałem testowy program który zachowuje kolejność etapów z użyciem hangfire

class Program
    {
        static void Main(string[] args)
        {
            GlobalConfiguration.Configuration.UseMemoryStorage();

            RecurringJob.AddOrUpdate<ParentJob>("paretnJobId", x => x.Execute(), "*/1 * * * *");

            var opt = new BackgroundJobServerOptions()
            {
                WorkerCount = 1,
                Queues = new[] { "erp" },
                ServerName = "ERP"
            };

            for (int i = 0; i < 10; i++)
            {
                RecurringJob.Trigger("paretnJobId");
            }

            var defaultServer =  new BackgroundJobServer();
            using (var server = new BackgroundJobServer(opt))
            {
                Console.WriteLine("Hangfire Server started. Press any key to exit...");
                Console.ReadKey();
            }

        }
    }

    class ParentJob
    {
        static object _sync = new object();
        static int counter = 0;

        public void Execute()
        {
            lock (_sync)
            {
                counter += 1;

                var stage1 = BackgroundJob.Enqueue<Job>(x => x.Execute(counter, 1, 2));
                var stage2 = BackgroundJob.ContinueJobWith<Job>(stage1, x => x.Execute(counter, 2, 1));
                var stage3 = BackgroundJob.ContinueJobWith<Job>(stage2, x => x.Execute(counter, 3, 1));
                var stage4 = BackgroundJob.ContinueJobWith<Job>(stage3, x => x.Execute(counter, 4, 1));
            }
        }
    }

    [Queue("erp")]
    class Job
    {
        public void Execute(int parentId, int stage, int wait)
        {
            Task.Delay(TimeSpan.FromSeconds(wait)).Wait();
            Console.WriteLine("ParentId: {0} Stage: {1}. Time: {2}", parentId, stage, DateTime.Now);
        }
    }

Edit:
Jeśli "ParentJob" będzie napisany z lock to nakładanie na siebie etapów nie będzie miało miejsca. Jeśli się go usunie to przeplatanie etapów z przebiegów może wystąpić :(

0
AdamWox napisał(a):

No i właśnie do tego nie mogę dopuścić. Ustalam kolejność zadań, etapów np. zadanie 1 -> zadanie 3 -> zadanie 2. Póki pierwsze się nie skończy nie może zacząć się drugie, nawet jeżeli będzie trwało bardzo długo.

W takim razie potrzebujesz szybszego sprzętu, inaczej - bez zmiany architektury aplikacji - nic z tym nie zrobisz. Możesz to obdrutować pauzowaniem quartza na czas przetwarzania tyknięcia, ale to chyba w ogóle nie tędy droga. Skoro masz interwał pół godziny i czasem się nie wyrabiasz, to pominięcie kolejnego interwału spowoduje, że jeszcze kolejny będzie miał dwa razy więcej przetwarzania. I tak co drugi interwał będzie się "nudzić", a co drugi będzie zasuwał przez godzinę z hakiem.
Ja na Twoim miejscu zrobiłbym PoC zrównoleglenia części operacji oraz zmienił sposób startu przetwarzania:

  • w PoC sprawdzasz, które części procesu można zrównoleglić. Z tego co na szybko ogarnąłem w internecie Optima może działać wielowątkowo, możesz mieć problemy z konkretnymi rzeczami (np. zakleszczające się transakcje w bazie danych), ale da się to ogarnąć przez sprawdzenie co można z czym łączyć. Nie rozumiem, dlaczego tak się przed tym bronisz, skoro jedyną alternatywą jest kupno dużo mocniejszego sprzętu, który jedynie odsunie problem w czasie.
  • kolejne przetwarzanie powinno być zakolejkowane przez poprzednie z interwałem nie większym, niż te Twoje pół godziny, czyli jeśli wyrobiłem się w 3 minuty, to następna iteracja startuje za 27 minut, jeśli wyrobiłem się w 25 minut, to następna iteracja idzie za 5 minut, jeśli wyrobiłem się w 30+ minut, to następna iteracja startuje od razu.

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