-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ui: Adds XHR connection management to HTTP/1.1 installs #5083
Conversation
This includes various things: 1. An object pool to 'acquire', 'release' and 'dispose' of objects, also a 'purge' to completely empty it 2. A `Request` data object, mainly for reasoning about the object better 3. A pseudo http 'client' which doens't actually control the request itself but does help to manage the connections
@i0rek just pointed me to this, I've not read it/thought about it completely but its pretty relevant here: Also gives me some interesting RFC ideas |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a cool way to handle this problem.
I really wish Ember Data had some sort of abort
hook out of the box. Both your solution and my solution to this problem are unfortunately invasive.
I had a lot to say, but nothing blocking.
ui-v2/app/services/client/http.js
Outdated
prev = item.nextHopProtocol; | ||
} | ||
return prev; | ||
}, 'http/1.1'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably works just fine given that scripts are built ahead of time and always served from the same host as the api, but it still scares me. Mostly I'm concerned of the last item happening to be http/2
despite some/most/all of the other requests being http/1.1
. I think this could only happen if some requests are from external sources, but I don't actually know. Just seems safer to to only override prev
if prev
isn't already http/1.1
, and then start with http/2
or whatever the correct token for that is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure the last script is the one that is currently executing. I remember thinking 'which is the best connection to check to fairly reliably guess what protocol the UI is using'. I couldn't wait for an actual API connection as I need the info before then. I think I went for the assumption that the script for the javascript for the UI would 'probably' be using the same protocol as the API. I need to double check what I was doing there really though, thanks for querying.
default: | ||
// generally 6 are available | ||
// reserve 1 for traffic that we can't manage | ||
maxConnections = 5; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to leaving a "channel" open for short-lived requests and unexpected what-have-yous.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hehe, I thought if somebody if somebody was going to use the word 'channel' it was going to be on the EventSource PR!
break; | ||
} | ||
set(this, 'connections', getObjectPool(dispose, maxConnections)); | ||
if (typeof maxConnections !== 'undefined') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is truthy when maxConnections
is null
. That's fine?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty sure maxConnections
can only be undefined
or a number
?
this._headers = { | ||
...headers, | ||
'content-type': 'application/json', | ||
'x-request-id': `${this._method} ${this._url}?${JSON.stringify(headers.body)}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this header value end up being deceptively large? Either via a long url or a large headers.body
value? It might be better to use a uuid generator here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm yeah I suppose a big KV value could get huge. What do you think would be better generating a uuid on a big text blob, or doing a maximum of 5 equality checks on a 5 large KV blobs? I've actually no idea, feels like the latter but wouldn't be surprised if I'm completely wrong there - might have a play and see.
const pool = getObjectPool(function() {}, 10, actual); | ||
pool.acquire(expected, expected.id); | ||
assert.deepEqual(actual[0], expected); | ||
pool.acquire(expected2, expected2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You aren't passing in expected2.id
as the second argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doh, good catch!
@@ -0,0 +1,52 @@ | |||
export default function(dispose = function() {}, max, objects = []) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like an odd design to allow the object array to be provided. It opens up the opportunity for it be mucked with externally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It means I can inspect it whilst testing:
You could pass your own array and then muck with it externally, there are lots of things you can do but shouldn't. I don't think we pass an array in here anywhere in the application code (only during testing). Maybe I should add some comments here to make it clearer?
// what happens if we can't get an id via getId or .id? | ||
// could potentially use Set | ||
objects.push(obj); | ||
if (typeof max !== 'undefined') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An object pool with no max set is just an array...why make setting a max optional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose it's a collection of objects that get disposed of by a configurable function. That collection can be constrained to a certain length, or be an infinite length, the thing is the disposal.
@@ -0,0 +1,52 @@ | |||
export default function(dispose = function() {}, max, objects = []) { | |||
return { | |||
acquire: function(obj, id) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this is just me being pedantic, but calling this an object pool when objects are provided directly is a misnomer. The core value of a traditional object pool is to prevent allocating and destroying tons of objects. Since this data structure isn't doing any form of object recycling, it isn't really an object pool.
This is still a valuable structure by all means, I'm just not sure what to name it. DisposableSet
? DestructorCollection
? That one sounds Java as h*ck.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm yeah I'm not 100% sure on the vocabulary here either. The main reason it's not a 'true' pool is that we don't really have the access via ember-data
API's currently to be able to do that. Ideally I'd like to turn it into a 'true' pool though, it all depends on a possible future HTTPAdapter
and how that might work.
You're right though, the actual functionality here is about disposing/destroying objects so a lot of the vocab works, I think its mainly the filename. Let me rethink it a little, right now I'm tempted to stick to the 'pool' idea as ideally thats what I'd like it to be along with HTTPAdapter
, but I'll give it another thought when I go back over.
itemId = item.id; | ||
} | ||
if (itemId === id) { | ||
index = i; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be nice if you could break here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hehe ah yeah, maybe find
here might be better, will check when I go over again.
Thanks for looking at this, agree its not great you have to get a bit low level in the ember scheme of things, hopefully a |
Hey @DingoEatingFuzz Sorry for the delay on this, I'm just starting to come back around here now. I had a little bit of a deeper look into the protocol sniffing, and it wasn't detecting the current script that we were running in (so So, I added the traditional 'get currently running script' technique, but in order to do this, you have to do it in an initializer outside of the initialize function (anything else in ember must run after a nextTick of some sort, so it gives you the wrong There are probably a few ways to pass this 'current script' in, I just went for the most straightforwards for me (add a method to the class to check), I could have used a setter/getter etc, but figured as I'm only using the value once this seemed like the most appropriate approach. If you have any other ideas that might be better lemme know, happy to switch it out for something else. |
Hey @DingoEatingFuzz I'm going to merge this, please feel free to add further comment, its going onto Thanks, John |
Adds xhr connection managment to http/1.1 installs This includes various things: 1. An object pool to 'acquire', 'release' and 'dispose' of objects, also a 'purge' to completely empty it 2. A `Request` data object, mainly for reasoning about the object better 3. A pseudo http 'client' which doens't actually control the request itself but does help to manage the connections An initializer is used to detect the script element of the consul-ui sourcecode which we use later to sniff the protocol that we are most likely using for API access
Adds xhr connection managment to http/1.1 installs This includes various things: 1. An object pool to 'acquire', 'release' and 'dispose' of objects, also a 'purge' to completely empty it 2. A `Request` data object, mainly for reasoning about the object better 3. A pseudo http 'client' which doens't actually control the request itself but does help to manage the connections An initializer is used to detect the script element of the consul-ui sourcecode which we use later to sniff the protocol that we are most likely using for API access
Adds xhr connection managment to http/1.1 installs This includes various things: 1. An object pool to 'acquire', 'release' and 'dispose' of objects, also a 'purge' to completely empty it 2. A `Request` data object, mainly for reasoning about the object better 3. A pseudo http 'client' which doens't actually control the request itself but does help to manage the connections An initializer is used to detect the script element of the consul-ui sourcecode which we use later to sniff the protocol that we are most likely using for API access
Adds xhr connection managment to http/1.1 installs This includes various things: 1. An object pool to 'acquire', 'release' and 'dispose' of objects, also a 'purge' to completely empty it 2. A `Request` data object, mainly for reasoning about the object better 3. A pseudo http 'client' which doens't actually control the request itself but does help to manage the connections An initializer is used to detect the script element of the consul-ui sourcecode which we use later to sniff the protocol that we are most likely using for API access
Blocking queries maintains an open connection to the
consul
API. When the API is using HTTP/1.1 to communicate, connections potentially reach 6 open connections, and as far as we are aware most browsers allow a maximum of 6 HTTP/1.1 connections per domain.This PR includes various things:
Request
data object, mainly for reasoning about the 'connection' object easier.Further details:
performance
API ( https://developer.mozilla.org/en-US/docs/Web/API/Performance/getEntriesByType ). This is done in a progressive manner so we fallback to HTTP/1.1 if we can't detect - I wrapped this in atry
to be sure. We check for other potentially multiplexing protocols here also.HTTPAdapter
(see UI: Maintain HTTP Headers from all API requests as JSON-API meta data #4946 )This is also mentioned in #5070 and will be used for connection management for blocking queries.
Most of this isn't currently in use, and is to be merged onto
ui-staging
. But if you can see any more gotchas here feedback would be most welcome.Last but most definitely not least, big thanks to @DingoEatingFuzz as I wouldn't have even realised we needed to do this if it wasn't for him 🙌