Pure FP Webflux i autoryzacja

0

Hej, piszę swój pierwszy czysto funkcyjny serwer w Webfluxie. Obejrzałem kilka prezek i githubów, ale chcę rozwiać wszelkie wątpliwości.

  1. Jeśli nic nie skonfiguruję to domyślnie Netty będzie obsługiwał żądania na 1 wątku ? Dobrą praktyką jest ustawić tyle wątków ile obsługuje procesor czy są inne podejścia ? Są jakieś wrażliwe miejsca, w których muszę pamiętać, że żądania współdzielą wątek i martwić się o race conditions itd ? Pytam o przykre doświadczenia ludzi, którzy więcej tak pisali. Generalnie czy takie rozwiązanie z nieblokującą architekturą jest słuszne produkcyjnie ?

  2. Autoryzacja robione przez high order function. Z tego co zrozumiałem zamysł jest taki, że user loguje się, z bazki userów jest on wyciągany po loginie i sprawdzane jest czy zahashowane hasło różni się z tym jego i jeśli tak to generowany jest JWT, który również gdzieś zapisujemy do in memory ConcurrentHashMapy jako klucz i wartość czyli dane sesji (id usera, wygaśnięcie). Security high order function wygląda tak, że z z headera jest token wyciągany, sprawdzane jest czy sesja w tej concurrent hash mapie istnieje i ewentualne sprawdzenie czy nie wygasła ? Czy to jest też produkcyjnie ok?

1
Bambo napisał(a):

Hej, piszę swój pierwszy czysto funkcyjny serwer w Webfluxie. Obejrzałem kilka prezek i githubów, ale chcę rozwiać wszelkie wątpliwości.

  1. Jeśli nic nie skonfiguruję to domyślnie Netty będzie obsługiwał żądania na 1 wątku ? Dobrą praktyką jest ustawić tyle wątków ile obsługuje procesor czy są inne podejścia ?

WebFlux sam z siebie jest nakładką na SpringBoota - jak masz pod spodem Tomcata, to zależy on od konfiguracji Tomcata. Jak masz pod spodem Reactora, to zależy on od Reactora. Reactora ma trochę inny model pracy niż tradycyjne serwery aplikacji - zamiast iluś tam wątków tworzy sobie pod spodem tzw. Worker Thready, do których trafiają zadania (coś trochę jak system agencki). Jest ich chyba tyle, co procków z defaultu.

Są jakieś wrażliwe miejsca, w których muszę pamiętać, że żądania współdzielą wątek i martwić się o race conditions itd ?

Race conditions będzie raczej problemem gdy wskoczysz do swojego kodu, tj. jeśli napiszesz architekturę tak, że będziesz miał wspólne zasoby nie będące thread-safe. Tzn. wszelkiego rodzaju mutowalne static'i oraz repozytoria typu baza danych itp. Tych pierwszych unikaj, te drugie należy dodatkowo zabezpieczać.

Pytam o przykre doświadczenia ludzi, którzy więcej tak pisali. Generalnie czy takie rozwiązanie z nieblokującą architekturą jest słuszne produkcyjnie ?

IMO tak, tzn. nigdzie jeszcze nie spotkałem się z większymi problemami z tym. Spotkałem się za to z problemami przy np. Weblogic'u, przy dosyć niskich liczbach użytkowników. Jeśli nie planujesz robić naprawdę dużego serwisu to konfiguracja domyślna powinna ogarnąć temat, jeśli chodzi o większy serwis to widzę pewien problem o którym wspomniałem dalej.

  1. Autoryzacja robione przez high order function. Z tego co zrozumiałem zamysł jest taki, że user loguje się, z bazki userów jest on wyciągany po loginie i sprawdzane jest czy zahashowane hasło różni się z tym jego i jeśli tak to generowany jest JWT, który również gdzieś zapisujemy do in memory ConcurrentHashMapy jako klucz i wartość czyli dane sesji (id usera, wygaśnięcie). Security high order function wygląda tak, że z z headera jest token wyciągany, sprawdzane jest czy sesja w tej concurrent hash mapie istnieje i ewentualne sprawdzenie czy nie wygasła ? Czy to jest też produkcyjnie ok?

Pytanie, co zamierzasz z tym zrobić. Bo np. jeśli ta HashMapa będzie zbyt duża to skończy ci się pamięć, dodatkowo takie rozwiązanie jest raczej słabo skalowalne (zakładasz istnienie jednej instancji - bo przy dwóch musisz jakoś te tokeny synchronizować).

3

Ja tak piszę i popularyzuję. Webflux czysto funkcyjny jest dla mnie mniejszym złem niż klasyczny Spring. Ale

