-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
attempt to make asynchttpserver better; fixes #15925; [backport:1.0] #15957
Conversation
And just like that, #12325 was fixed. 👍 |
@disruptek Yeah, I had a vague memory of your PR. Good we're finally sorting it out. |
There is an issue releated to |
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.
If I understand the problem this is solving then I think you can simplify this and make it clearer to the users:
maxClients
argument to AsyncHttpServer- increment a
clientsCount
counter inprocessClient
- decrement after connection is closed
- don't
accept
unlessclientsCount
<maxClients
.
## while true: | ||
## if server.shouldAcceptRequest(5): | ||
## var (address, client) = await server.socket.acceptAddr() | ||
## asyncCheck processClient(server, client, address, cb) | ||
## else: | ||
## poll() |
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 example is under "basic usage", you shouldn't complicate it.
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.
People copy&paste this section, it needs to be reasonably complete rather than simple. In fact, I should probably add some basic error handling there too.
proc serve*(server: AsyncHttpServer, port: Port, | ||
callback: proc (request: Request): Future[void] {.closure, gcsafe.}, | ||
address = ""; | ||
assumedDescriptorsPerRequest = 5) {.async.} = |
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 very arbitrary and therefore likely error prone.
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's much better than no limit and limits are always arbitrary.
## | ||
## You should prefer to call `acceptRequest` instead with a custom server | ||
## loop so that you're in control over the error handling and logging. | ||
listen server, port, address |
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 shouldn't be here, unless I'm missing something.
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 has to be there thanks to bad API design (it should be part of newAsyncHttpServer
IMHO but this would be a breaking change).
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.
Won't this literally break code? You'll have code in the wild calling listen
twice now.
Edit: oh, I see, didn't realise you introduced the listen
.
This wouldn't work as well because serving a single async request can require multiple different FDs, say if you load data from a DB before serving the request. It's much more robust to ask the event loop instead. |
I think I agree with @Araq here. At first my reaction was kind of like @dom96, but then I thought about it a little more. The current way or its mathematical complement (clients limit & There might be a "silent underprovisioning" argument to be made. The least max fds I've ever seen in 30 years of using Unix is 64, HP-UX in the 1990s, IIRC. So, std(in|out|err)+cpl overhead + 5 headroom still leaves (probably) ~54 concurrent connections in the crazy worst case. That's actually still a lot of concurrent connections, TBH. Except for debugging/testing like for this problem, people never lower such small fd limits. So the "spare fds way" doesn't really risk underprovision, and the complex guesstimation above kind of exhibits what happens to careful users in the "complementary coordinates". Such guestimation seems ugly & more error prone..You may be a forked process inheriting a bunch of fds, too. So, I think specifying from the spares/headroom side is best. I do think having a 5 line API call to crank up the soft max to the hard max (somewhere) would be helpful..basically just: import posix # add more error checks, of course
var fdLim: RLimit
discard getrlimit(RLIMIT_NOFILE, fdLim)
fdLim.rlim_cur = fdLim.rlim_max
discard setrlimit(RLIMIT_NOFILE, fdLim) Maybe call it |
proc serve*(server: AsyncHttpServer, port: Port, | ||
callback: proc (request: Request): Future[void] {.closure, gcsafe.}, | ||
address = ""; | ||
assumedDescriptorsPerRequest = 5) {.async.} = |
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.
factor assumedDescriptorsPerRequest = 5
with the constant used in
proc shouldAcceptRequest*(server: AsyncHttpServer;
assumedDescriptorsPerRequest = 5): bool {.inline.} =
@Araq good PR but please always squash and merge, this PR will break can we do the proposal in #8664 would prevent such issues in future:
(and even force squash and merge); in the rare cases where a squash and merge isn't desirable, the other merge strategies could be enabled temporarily |
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 wouldn't work as well because serving a single async request can require multiple different FDs, say if you load data from a DB before serving the request. It's much more robust to ask the event loop instead.
Then perhaps it should be part of asyncdispatch itself?
## | ||
## You should prefer to call `acceptRequest` instead with a custom server | ||
## loop so that you're in control over the error handling and logging. | ||
listen server, port, address |
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.
Won't this literally break code? You'll have code in the wild calling listen
twice now.
Edit: oh, I see, didn't realise you introduced the listen
.
But previously |
The APIs that are added are part of asyncdispatch, yes. |
The best place to limit fd creation for a network service is just before |
I know, it was a simple mistake. Sometimes github defaults to the wrong button for some reason. |
No, I mean the handling of FDs. Wouldn't it be better to have it handled in |
There could be a |
Maybe, but this would be an even more invasive change. Also: People requested an API like mine for asynchttpserver before, nobody requested this feature for |
Technically, this same problem exists for literally every system call returning a file descriptor. So, some kind of |
Huh, this implies that the OS should handle this for us then. If so why do we need a separate mechanism in the stdlib? |
The OS still limits your open fds. What I meant is that the clients will not see an ECONNREFUSED-type situation until the backlog buffer gets filled up. So, just not doing the accept syscall is more graceful, deferring failure if there is just a very transient spike in connections. |
That's true but please consider: This PR with an API change will be backported to version 1.0.x. I consider it a critical omission -- more critical than other bugfixes that we backported -- but at the same time I tried my best to keep the changes to a minimum. |
Oh, sure. I was just responding to the @dom96 idea of |
Does anyone have a real repro for issue #15925? All I see are synthetic code samples that don't seem realistic to me. I'm quite confused about how this would crop up in real code without it being due to a real FD leak. |
As far as I can know this affects our very own forum software. (Still investigating though.) |
The origin case was in the Forum thread. The |
Well... we just tested his exploit and it doesn't do anything. Furthermore, I checked the server and it has been running for 2 months without crashes. |
So my recommendation is to change this slightly:
|
proc acceptRequest*(server: AsyncHttpServer, port: Port, | ||
callback: proc (request: Request): Future[void] {.closure, gcsafe.}) {.async.} = |
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.
also, the port
here is unused
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.
Ah, good one. Will do a follow-up PR later.
I'm not sure how the Forum server process never died, but you also said server not server process. So, maybe ambiguous? I didn't look at the Forum code, anyway. I do think it is not great example code/library behavior to just exhaust file descriptors. Reserving at least a few for the user code just makes very basic sense to me. I'm not sure why anyone would be against it. You need at least 1 free for the My simpler test #15925 (comment) behaved as one would expect with that |
The Forum process itself never died. |
I don't know what's going on with the Forum recovery from overload, but this simple program: import asynchttpserver, asyncdispatch, asyncnet, posix
var fdLim = RLimit(rlim_cur: 7, rlim_max: 1024)
discard setrlimit(RLIMIT_NOFILE, fdLim)
const
s = "HTTP/1.1 200\r\ncontent-type: text/html\r\ncontent-length: 3\r\n\r\nHi\n"
proc svc(req: Request, staticDir="") {.async.} = await req.client.send(s)
proc cb(req: Request) {.async.} = await req.svc("static")
var server = newAsyncHttpServer()
waitFor server.serve(Port(8080), cb) dies immediately with the old code even for |
No description provided.