pedestal 2024-10-11

I'm starting to look at https://github.com/pedestal/pedestal/issues/751 It's about easily allowing for listening on multiple ports. In light of the work I've done on routing (making it easier to mix together multiple routing specifications), I feel it is less likely to require separate servlet bridges or pipelines; I think one pipeline should suffice, with port-specific routing. In my prior experience at Walmart, we had servers configured on two ports: outward facing (via load balancers and reverse proxies, for customer applications) that used one set of authentication rules, and inward facing (from inside the firewall, from other services) which used a different set of authentication rules. We ended up with multiple authentication interceptors, each of whose behavior was keyed off of the specific port for the request. I don't know that a typical, modern application is going to need more than that, i.e., in the world of containerization, a lot of these decisions will be made long before the request arrives inside Pedestal. I also feel that stretching the bootstrap support (the code in the io.pedestal.http namespace) to support multiple interceptor pipelines, multiple routers, and so forth is going to make that code inscrutable. Thoughts?

• The occasion I have most often wished to make a server listen at two ports... is when those ports are 127.0.0.1 and 1; because there seems to be no localhost analog of (all interfaces). So maybe if we just keep #751 open a little while longer IPv4 will just go away (or Putin, Orban, and Trump will kill off IPv6) and the problem will disappear.

I've made a website which has 4 parts: customer facing, front-end api, backoffice, and webservice, all served from same domain. The website was a monolithic clojure app with embedded jetty serving pedestal service. I made a pedestal's table routes which has 4 subsets of routes, each with its own common interceptors (like auth, components injection, etc). It worked well in traditional dedicated server hosting where the application servers are behind nginx proxy. Back then I thought running multiple embedded jetty listening on different ports for each part would be waste of resources. I also felt that maybe my code could be simplified if somehow pedestal could setup jetty to use multiple ports for multiple services. So, in my case, 1 embedded jetty -< multiple service each with its own port.

Let's play what if. What if table/table-routes could specify (in the options map) a base set of interceptors, and all routes defined within would inherit those base interceptors? terse-routes does something similar. This would, essentially, let you link the port for all those routes to a base set of interceptors for all those routes - and those interceptors could cover all the standard concerns including authentiation. e.g.

(route/expand-routes
  (table/table-routes {:port 8090 :interceptors [internal-auth]} [["/api/..." :get [more-interceptors ...] ...

So one Jetty instance with N listeners on N ports, and N sets of routes, where port and initial interceptors for each route in the set are specified once per set, in the table options.

phill: so you are interested in listening on multiple hosts and/or interfaces?

Complications: SSL, SSL port, SSL-only Jetty connectors.

I wondered how the service-map construction would look like, example:

(def public-routes
  (table/table-routes
   {:port 8890 :interceptors [external-auth]}
   [["/" :get [home-page]]]))

(def internal-routes
  (table/table-routes
   {:port 8891 :interceptors [internal-auth]}
   [["/status" :get [status-check]]]))

(def all-routes
  (route/expand-routes public-routes internal-routes))

(def service-map
  {::http/routes all-routes
   ::http/configurator (fn [] ...)
   ::http/context-configurator (fn [] ...)})

(http/create-server service-map)
route/expand-routes accepts multiple route-specs each with its own port then we pass the resulting expanded routes to service-map's http/routes and (in jetty case), add configurators. http/create-server then setup approriate listening ports each with their own handlers.

Your example is what I’ve figured (though I might use routes-from rather than expand-routes to support development mode). Obviously, the :interceptors key is hypothetical but I think it’s really valuable, this this discussion. Currently, the routes in the service map are not used when setting up ports/listeners; it is done in parallel and the relationship between listener ports and route ports is implied not derived; that is, you the developer know the ports and must set things up to match. This makes sense in an order-of-operations sense the Jetty instance is always setup at start, but in dev mode, the routes and ports of those routes are not known and may change during execution. I’m constantly dealing with the stress of improving Pedestal without throwing backwards compatibility out the window. Revising the relationship above, where routes must be known before the servlet engine is started, would be quite the bend, and we would still need further data about the ports (support SSL or not, etc.). In short, I’m not too keen on making that kind of dramatic change because it eventually feels like “is this still Pedestal?” becomes a valid question. My goal has been to “put the Djinn back in the bottle” when it comes to excess implementation details (functions, protocols) in public namespaces (by marking them deprecated and then later deleting them or making them private), while also introducing new optional and compatible features that build on what’s there (like routes-for, or this proposed extension to table-routes, or the sawtooth router). Not perfect (“house built in a bad foundation 🎵 “) but hopefully useful and useable.

To be honest, the Walmart setup was more about pragmatism when running our fleet of services, and the choices were made long before we used containerization (and long before I joined); it would have been better to have a subset of servers dedicated only to outside requests, and a smaller subset dedicated to internal requests, with different setup for the two (and lots of shared code). However, even in modern terms, there's operational URLs (status checks, exposing metrics, coordinated shutdown) that should exist in a customer facing services, but absolutely be segregated with no way for a client to invoke them, and separate ports are a very good way to approach that. In the end, this is still about efficiency; we could (of course) start two Jetty instances, one per port, but they would consume far more resources (threads, memory, connection pools, etc.) than one Jetty instance listening on two (or more) ports.