Fork me on GitHub
#component
<
2022-04-29
>
Serafeim Papastefanos10:04:43

I'm trying to create a component for my jetty webserver. My first try is this:

(defrecord Server [config http-server shutdown]
  component/Lifecycle

  (start [this]
    (log/info ";; Starting Server" this http-server)
    (if http-server
      this
      (assoc this
             :http-server (ring.adapter.jetty/run-jetty app {:port 3000 :join? false})
             :shutdown (promise))))

  (stop [this]
    (log/info ";; Stopping Server" this http-server)
    (if http-server
      (do
        (.stop http-server)
        (deliver shutdown true)
        (assoc this :http-server nil))
      this)))

(defn new-server [config]
  (map->Server {:config config}))
Now this works fine when I start and stop the system however this is not idemponent. For some reason the (assoc this :http-server) thingie ain't working. So if I try to run (start) twice it will throw an error address already bound on the 2nd try

Serafeim Papastefanos10:04:30

Do you have any idea what could be the problem? the http-server is always nil !

Serafeim Papastefanos10:04:56

is it possible that (ring.adapter.jetty/run-jetty app {:port 3000 :join? false}) returns nil ? if it returns nil how is it working here: https://github.com/seancorfield/usermanager-example/blob/develop/src/usermanager/main.clj#L164

Serafeim Papastefanos10:04:56

No it doesn't return nil i checked it.

Serafeim Papastefanos10:04:52

I'm trying toi debug that... I've added this:

(comment 
  (def s (new-server {}))
  (.start s ))
When I run (.start s) the s has the correct info (i.e http-server is not nil). However if i run (.start s) again it will get nil for the http-server and will throw the bind error

Serafeim Papastefanos14:04:12

Am i doing something wrong? I understand that my example isn't working because of the immutability of s. But why the component isn't working?

seancorfield16:04:05

Or you might do this sort of thing:

(defn -main
  "Start Jetty and run auth Ring app."
  [& [port]]
  (let [port (or port (get (System/getenv) "PORT" 9110))
        port (cond-> port (string? port) Integer/parseInt)
        running-system (component/start (new-system port))]
    (alter-var-root #'sys (constantly running-system))
    (start-telemetry (:application running-system)
                     "api" "Application API" `probe)
    ;; wait for web server to shutdown
    (->  running-system :web-server :shutdown deref)))
That's one of our actual applications at work that uses the same pattern. In both cases the running system is stored in a global purely so you can start the app with a Socket REPL (via a JVM option) and then connect into the app from an editor or from the command-line, and then inspect the state of the app and run its code.

seancorfield16:04:34

If you did:

(let [running-system (component/start (new-system ..))]
      the-system (component/start running-system)]
  ...)
you should find the second call to start will "work" and be a no-op.

seancorfield16:04:36

The key to this is that you must capture the result of calling start (or stop) and use it in the next expression that operates on it -- that's the "Clojure way" due to immutability. In your code, you're calling start but "throwing away" the result and trying to start s again from its original state (and using .start is not idiomatic for calling functions from protocols -- the .fn approach is intended for Java interop).

seancorfield16:04:43

Hope that helps?

Serafeim Papastefanos16:04:50

Thank you @seancorfield for the help. The thing is that the https://github.com/stuartsierra/component.repl I'm using is supposed to behave this way ... Ie replace the system with the new version after calling start. Maybe I'm doing something wrong, I'll research it a bit more and if i can't fix it I'll do it manually as you propose

seancorfield16:04:52

I avoid the "reloaded" stuff altogether -- we don't use that component.repl stuff. We don't use tools.namespace for it either.

Serafeim Papastefanos18:04:20

Can you clarify why not?

seancorfield20:04:29

Because a) it isn't needed b) it's fragile and can break in unexpected ways (I see a lot of beginners get into trouble with it) and c) it requires additional dependencies. Developing a good, REPL-friendly workflow without tools like that will benefit your life 🙂

Serafeim Papastefanos21:04:51

Yes i guess you are right! I'll do it with the simple way at first so i can understand everything happening 😁 thank you

1