This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-10-12
Channels
- # aleph (61)
- # announcements (2)
- # babashka (65)
- # beginners (64)
- # calva (2)
- # clerk (1)
- # cljsrn (1)
- # clojure (60)
- # clojure-austin (7)
- # clojure-europe (13)
- # clojure-italy (2)
- # clojure-losangeles (4)
- # clojure-nl (2)
- # clojure-norway (94)
- # clojure-romania (2)
- # clojure-uk (7)
- # clojuredesign-podcast (5)
- # clojurescript (3)
- # core-typed (2)
- # datomic (42)
- # docker (24)
- # emacs (10)
- # exercism (50)
- # graphql (83)
- # honeysql (25)
- # hyperfiddle (12)
- # malli (13)
- # membrane (49)
- # off-topic (50)
- # podcasts-discuss (1)
- # re-frame (3)
- # reagent (12)
- # reitit (5)
- # releases (2)
- # remote-jobs (8)
God morgen, venner! Dagens spĂžrsmĂ„l: Om web servere. NĂ„ bruker jeg Ring+Jetty fordi det var enkelt. Men er det bra nok for produksjon til en intern app med maks 500 aktive brukere? Jeg fant https://ericnormand.me/mini-guide/clojure-web-servers, som ble skrevet i 2019 og sist oppdatert i mars 2021. Den guiden frarĂ„der https://github.com/http-kit/http-kit, som andre har anbefalt meg tidligere (@teodorlu nevnte det for leden dag): > [âŠ] because it is a fresh implementation (with no dependencies!), it suffers from a small pool of contributors. It doesn't get the attention it needs to keep up with bugs and features like the Java-based libraries do. It isn't as reliable and it isn't as fast. There's really no need to use HTTP Kit now. It filled a need in the Clojure world for a while, but that need is now better served by Java-based solutions with good Clojure wrappers. Er den kritikken av HTTP Kit (fortsatt) gyldig, og har dere noen andre synspunkter rundt hva en bĂžr tenke pĂ„ nĂ„r man velger HTTP server?
Fortum bruker http-kit med over hundre tusen brukere, sĂ„ gjetter pĂ„ at det skal gĂ„ greit med 500. đ
http-kit er lettere Ä jobbe med om du skal ha async-ting. Den bruker ogsÄ fÊrre trÄder enn jetty om jeg ikke tar helt feil. Vi fikk ihvertfall mer ut av hver prosess da vi bytta fra jetty til http-kit
Jeg ser at Ring+Jetty ikke stÞtter WebSockets og streaming-aktige fasiliteter, men det ser ut som at HTTP Kit gjÞr det. PÄ sikt har jeg litt lyst til Ä koble opp appen min til Kafka, og pushe data fra f.eks. Kubernetes inn i en Kafka topic, som jeg lytter pÄ fra Clojure back-end og pusher ut til ClojureScript klienten i "sanntid." Bare tidlige og lÞse tanker, kanskje dumt.
I og med at Kafka er skrevet i Java, tipper jeg det er relativt enkelt Ă„ bruke fra Clojure đ Nesten alle systemer vi har prater med hverandre via Azure Service Bus eller Kafka.
Evt. om min Clojure back-end henter ut data fra f.eks. Kubernetes og dytter det inn i Kafka, som min ClojureScript klient lytter pÄ. Da kan jeg (mis)bruke Kafka som en "database."
Det er trivielt Ä bytte mellom jetty og http-kit, sÄ jeg ville ikke brukt sÄ mye tid pÄ det nÄ. Du kan streame med jetty, men da mÄ alle handlerne og middlewarene dine implementere en egen arity for async, det er skikkelig sÞl. Http-kit har et mer pragmatisk API for Ä ha noen async endepunkter
http-kit fĂ„r min stemme, generelt greit Ă„ unngĂ„ de âthread firstâ-serverene fra gamle dager, som Jetty og Tomcat. Men tror ikke jeg har jobbet pĂ„ noen prosjekter hvor det ville fĂžrt til problemer Ă„ bruke Jetty đ€·
Haha, ja, det fÄr man si var en triviell endring! Samme eksempel fra i gÄr, men nÄ med HTTP Kit:
(ns core
(:require [org.httpkit.server :as hk-server]))
(defonce server (atom nil))
(defn say-hello [name]
(str "Hello, " name "!"))
(defn app [req]
{:status 200 :body (say-hello "Leif") :headers {}})
(defn stop-server []
(when-some [s @server]
(s :timeout 100)
(reset! server nil)))
(defn start-server []
(stop-server)
(reset! server (hk-server/run-server app {:port 3001})))
(defn -main []
(start-server))
(comment
(start-server)
(stop-server))
Jeg liker spesielt dette:
FĂžr:
(defn start-server []
(stop-server)
(reset! server
(jetty/run-jetty (fn [req] (app req))
{:port 3001 :join? false})))
Etter:
(defn start-server []
(stop-server)
(reset! server (hk-server/run-server app {:port 3001})))
Ah! Nei, det var kanskje dumt. Jeg mÄ vel ha en funksjon der inne for Ä kunne endre app uten Ä mÄtte restarte serveren.
eneste som typisk kan vĂŠre annerledes er at objektet man fĂ„r i body i diverse settinger kan vĂŠre forskjellig. Typ om du laster opp en fil eller gjĂžr noe annet ârartâ, da er det typisk en Jetty-klasse eller en http-kit-klasse man fĂ„r inn der
Jepp! Det gikk, @teodorlu
(defn start-server []
(stop-server)
(reset! server (hk-server/run-server #'app {:port 3001})))
Tror det er shorthand for (var app). Vars kan brukes dirkete som funksjoner. Men da slÄr du opp "hva er dette definert som nÄ?" fÞr hvert funksjonskall.
ville vel nesten argumentert for Ă„ bare bruke var
i stedet for, litt lettere Ă„ slĂ„ opp i dokumentasjon osv đ
Tror jeg fÞrst lÊrte dette av Ä lese dokumentasjonen til http-kit, men den ser ikke ut til Ä vÊre tilgjengelig pÄ nett lenger. http://Http-kit.org peker til noe annet.
#' er var-quoting, ja. Samme som var
. Grunnen til at du vil bruke denne er at du da snakker om referansen, ikke selve verdien. Den praktiske forskjellen er at (do-thing my-fn)
fÄr en konkret implementasjon av funksjonen i my-fn
, sÄ om du reevaluerer definisjonen dens i REPL-et vil ikke do-thing
kjenne til den nye versjonen. Dersom du heller gjĂžr (do-thing #'my-fn)
sÄ vil do-thing
fÄ var-en/referansen i stedet, og hver kan den kaller den som en funksjon sÄ ruter Clojure frem den gjeldende verdien til var-en.
Basically: Hvis du vil ha et REPL med fungerende reload sÄ mÄ du var-quote navngitte funksjoner som sendes rundt.
@U9MKYDN4Q godt formulert! Jeg prĂžvde Ă„ si det samme, men fĂžlte ikke helt at jeg traff đ
Kult, fant ut at jeg kan bruke ring.middleware.params
for Ă„ parse URI params:
(defn app [req]
(case (:uri req)
"/greet" {:status 200
:headers {"Content-Type" "text/plain"}
:body (greet (get-in req [:params "name"]))}
{:status 404
:headers {"Content-Type" "text/plain"}
:body "Not found."}))
(defn start []
(stop)
(reset! server (hk/run-server (wrap-params #'app) {:port 3001})))
@U04V5VAUN, var det slik du mente nÄr du sa at jeg ikke trenger en router? Typ med case
pÄ :uri
Veldig nice faktisk! Jeg ser for meg at app
funksjonen kan bli ganske stor etterhvert med flere routes, men det er vel ikke noe problem
Men kanskje det er vanlig/lurt Ä flytte den ut i et eget namespace, eller dele den opp pÄ en mÄte? :thinking_face:
Det du vanligvis ville gjort var Ă„ lage en funksjon for hver case. Denne funksjonen kan ta req som param og returnere en response
jeg er sykt glad i Ă„ ha alle âroutesâ eller endepunkter eller hva man vil kalle det i Ă©n og samme fil, sĂ„ det er kjempelett Ă„ greppe etter âhvor er koden som kalles nĂ„râŠ. â
og da gjerne uten nĂžsting osv Nay:
(context "/api"
(GET "/foo" ...)
(GET "/bar" ...))
(context "/user"
(GET "/:id" ...)
(GET "/create" ...))
Yay:
(GET "/api/foo" ...)
(GET "/api/bar" ...)
(GET "/user/:id" ...)
(GET "/user/create" ...)
Kanskje sÊrlig nÄr den vokser, faktisk, for da blir det vanskelig Ä ha kontekst-leksografien i hodetVar det noe slik du tenkte pÄ, @U04V5VAUN?
(defn greet-handler [name]
{:status 200
:headers {"Content-Type" "text/plain"}
:body (greet name)})
(defn not-found-handler []
{:status 404
:headers {"Content-Type" "text/plain"}
:body "Not found."})
(defn app [req]
(case (:uri req)
"/greet" (greet-handler (get-in req [:params "name"]))
(not-found-handler)))
Videee ville jeg, mÄr handlerne blir stÞrre og mer komplisert, la dem kun gjÞre det som trengs for Ä gÄ fra http-domenet til forretningsdomenet, og ha egne funksjoner som utfÞrer forretningslogikken uten Ä vÊre bundet til http
NĂ„r du sier "sendt req
" mener du at handler-funksjonene bĂžr ta req
som parameter?
SĂ„nn?
(defn greet-handler [req]
{:status 200
:headers {"Content-Type" "text/plain"}
:body (greet (get-in req [:params "name"]))})
(defn not-found-handler []
{:status 404
:headers {"Content-Type" "text/plain"}
:body "Not found."})
(defn app [req]
(case (:uri req)
"/greet" (greet-handler req)
(not-found-handler)))
Er det forresten god praksis Ă„ samle alle handler-funksjonene i et separat/eget handlers
-namespace?
Kult, men da vet jeg i alle fall at jeg ikke er heeelt ute og kjĂžrer med tankesettet.
Jeg synes det er viktig Ä pÄpeke at din initielle lÞsning er helt super! Det jeg har pÄpekt kan vÊre nytting for veien videre. Det er superviktig Ä fÄ lov til Ä starte med det enklest mulig, og dele ting opp etterhvert som du set behov for det.
Det som er skikkelig drit er rammeverk som tvinger deg inn i en oppdeling som krever masse arbeid initielt.
Det som er viktig er Ä skjÞnne nÄr ting trenger Ä deles opp, og ta seg tid til Ä gjÞre det da, ikke fÞr, ikke etter.
Enig! Godt poeng. Det er dog svĂŠrt tilfredsstillende Ă„ se server
-namespaces sÄ clean:
(ns server
(:require [org.httpkit.server :as hk]
[ring.middleware.params :refer [wrap-params]]
[handlers :as h]))
(defonce server (atom nil))
(defn app [req]
(case (:uri req)
"/greet" (h/greet-handler req)
(h/not-found-handler)))
(defn stop []
(when-some [s @server]
(s :timeout 100)
(reset! server nil)))
(defn start []
(stop)
(reset! server (hk/run-server (wrap-params #'app) {:port 3001})))
(defn -main []
(start))
(comment
(start)
(stop))
tenker at man ihvertfall bÞr vÊre konsekvent. Routeren bÞr enten kalle funksjoner som fÄr req sendt inn, og returnerer et response i ring-format, eller sÄ bÞr routeren kalle funksjoner med spesifikke parametere sendt inn, og som returnerer data som ikke har noe med ring-responses og gjÞre, som routeren sÄ oversetter til ring
i og med at ring-requests ikke er en type, men bare data, sÄ er det vel liten/ingen grunn til Ä ha et mapping-lag her (altsÄ gÄ for fÞrstnevnte)
i Kotlin-boka sĂ„ tar alle handlers inn data som ikke har noe med Ktor (router-rammeverket) Ă„ gjĂžre, og returnerer en generisk WebResponse som er en klasse man lager selv i boka. Men i Clojure-land sĂ„ er formatet pĂ„ ring-requests sĂ„pass universelt at man oppnĂ„r samme âfrikoblingâ fra router/rammeverk/⊠om man bare tar inn ring-data direkte
En mental Ăžvelse som jeg ofte gjĂžr er hva om jeg skulle skrive om denne appâen til Ă„ ikke vĂŠre en webapp, hvordan kan jeg skrive den sĂ„nn at endringene blir minimale.
Ja, pÄ en mÄte er det kanskje en nyttig Þvelse Ä gjÞre utvikling av web app og CLI (eller en annen type klient) mot samme API samtidig.
hvis man skal ha et sĂ„nt lag, altsĂ„ noe som frikobler deg helt fra ring, sĂ„ tenker jeg at routeren ikke nĂždvendigvis er feil sted Ă„ gjĂžre det pĂ„. Om man ikke gjĂžr det i routeren, sĂ„ ender man jo opp med en hel del âboilerplate-aktigeâ handler-funksjoner hvor alt de gjĂžr er Ă„ ta i mot request, sende inn noe data til funksjon, ta data fra funksjon og gjĂžre om til ring-request. Ofte greit Ă„ bare gjĂžre det in-line i route-definisjonen synes nĂ„ jeg ihvertfall đ
men om jeg jobbet sammen med @U04V5VAUN ville jeg pratet med ham om det og sÄ gÄtt med pÄ hva nÄ enn vi kom frem til sammen - ser ikke pÄ dette som noe pattern som man bare mÄ fÞlge, mange veier til Rom, osv
i Kotlin-boka sÄ har jeg gjort denne frikoblingen som sagt, men det er fordi de samme handlerene kalles bÄde i serverless lambdaer, i Ktor, i Jooby (et alternativ til Ktor), og i det hele tatt. Der skal faktisk samme kode kjÞre i flere settinger
usikker pÄ om future proofing om at ring plutselig forsvinner er viktig. Men, det er jo noe med at man sitter igjen med business-logikk som tar imot data og returnerer data, uten Ä vite noe om verden rundt, som er vakkert og deilig
(og jeg ser pĂ„ business-logikk som vet om postgres-spesifikke SQL-er og sĂ„nne ting som âuten Ă„ vite noe om verden rundtâ, det er core for domene-koden spĂžr du meg. Havnet plutselig i det reflekterte hjĂžrnet her jeg sitter)
utrolig spennende Ă„ se hvordan dere alle tenker her!
> Videee ville jeg, mÄr handlerne blir stÞrre og mer komplisert, la dem kun gjÞre det som trengs for Ä gÄ fra http-domenet til forretningsdomenet, og ha egne funksjoner som utfÞrer forretningslogikken uten Ä vÊre bundet til http
Merker at jeg blir gira selv â fĂ„r lyst til Ă„ prĂžve Ă„ ha minapp.web
med web-ting og ring-requests, og minapp.api
eller minapp.problemnavn
som tar inn og returnerer idiomatisk clojure-data â som er lett Ă„ bruke i REPL og i tester.
Har tidligere lagt HTTP-handlers som er litt store og gjĂžr litt mye, fĂžler pĂ„ at det er litt kjipt Ă„ sende {:uri "âŠ", âŠ}
inn i app for at den videre skal kalle handlers, og det jeg fÄr ut er html som tekst, ikke data som data.
NĂ„ ble det straks vanskeligere Ă„ teste en handler da, fordi jeg mĂ„ fake en request pĂ„ et vis! đ Jeg gjorde det slik:
(ns server-test
(:require [clojure.test :refer [deftest is testing]]
[clojure.string :as str]
[server :as s]))
(deftest greet-test
(testing "Greeting includes person's name"
(is (str/includes?
(s/app {:uri "/greet" :params {"name" "Leif"}})
"Hello, Leif!"))))
ville definitivt gjort testingen pÄ hakket etter routeren ja, sÄ :uri ikke har noe med saken Ä gjÞre
Ja, jeg ble litt usikker pÄ om jeg skal/bÞr teste routeren eller handlerne, eller begge deler.
hvis man fÞrst skal teste hva som skjer nÄr man kaller en URL, ville jeg lagt inn et ord for Ä faktisk spinne opp en server, og bruke en http-klient, osv
ville definitivt gjort testingen pÄ hakket etter routeren ja, sÄ :uri ikke har noe med saken Ä gjÞreSant. Men da fÄr du jo ikke en fullstendig, 100 % riktig Ring-request? Putter du bare inn det du trenger fra map-et da, feks :route-params
?
(jeg har lurt litt pÄ hva som er vanlig Ä gjÞre for andre folk her)
tenker da at man gjÞr testingen pÄ funksjonene man har (?) som ikke vet noe om Ring
ville definitivt gjort testingen pÄ hakket etter routeren ja, sÄ :uri ikke har noe med saken Ä gjÞreSÄ slik, da? Bad:
(ns server-test
(:require [clojure.test :refer [deftest is testing]]
[clojure.string :as str]
[server :as s]))
(deftest greet-test
(testing "Greeting includes person's name"
(is (str/includes?
(s/app {:uri "/greet" :params {"name" "Leif"}})
"Hello, Leif!"))))
Good:
(ns server-test
(:require [clojure.test :refer [deftest is testing]]
[clojure.string :as str]
[handlers :as h]))
(deftest greet-test
(testing "Greeting includes person's name"
(is (str/includes?
(:body (h/greet {:params {"name" "Leif"}}))
"Hello, Leif!"))))

Teodor er en god August. Jeg kan endelig trekke meg tilbake og la tingene gÄ sin gang her i denne trÄden
Det som er fint er at en ring request er bare data, sÄ det er ganske trivielt Ä mekke en passende request, i motsetning til i Javaland hvor du mÄ til med en MockHttpServletRequest
eller deromkring. NĂ„r det er sagt, kommer ring med helpers for Ă„ generere mock requests https://github.com/ring-clojure/ring-mock
Vet dere om det finnes noe pĂ„ java-plattformen (eller et annet Clojure-vennlig sted) som kan erstatte ikke-URL-vennlige tegn med URL-vennlige tegn? Feks ta inn "Ăremerket"
og returnere "Oremerket"
?
Jeg sĂžkte litt etter URL-normalisering med java, og https://www.baeldung.com/java-remove-accents-from-text, men den gjĂžr ikke det jeg Ăžnsker for ĂĂĂ
:
(java.text.Normalizer/normalize "ÊÞÄ" java.text.Normalizer$Form/NFKD)
;; => "ÊÞÄ"
Jeg skulle helst hatt returverdien âaoaâ.Jeg bruker biblioteket Sluj: https://github.com/rawleyfowler/sluj
(require '[sluj.core :as sluj])
=> nil
(sluj/sluj "ÊÞÄ")
=> "aoa"
Etter normalisering kan du fjerne diakritiske tegn: https://github.com/cjohansen/portfolio/blob/main/src/portfolio/ui/search/pliable_index.cljs#L36
AltsĂ„, kun Ă„ av de. Det der tar ogsĂ„ unna tĂždler, tider og masse annen bokstavpĂžnt đ
Jeg ble egentlig skikkelig positivt overrasket av at dette pÄ sett og vis er lÞst i Unicode. Jeg trodde jeg mÄtte finne et bibliotek som stÞttet URL-normalisering pÄ en rekke sprÄk, der norsk er stÞttet. Hadde ikke hÞrt om "diakrtitske tegn" fÞr i dag!
https://clojurians.slack.com/archives/C02M6N5C137/p1696912504243649 er kanskje verdt Ä ta en titt pÄ @leif.eric.fredheim?