Mindfuck z CompletableFuture.

0

Dobry wieczór, postanowiłem że w ramach przypomnienia/poćwiczenia operacji asynchronicznych, przepisze sobie zwykłą metodę w serwisie na asynchroniczną. Jak ona wygląda i w czym jest problem:

public Activity addActivityToActivityList(AddActivityCommand addActivityCommand) {

        // Wypisuje normalnie uzytkownika powiązanego z Listą Aktywności - więc lista jest normalnie znajdywana
        System.out.println(activitiesListRepository
                .findById(addActivityCommand.getActivitiesListId())
                .get()
                .getUser()
                .getUsername());

        // Optional wyrzuca mój wyjątek - nie może znaleźć listy, mimo tego, że u góry to samo działa
        CompletableFuture<ActivitiesList> owningActivitiesList = CompletableFuture.supplyAsync(
                () -> activitiesListRepository
                        .findById(addActivityCommand.getActivitiesListId())
                        .<ActivitiesListNotFound>orElseThrow(ActivitiesListNotFound::new)
                        // ^^ btw. bez jawnego zadeklarowania typu wyjątku  w diamond operator javac nie chciało mi kompilować 
        );

        // Raczej nieważne rzeczy, dalsze asynchroniczne działania
    }

Co robi - persystuje obiekt Activity, mając parę danych i id *Listy Aktywności *, z którą ma być powiązany.
W czym problem - normalnie, bez żadnych asynchronicznych działań wszystko działa. Gdy wrzucę tą samą metodę do CompletableFuture, nagle obiekt nie może być znaleziony.
Test integracyjny w Junit5, wcześnie persystuje obiekty używając TestEntityManager:

    @Test
    void shouldAddNewActivityToActivitiesList() {
        //given
        activitiesServiceImpl.addNewActivitiesListToUser( new CreateNewActivitiesListCommand(user.getId()));
        List<ActivitiesList> activitiesList = user.getActivitiesList();
        // when
        // Sprawdzalem czy testEntityManager znajduje obiekt - znajduje
        System.out.println(testEntityManager.find(ActivitiesList.class, activitiesList.get(0).getId()));

        activitiesServiceImpl.addActivityToActivityList( new AddActivityCommand(
                activitiesList.get(0).getId(),
                testActivityType.getId(),
                0,
                0
        ));
        // then
        assertEquals( 1, user.getActivitiesList().get(0).getActivities().size(),
                "Activity weren't persisted" );
    }

Repositorium to czyste repozytorium Spring Data -

public interface SpringDataActivitiesListRepository extends ActivitiesListRepository, JpaRepository<ActivitiesList, Long> {}

Serwis ma adnotacje:

@Service
@Transactional

Klasa testowa :

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureTestEntityManager
@Transactional

Szczerze powiedziawszy to nie mam nawet pomysłu jaki może być problem - pewnie coś z JPA/Hibernatem, ale nie potrafię powiedzieć. Próbowałem robić * flush()* i clear() na testEntityManager, ale nie pomaga, dalej to samo.

TLDR: Ta sama metoda w wersji "po kolei" znajduje zapisany obiekt bez problemu. Po wprowadzeniu asynchroniczności, obiekt nie może zostać znaleziony. Jakim cudem obiekt w supplyAsync() nagle przestaje być persistent?

1

Bo operujesz na różnych wątkach ? pewnie context @Transactional jest przypisany do wątku, a supplyAsync wykona się gdzieś indziej - a jako że autorzy @Transactional i całego tego AOP wokół zapisują stan gdzieś w ThreadLocal storage gubisz wpisy czy tam odczyty.

2

Spring niestety nie jest kompatybilny z javą :-)

W Springu masz do takich rzeczy kolejną adnotację najłatiwej to chyba z : @Async .

Ale najlepiej olej Springa i zacznij programować w Javie. Wtedy CompletableFuture będzie działał.

Mam na ten temat nawet wykład:

0

Szukając rozwiązania przeczytałem o @Async - jednakże chciałem użyć znanego mi CompletableFuture.

@rubaszny_karp:

W jakim sensie gubię? I dlaczego jeden wątek (tutaj powiedzmy normalny, ten pierwszy), znajduje obiekt, a nowy (stworzony chyba potem...?) już nie? Obiekty do których ten serwis się odnosi (User, ActivitiesList) są persystowane w klasie testowej, więc chyba jeszcze w innym wątku?
Szczerze powiedziawszy nie rozumiem.

@jarekr000000
Dobra, kompatybilność Javy i Springa mnie rozbawiła :D.

Spróbuje jutro z @Async, ale nie wiem czy to będzie tego warte.

Ewentualnie - gdzie mogę poczytać o zależnościach między Springiem a klasami Javowymi jak Future/CompletableFuture/ExecutorService? A co w ogóle z RxJava i Springiem?

0

Dobra, poczytałem trochę a tych zagadnieniach. Znalazłem coś takiego: https://stackoverflow.com/questions/45473215/how-to-make-a-async-rest-with-spring , wypowiedź @rubaszny_karp pokrywa się z tym:

