Fork me on GitHub
Matthew Davidson (kingmob)10:06:07

Announcing Aleph 0.7.0-alpha1! This is a preview of the HTTP/2 code we've been adding. For 0.7.0-alpha1, it's client-only, not server. Please give it a try. For the most part, the changes will be invisible (as long you weren't using undocumented functions). Two new options are available for connection-pool: 1. http-versions - a vector of preferred versions you'll support, defaults to [:http2 :http1]. If you want to force HTTP/1-only, you'd create a custom pool like: (connection-pool {:connection-options {:http-versions [:http1]}}) . NB: servers may ignore your preferred protocol order and choose any allowed version. 2. force-h2c? - by default, you can't do insecure HTTP/2, but the spec allows it if you know in advance a server supports it. And by server, that means your server that you control, probably in a private intranet. Setting this to true will use non-TLS, cleartext HTTP/2.

🙌 10
Arnaud Geiser10:06:53

Great achievement! Nice job!

Matthew Davidson (kingmob)10:06:13

Incoming bug reports in 3...2....1

Matthew Davidson (kingmob)10:06:56

Many thanks to Arnaud for his review help!


Hey, is there a way in an aleph (clojure-side) handler to extract something from the Netty context? I have to deal with a library that sets an attribute on the ctx channel and I'd like to access this attribute from my clojure-side handler. Is there a way to achieve this?

Arnaud Geiser13:06:12

On top of my head, Aleph is not giving you a way to access the ChannelHandlerContext to get an attribute. Maybe Matthew has a workaround though.

Arnaud Geiser18:06:17

My workaround would be to monkey-patch the following function that would give you access to the channel on your handler, and thus the attributes.

Arnaud Geiser18:06:46

But maybe there is something I overcome that is a little bit less hacky...

Matthew Davidson (kingmob)18:06:13

I have a solution, but it's equally hacky... 😛

Matthew Davidson (kingmob)18:06:04

@vale Aleph's not really designed for this, but I can think of a hack if you really need it. It will only work with requests, not responses, though, so it won't work on the client-side. So, the incoming Ring request your handler gets isn't a plain Clojure map. It's actually a type defined by potemkin's def-derived-map, which eventually bottoms out in deftype. (It has a few cool features, like being able to define key lookups on mutable data, like a blend of deftype and defrecord, and making map values effectively lazy. But for the most part, I'm not sure it still makes sense over a plain defrecord) def-derived-maps present to the user as a regular map with a set of keys, but under the hood, the values are computed on the deftype fields, one of which guessed it, ch, a Channel. So in your handler, you could use the field access syntax to read the ch field, like:

(fn my-hander [req]
  (do-something (.-ch req)))

❤️ 2

Very interesting! I was aware of the def-derived-map sorcery (iirc the main point of it is that it's close to zero-copy so it doesn't allocate all the request's values again), but I had no idea you could access "hidden" fields like that... I'll have to look at the implementation again. I'll give this a try!


It works like a charm, thanks!

Matthew Davidson (kingmob)07:07:45

Glad to hear it! Yeah, zero-copy is the biggest benefit. However, the potemkin code was written at the dawn of Clojure; comparing it with modern map/defrecord performance has been on my to-do list for a while. It might not still be worth it, especially for smaller types like NettyResponse, which only has 6 keys.


I'm afraid it really doesn't... It's especially apparent when you frequently pass it through functions that desructure arguments. I think the reason is that its get first looks at the extra keys, then does a set contains? and only then the case-based (theoretically super-fast) lookup. I suspect it could be improved hugely if it first used case then checked the less-common added## etc patterns (untested of course)


When I was using derived maps in a project, I monkey-patched its get to

(~'get [this## key# default-value#]
             (case key#
                   (keys m)
                   (map (fn [m] `(~(symbol (str "." m)) this##)) methods))
               (get added## key# default-value#)))
Not sure how equivalent or not

Matthew Davidson (kingmob)07:07:42

Yeah, the only problem with that is it breaks the map illusion, because then you can't override existing keys.


Doesn't assoc create a whole new instance though with the key replaced?

Matthew Davidson (kingmob)08:07:05

That's true for Clojure maps, but there's no way to "replace" the value when it's a function lookup defined in an interface

Matthew Davidson (kingmob)08:07:39

Unless we create a new derived map interface on the fly

Matthew Davidson (kingmob)08:07:31

It's kinda hacky, but we could make a separate derived-map version that's returned only on assoc/dissocing

Matthew Davidson (kingmob)08:07:58

That way, if you never change keys, you get the fast path, but if you do, you have to get the slow path


Ugh... In a ring context associng into the request map is super common so it gets quite complicated

Matthew Davidson (kingmob)08:07:33

Yeah. Middleware would quickly thrash it all. It's a shame all the fundamental map things are interfaces, not protocols, or there'd be a lot more innovative map usage

Matthew Davidson (kingmob)08:07:16

Anyway, I'll get around to comparing it one day. Of course, I also need to check and make sure nothing will break if we switch from laziness to eagerness...

Matthew Davidson (kingmob)18:06:32

I've been thinking about exposing more of the Netty machinery for users, as well as for debugging. The catch, of course, is relying on something that used to be just an implementation detail. Can I ask what library you're using that's attaching data to the context and/or channel?


It's the Datadog tracer. Since it has to trace across threads in Netty the only way for it to pass along its own "span" is by putting it in the ChannelHandlerContext. I then need this span in the Clojure-side code to associate the Netty trace with the Clojure-side execution

Matthew Davidson (kingmob)07:07:50

That makes sense. My initial thought is to stick them in keys like :aleph/ch or :aleph/ctx, but I'd have to consider the ramifications. E.g., in http2, multiplexed channels only live as long as the request/response pair, so if you save a channel for later, it probably won't exist. Other alternatives are to expose a few fns in aleph.netty that handlers can call, or consider multi-arity handlers that get the ch/ctx as extra params.

Matthew Davidson (kingmob)07:07:47

If your company needs something less hacky, I have availability 😉

Matthew Davidson (kingmob)07:07:05

Hmmm, I must have been tired when I wrote this. Chans would still exist, but I'm not sure about holding a reference to them. Maybe the only consequence is holding onto memory. I was originally thinking of something like weak references, but perhaps it'd be okay to keep them around. It's more an issue for the client side, where HTTP2 chans may be closed by the time the resp map is returned. On the server-side, the chan will exist until your response is finished, so that's a non-issue.

Matthew Davidson (kingmob)07:07:37

@vale To follow up on this, I assume you're placing DD spans in the Netty Channels using .attr()? Regardless, I suspect the new HTTP/2 multiplex channels may cause problems with this. The child Channels won't be long-lived, they'll get created/destroyed for each new incoming request. And afaict, they don't inherit attr data from the parent Channel


It's not me -- that's how the DD agent automatically instruments Netty. When I inspected the pipeline to figure out what the DD agent does to it, I found one of these there. To link my application-thread workload together with the Netty request trace I then extract the DDSpan from the ChannelContext and activate it in my application thread (the rest is agent magic I think). It's pretty hacky and uses undocumented stuff, but I'm in touch with their tech support to figure out how it's supposed to be done.


If they tell me "just don't", then I'll disable automatic Netty server instrumentation and manually produce the trace from the ring request/response, or mimic what they do and build my own tracing handler. The latter has the advantage that I can put it at the head of the pipeline so it can include stuff like the time it takes for Netty (aleph?) to handle an incoming upload for example before it's passed to the ring handler.