ad 1 . To domyślnie nie będzie jeden wątek, ale chyba tyle co masz rdzeni - zapomniałem już co dokładnie. Da się to ustawiać.
(tu dla zabawy ustawione 300 wątków - https://github.com/javaServerFun/betterBadFibonacci/blob/full/src/main/kotlin/pl/setblack/fibo/FiboServer.kt)

Zasadniczo 1 watek na rdzeń ma sens (z punku widzenia low latency i efektywnościoprzetwarzania), ale są wyjątki.
Wyjątki są takie jeśli część twojej architektury jest jakoś blokująca (np jdbc). Wtedy może być tak, że warto mniej - jęsli blokującytm zadaniom wydzelisz inną pulę wątków, albo więcej - jesli czasem blokujące zadanie poleci z puli netty (niby się nie powinno tego robić..ale bywa i działa).
(btw. widziałem całkiem krytyczną aplikację, która naprawdę grube operacje na jdbc robi blokujaco na wątkach netty i (to straszne, ale działa) bez problemu- podwyższyli chłopaki po prostu liczbę wątków i zrobili z netty tomcata :-) ).

Produkcyjnie WebFLux mi działa, ale akurat mam dość specyficzne systemy - nie mam nigdzie WebFluxa ostro online obciążonego, a load testy wiadomo, że świnie. (Mam całkiem niezłą produkcję na gołym netty i javie, ale to inna historia).

Mam inny wniosek - dotyczący generalnie programowania reaktywnego, kóre się z takim WebFluxem uprawia:
** java się do tego zupełnie nie nadaje** na większą skalę trudno utrzymać kod czytelnym (da się, ale strasznie to męczące).
Kotlin nadaje się prawie (to uprawiam).

ad 2. z JWT generalnie nic nie musisz cachować w ConcurrentHashMap, możesz za każdym razem po prostu sprawdzać podpis. ale możesz dla "szybklości" taki cache zrobić (tak się robi).

1

Dzięki za odpowiedzi.

@jarekr000000 piszę w Kotlinie, a nie w Javie.

Jeszcze czegoś tu nie rozumiem .. jeśli robię WebFluxa jako aplikację Spring Bootową - normalnie z adnotacjami to mimo wszystko wstaje mi Netty. Wtedy mimo wszystko mam architekturę nieblokującą ?

Chcę to napisać bez adnotacji i beanów z tego względu, że generalnie odkąd dowiedziałem się o czymś takim jak Spring (i generalnie o 1 frameworku webowym w moim życiu) oraz JPA zastanawiałem się gdzie tu jest w tym wszystkim ułatwienie skoro tak naprawdę nie wiadomo co się dzieje (rozkminiałem to zanim zacząłem oglądać konfy). Konfiguracja security w beanie to dla mnie ostra patola, generalnie wolę mieć wszystko napisane i widoczne w kodzie a nie zgwałcone gdzieś przez framework.

Z tymi pytaniami o zastosowania tego produkcyjnie chodziło mi o to czy taki serwerek będzie się sprawdzał przy aplikacji społecznościowej, która może mi się rozrastać. Póki co ma planowany ruch na max 50 userów, ale kto wie co dalej :D Chciałbym przewidzieć jakie problemy i trudności może mi sprawić napianie tego w taki czysty sposób bez całych bajerów springowych, bo zakładam, bo miimo wszystko o więcej rzeczy muszę się martwić niż przy klasycznym podejściu - chociażby thread safe,

4

Po to stworzono architektury nieblokujące, aby wytrzymać bardzo duży ruch.
Ale czy faktycznie wytrzyma zależy od tego czy:

  • chłopaki od WebFlux czegoś nie zrypali - raczej nie. To Spring, mam zaufanie do ich jakości zasadniczo - nawet jak coś spsują to naprawiają w mniej niż 2 lata.
  • czy ty w swojej aplikacji czegoś nie zepsujesz - no i tu zależy. Niestety kotlin i funkcyjne podejście Cię nie chronią, aż tak bardzo jak by się chciało.

Chcesz mieć bezpieczeństwo -- napisz testy obciążeniowe i w miarę jak aplikacja będzie używana weryfikuj ich miarodajność ( np. sprawdzaj statystykami co najczęściej robią użytykownicy).

0

A takie funkcyjne endpointy łatwo udokumentować jakimś swaggerem ?

2

Swagger wspiera WebFluksa podobno dopiero od wersji 3.0.0, która obecnie jest w snapshocie. Możesz użyć https://stackoverflow.com/questions/55138027/spring-rest-docs-with-junit-5-and-webflux