The response body is blank because the @Async annotation is used at findEmail method of UserRepository class, it means that there is no data returned to the following sentence User user = userRepository.findByEmail(email); because findByEmail method is running on other different thread and will return null instead of a List object.

Ale ja mam pytanie - z czego wynika, że na różnych wątkach repozytorium zwraca różne wyniki? Z JPA, z Hibernate'a, ze Springa, z poziomów izolacji BD, czy z czego?

1

Jesli korzystarz ze Springa aspectowego i ORM to nie powinenes korzystac z własnych Executor Servisów i tym podobnych, tylko powierzyć to Szpringowi, ponieważ Spring do zarządzania transakcjami wykorzystywuje ThreadLocale

0

@scibi92:
Aaaaaa, no to by miało sens w takim razie. Trochę głupie.

0

Ale ja mam pytanie - z czego wynika, że na różnych wątkach repozytorium zwraca różne wyniki? Z JPA, z Hibernate'a, ze Springa, z poziomów izolacji BD, czy z czego?

To się nazywa persistence context - w nim są trzymane wszystkie zmiany które wykonujesz w danej metodzie, potem dopiero, kiedy zakończysz metode, te zmiany są flushowane fizycznie do bazy (a pewnie może być jakiś cache on top) - ten context, otwiera i zamyka spring przez AOP (aspect oriented shyyyt).

Więcej nie wiem bo w sumie nigdy nie korzystałem, całe życie #nosql XD

0

@rubaszny_karp:
Tak własnie myślałem, ale to chyba nie to - można samemu wywołać metodę flush() na (test)EntityManagerze i gdy tak robiłem w metodzie testowej, to nie pomagało. Chyba że powinienem to zrobić gdzieś indziej, w innym wątku, no ale nie wiem.

0
AndrzejAd napisał(a):

@rubaszny_karp:
Tak własnie myślałem, ale to chyba nie to - można samemu wywołać metodę flush() na (test)EntityManagerze i gdy tak robiłem w metodzie testowej, to nie pomagało. Chyba że powinienem to zrobić gdzieś indziej, w innym wątku, no ale nie wiem.

powinieneś to zrobić w metodzie która robi zmianę i przed tym kiedy tworzysz zadanie async w które wykona się w innej metodzie, ale jaką masz pewność że driver jdbc szybciej wrzuci zmiany do bazy niż asynchroniczny task zostanie wykonany ?

0

@AndrzejAd: no to nie jest takie głupie. Frameworki takie są że cię ograniczaja i narzucają architekture, ale z 2 strony ułatwiają ci pisanie kodu. Nikt na ogół nie wpada na pomysł odpalania transakcji w jakiś executor service'ach, większośc woli to zrobić klasycznie per request. Dzięki tej całej magi wystarczy dodac adnotacje @Transactional i działa w 99,99% przypadkow. Oczywiście nie każdy tak lubi, ale nie musi wtedy korzystać z frameworków do ORM albo programowania aspektowego.

0

@scibi92:
Tak wiem, po coś to zostało zrobione.

@rubaszny_karp:
Rozumiem w czym problem, ale:

  1. Czemu flush() w metodzie testowej w takim razie nie pomagał? Czy on od razu nie powinien powiedzieć Hibernatowi "koniec zwlekania, persystuj te obiekty"? W tym moim serwisie nie robię żadnych zmian, tylko odczytuje dane.
  2. Czy to, że postępując sekwencyjnie (tzn. pierwszy System.out.print() ), udało mi się odszukać obiekt w bazie nie oznacza, że driver zdążył to zrobić? Czy chodzi o to, że ten wątek async który stworzyłem działa zupełnie poza Springowym czasem i przestrzenią, przez co tak naprawdę nie wiem kiedy ForkJoinPool to wykona?
1

ten wątek async który stworzyłem działa zupełnie poza Springowym czasem i przestrzenią,

Dokładnie.

Dodatkowo inne uwagi:

Z tym RxJava i Springiem to trochę bez sensu - jak od wersji Spring 5 **projetreactor ** jest wspierany defaultowo.
Ponieważ RxJava i projectreactor to w zasadzie odpowiedniki - to w Springu chyba lepiej wybrać projectreactor.

Przyznaję się do stosowania Spring 5 i nawet polecania.
Ale jest to Spring 5 bez annotacji, beanów, aspektów, @Transactional i innych badziewi, które nie działają z Javą.
W Spring 5 można ich używać, tylko nie wiem po co, mi niewiele ułatwiają, a wręcz utrudniaja - np utrudniają wiarygodnie Testowanie, i przede wszytkim utrudniają rozumienie tego co robi kod,
Twój przykład jest w miarę typowy.

0

Ok, dzięki wszystkim za odpowiedzi.
Szczerze powiedziawszy to na razie poddaje się, skorzystałem z @Async (przebudowując dosyć mocno klasy), ale dalej nie działa. Zmiany na razie zostaną na osobnej branchy, wrócę do nich kiedy indziej, może gdy będę bardziej ogarniał Hibernate'a, bo z tym też pojawiały się problemy.

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