code-reviews 2023-05-11

Have been having a lot of fun with core-async recently. Here's a wrapper around sockets -- would love your feedback if you get time!

(defn start-socket [{:keys [id ws-conn send-ch receive-ch cleanup-ch
                            start-receive-worker]
                     :as socket}]

  (log/infof "started socket id = %s" id)

  (let [send-worker-ch (start-send-worker socket) ;; returns a go-loop 
        receive-worker-ch (start-receive-worker socket)] ;; returns a go-loop
    ;; trap if these workers ever fail
    (a/go
      (let [[e] (a/alts! [send-worker-ch receive-worker-ch])]
        (when (instance? Throwable e)
          (log/error e "worker errored id = %s" id)
          (a/close! cleanup-ch)))))

  ;; cleanup 
  (a/go
    (a/<! cleanup-ch)
    (log/infof "cleaning up socket id = %s" id)
    (remove-socket! id)
    (a/close! send-ch)
    (a/close! receive-ch)
    (when (.isOpen ws-conn)
      (.close ws-conn))))

(defn create-undertow-config [{:keys [id start-receive-worker]}]
  (let [send-ch (a/chan)
        receive-ch (a/chan)
        cleanup-ch (a/chan)]
    {:undertow/websocket
     {:on-open (fn [{ws-conn :channel}]
                 (start-socket {:id id
                                :ws-conn ws-conn
                                :send-ch send-ch
                                :receive-ch receive-ch
                                :cleanup-ch cleanup-ch
                                :start-receive-worker start-receive-worker}))

      :on-message (fn [{:keys [data]}]
                    (a/>!! receive-ch (<-json data true)))
      :on-error (fn [{throwable :error}]
                  (condp instance? throwable
                    java.net.SocketException nil

                    (log/error throwable "error")))
      :on-close-message (fn [{:keys [message]}]
                          (log/infof "close-message received id = %s message = %s"
                                     id (bean message))
                          (a/close! cleanup-ch))}}))
Most of the work happens in start-socket. Main thing I don't like right now:
(let [send-worker-ch (start-send-worker socket) ;; returns a go-loop 
        receive-worker-ch (start-receive-worker socket)] ;; returns a go-loop
    ;; trap if these workers ever fail
    (a/go
      (let [[e] (a/alts! [send-worker-ch receive-worker-ch])]
        (when (instance? Throwable e)
          (log/error e "worker errored id = %s" id)
          (a/close! cleanup-ch)))))
If workers ever fail, they stop and return an exception. This alts! is checking if these workers ever stop b/c of an exception. I wonder if there's an abstraction I should create here (something like trap-first-err , but am not sure)

For posterity: I wrote up a wait? abstraction, inspired by more.async. It waits for all channels to close, but if any channel returns an exception, it re-throws it.