0

Dzięki. Tak jeszcze jak to piszę to zastanawiam się jak łączyć Vavra i Mono/Fluxy .. No bo bez sensu, żeby serwisy czy repo zwracały Mono<Option<T>> chyba nie ? :D Jak to ogrywacie ? Either jeszcze rozumiem bo mamy powód, ale mimo wszystko takie Mono<Either<Error, Object>> wygląda dziwnie

0

Ja bym już się trzymał Reactora i programowania reaktywnego.

1

Co do zwracania Mono<List<Option<Either<Srajder< .itd. tojakimś rozwiązaniem są typealiasy. Masz kotlina - używaj. Dodatkowa zaleta to możliwość szybszego refaktoringu (wiadomo gdzie taki typ specyficzny jest potem używany).

Choć nie ukrywam, że wymyslanie nazw na Mono<Either<E,Pies>> też jest wyzwaniem - PiesSource, PiesChance ?IsPies, ?

Vaughn Vernon (ten od DDD) strasznie się swego czasu wkurzał na to Mono i proponował jeszcze większsego raka IMO (jakieś tam Continuable czy coś).
Ze względu na składanie wyników Mono<CośTam> jest całkiem spoko w serwisach. Ludzie niech sie przyzwzajają do flatMapowania :-)

Jak robisz coś bardziej jak bibliotekę to możesz wystawiać Publishera.

2

moglibyście podać jakis przykład takiego projektu w spring 5 który nie używa adnotacji? chodzi wam o zupełne nieużywanie kontenera zalezności w kontekscie wstrzykiwania przez adnotacje i ogólnie żadnych adnotacji?

chciałbym sobie coś takiego w springu przejrzeć :P @jarekr000000 może ty?
jakiś github/prezentacje?

1

Tu masz czyjś - przynajmniej jest jakoś opisany:
https://github.com/making/vanilla-spring-webflux-fn-blank

ja zawsze wrzucam swojego ratponga, w którym jest nieco więcej kodu
https://github.com/javaFunAgain/ratpong

Tylko on nie używa spring Webfluxa, (tylko ratpacka),
ale z WebFluxem kod byłby prawie taki sam (byłby minimalnie lepszy , API jest fajniejsze niż to ratpackowe).
(tak czy siak, nie chciało mi się na springa przepisywać)

0

Walczę z połączeniem Mono i Either od kilku h i nie mogę za cholerę pożenić operacji pobrania i zapisu encji z wykorzystaniem webfluxa i vavra:

data class Val1(val value: String) {
    companion object {
        fun of(str: String): Either<Error, Val1> {
            //
        }
    }
}

data class Val2(val value: String) {
    companion object {
        fun of(str: String): Either<Error, Val2> {
            //
        }

}

data class Entity(val id: String) {

}
    fun update(val1: Val1, Val2): Entity {
        //
    }

    fun dto(): EntityDto {

    }
}

interface Repo {

    fun load(id: String): Mono<Entity>

    fun save(entity: Entity): Mono<Entity>>
}

class Service {

    fun update(entityId: String, str1: String, str2: String): Mono<Either<Error, EntityDto>> {
        val val1: Either<Error, Val1> = Val1.of(str1)
        val val2: Either<Error, Val2> = Val2.of(str2)
        val entity: Mono<Entity> = repo.load(entityId)
        
        // TODO save i zwrocenie dtosa
    }
}

Ktoś chętny ?

2
  fun update(entityId: String, str1: String, str2: String): Mono<Either<Error, EntityDto>> =
            repo.load(entityId).flatMap { entity ->
                Val1.of(str1).flatMap { val1 ->
                    Val2.of(str2).map { val2 ->
                        entity.update(val1, val2)
                        repo.save(entity)
                                .map { savedEntity ->
                                    Either.right<Error, EntityDto>(savedEntity.dto())
                                }
                    }
                }.mapLeft { Mono.just(Either.left<Error, EntityDto>(it)) }
                        .getOrElseGet { it }
            }

żeby to lepiej wygladało to

fun Either.merge() =  this.getOrElseGet { it } 

 fun <E,T> Mono.left(v:T) = Mono.just(Either.left<E, T>(v))
0

Ah jak ja uwielbiam te dyskusje :D Z tego forum się nauczyłem 100x więcej niż z reszty internetu i ksiażek :D

@jarekr000000 dzięki, ten malLeft naprawił sprawę, kombinowałem z samymi mapami i flatMapami.
Generalnie zdaję sobie sprawę, że takie przypadki pojawiają się często i występują pewnie w jeszcze zawiłej formie. Masz jakiś patent na łączenie tego czy po prostu kwestia posiedzenia z mapami i flatMapami ? Bo często dojście z opakowanym podwójnie w jakieś Mono i Eithery obiektem, a zakładam, że mogą się pojawić po3opakowane jest nietrywalnym problemem. Wczoraj nawet zacząłem to sobie na kartce rysować i opakowywać kółeczka w kwadraciki i romby XD.

Ułatwiłem sobie w ten sposób:

enum class AppError {

    CANNOT_PARSE_DATE,
    CANNOT_PARSE_CURRENCY,
    CANNOT_PARSE_PRICE;

    fun <T> toEither() = Either.left<AppError, T>(this)

    fun <T> toMono() = Mono.just(toEither<T>())

}

I czy ta metode merge() nie powinna wyglądać tak:

fun <T> Either<AppError, T>.merge() = this.mapLeft { it.toMono<T>() }
        .getOrElseGet { this.get() }

Bo ta Twoja to się nie kompiluje :D

EDIT: A nie tak nie może wyglądać bo ten get na końcu ..

Ale to poniżej się w ogóle nie kompiluje:

fun <T> Either<AppError, T>.merge() = this.mapLeft { it.toMono<T>() }
        .getOrElseGet { it }

EDIT2
Czy testując jednostowko serwisy zwracające Mono<Either>> nie jest super błędem walić na mono block() i eitherze get() ? :D

1

Jak sobie radzić:

  • typealiasy pomagają,
  • używam w miarę możliwości tylko wyrażeń, instrukcje won, wtedy praktycznie każda linijka musi mieć jakiś typ i musi się to zgadzać, (prawie - bo czasem deklaruje zmienne (val))
  • wtedy ratuje mnie kompilator i podpowiadanie typów (show type hints)

Co do get i block w testach. Jest taka jedna zasada, że niby kod w testach nie może być gorszej jakości niż ten w normalnym kodzie,
skoro w normalnym kodzie get to błąd to i w testach też. Ale całkiem 100% nie jestem przekonany do tej zasady - za dużo kosztuje czasem i mi zniechęca ludzi do testów :-(

Co do block() to masz reactor-test i polecam, (choć nie jest idealny, ma zwodnicze api trochę),
Co do get() - to najlepiej by napisać asercje, matchery, własne na to (czyli mieć get w jednym wyjątkowym miejscu, we frameworku).
Ale prawda czasu jest taka, że zawsze powtarzam sobie, że trzeba by... a tymczasowo robię w testach Either.get()... i tak już zostaje.

0

Zrobiłem coś takiego:

    fun <R> assertMonoEitherRight(mono: Mono<Either<AppError, R>>, asserts: (r: R) -> Unit) {
        assertNotNull(mono)
        val either = mono.block()
        assertTrue { either!!.isRight }
        asserts(either!!.get())
    }

    fun <R> assertMonoEitherLeft(mono: Mono<Either<AppError, R>>, asserts: (appError: AppError) -> Unit) {
        assertNotNull(mono)
        val either = mono.block()
        assertTrue { either!!.isLeft }
        asserts(either!!.left)
    }
2

To merge wyżej pisałem oczywiście bez kodowania - wiec sie walnąłem - ma być:
fun <T> Either<T, T>.merge() = getOrElseGet { it }

Kluczowe jest <T,T> najpierw sprowadzamy lewą i prawą stronę do tego samego - a potem merge wybiera jedną z nich - czyli zostajemy z T.
(kocnept merge ukradłem ze ScalaZ - sprawdzał tam mi się).

0

@jarekr000000:
Dzięki, wszystko działa zajebiście :D napisałem już cały core pure FP, z ręcznym DI. Wygląda wszystkie zajebiście ! Robię demko, żeby propagować wiedzę w robocie, więc wstawię tu jak zrobię wsio w pełni.

Jeszcze tylko taka sprawa: wiem, że trzymasz kolekcje vavra bezpośrednio w encjach. Przy zapisie im memory wiadomo nie ma z tym problemu. A z jakimś mongo ? Próbowałem, ale przy odczycie encji coś się wywalało.

Robienie wartswy dbosów wydaje mi się średnim pomysłem.

0

Mam vavra w modelu i czasem przeszkadza przy serializacji. Nie kojarzę jak to jest z mongo (za dawno coś w tym robiłem, nawet nie wiem czy używałem wtedy coś jak vavr).
Przy serializacji do json zwykle można podać własne serializaery.
Np. w takim Jacksonie używam tego:
https://github.com/vavr-io/vavr-jackson

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