Understanding persistent connection reuse

558 views
Skip to first unread message

Jimmy

unread,
Mar 8, 2010, 10:08:43 AM3/8/10
to ASIHTTPRequest
Hey all,

Either I see a problem in the ASI stuff, or am not understanding how
persistent connections are supposed to work in it. Either way...

I've integrated a031a030f949 into my codebase (was HEAD not too long
ago if it isn't still) in order to add Keep-Alive support to my
project. I'm still in the "get it back to where it was before
upgrading" stage, so I haven't done any work to get Keep-Alives
working.

I use the NSOperation method of doing requests. I create a request
(sometimes ASIHTTPRequest, sometimes ASIFormDataRequest) every time I
want to do a new transaction.

The behavior I'm seeing is that at the start of the app, the
connections all work. After that, none of them work. They all have an
error as follows:
2010-03-08 10:00:59.977 MyApp[47253:207] RESTHelper[0x54207f0]: Error
was Error Domain=ASIHTTPRequestErrorDomain Code=1 UserInfo=0x5413a80
"A connection failure occurred"
2010-03-08 10:01:00.018 MyApp[47253:207] RESTHelper[0x54207f0]: Error
userInfo is {
NSLocalizedDescription = "A connection failure occurred";
NSUnderlyingError = Error Domain=NSPOSIXErrorDomain Code=57
UserInfo=0x1293800 "Operation could not be completed. Socket is not
connected";
}

So.. how *should* I be creating the connections? Keeping a pool of my
own to reuse? That seems in keeping with the
ASIHTTPRequest#connectionCanBeReused property. I notice that each
ASIHTTPRequest has its own connection pool, and I don't understand why
that would be, since it's an instance variable, not global.

Any help you can offer would be immensely appreciated. :)

Jimmy

Ben Copsey

unread,
Mar 8, 2010, 11:13:22 AM3/8/10
to asihttp...@googlegroups.com
Hi Jimmy

> Either I see a problem in the ASI stuff, or am not understanding how
> persistent connections are supposed to work in it. Either way...

As you've discovered, the way persistent connections work is rather confusing. :) This is mostly because the way they work in CFNetwork is very confusing (and, as far as I can see, entirely undocumented, except for a few Apple mailing list posts...)

ASIHTTPRequest has a connection pool, stored in the static array persistentConnectionsPool. Each item in this pool is actually a dictionary that contains:

* information about the host, port and scheme (eg http) the connection is for
* a reference to the request that is currently using a connection (if there is one)
* a reference to the stream used for the request
* an ID that uniquely identifies the connection
* a date corresponding with when the connection should expire

When a request is started, it first looks in the connection pool to see if there is a connection it can reuse. It will attempt to reuse a connection if:

* The host, port and scheme are the same as the request
* The date the connection is supposed to expire has not yet passed
* The connection is not already in use (if it has a value for the request key, it is already in use)

If it finds a connection to use, the request will set its connectionInfo property to the dictionary for the connection it will use. If not, it creates a new connectionInfo dictionary (since it will need to create a new connection) and adds it to the pool. If it finds any connections that should have expired, it removes them from the pool.

Behind the scenes, CFNetwork keeps connections open as long as there is a stream attached to that connection that hasn't been closed. So, when a request finishes, we actually keep the readStream open so a subsequent request can use the connection - once a new request starts and its stream has been opened, we close the old stream.

Each connection's unique ID is used to 'tag' the readStream to tell CFNetwork 'reuse the connection that matches this id, or create a new connection if you can't find a connection that matches this id'. As far as I know, there is no way to access to the actual connection itself - all you can do is to provide CFNetwork with a hint as to your intention.

> The behavior I'm seeing is that at the start of the app, the
> connections all work. After that, none of them work. They all have an
> error as follows:
> 2010-03-08 10:00:59.977 MyApp[47253:207] RESTHelper[0x54207f0]: Error
> was Error Domain=ASIHTTPRequestErrorDomain Code=1 UserInfo=0x5413a80
> "A connection failure occurred"
> 2010-03-08 10:01:00.018 MyApp[47253:207] RESTHelper[0x54207f0]: Error
> userInfo is {
> NSLocalizedDescription = "A connection failure occurred";
> NSUnderlyingError = Error Domain=NSPOSIXErrorDomain Code=57
> UserInfo=0x1293800 "Operation could not be completed. Socket is not
> connected";
> }
>
> So.. how *should* I be creating the connections? Keeping a pool of my
> own to reuse? That seems in keeping with the
> ASIHTTPRequest#connectionCanBeReused property. I notice that each
> ASIHTTPRequest has its own connection pool, and I don't understand why
> that would be, since it's an instance variable, not global.

Each request has its own connectionInfo dictionary, and the pool (which is global for all ASIHTTPRequests) is an array of these connectionInfo dictionaries.

You shouldn't really need to do anything special to make persistent connections work. connectionCanBeReused is really only used internally - it has a public accessor so I can see what a request is doing in tests. The only property you should generally need to worry about is shouldAttemptPersistentConnection - if you set this to NO, ASIHTTPRequests won't attempt to use persistent connections at all.

The first step in debugging problems with persistent connections should be to turn on DEBUG_PERSISTENT_CONNECTIONS (the easiest way is to modify ASIHTTPRequestConfig.h). This will cause ASIHTTPRequest to print information about how it is using and reusing connections to the console.

Also, check the headers the server you are connecting to returns. If it returns a Keep-Alive header, ASIHTTPRequest can use the information about when the connection should expire to prevent the kind of problem you've been seeing (I'm guessing, it is trying to use connections that should have been closed). If it isn't returning a keep-alive header, ASIHTTPRequest will attempt to reuse the connection for 60 seconds after the request using it finishes. Depending on the server you're connecting to, this may be the wrong number entirely - servers tuned for performance may well close unused connections much earlier.

I think the main flaw in the current implementation is ASIHTTPRequest won't attempt to retry if the server closes the connection - I hope to look at this fairly soon.

I hope this makes sense!

Best

Ben

Jimmy

unread,
Mar 8, 2010, 1:59:56 PM3/8/10
to ASIHTTPRequest
Hey

On Mar 8, 11:13 am, Ben Copsey <b...@allseeing-i.com> wrote:
> ASIHTTPRequest has a connection pool, stored in the static array persistentConnectionsPool. Each item in this pool is actually a dictionary that contains:

Ah, static.. I hadn't noticed that. Thanks.

> Also, check the headers the server you are connecting to returns. If it returns a Keep-Alive header, ASIHTTPRequest can use the information about when the connection should expire to prevent the kind of problem you've been seeing (I'm guessing, it is trying to use connections that should have been closed). If it isn't returning a keep-alive header, ASIHTTPRequest will attempt to reuse the connection for 60 seconds after the request using it finishes. Depending on the server you're connecting to, this may be the wrong number entirely - servers tuned for performance may well close unused connections much earlier.
>
> I think the main flaw in the current implementation is ASIHTTPRequest won't attempt to retry if the server closes the connection - I hope to look at this fairly soon.

I bet this is the culprit. So, I took a look at the spec (http://
www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.1.4) and it says
there are no requirements for time-outs on client or server. Also, it
says that both sides should watch out for a closed connection and
respond "as appropriate." :P

So, the dumb/right thing for me to do is to tweak the timeout value on
the client side until it stops breaking, unless I want to open a
connection and see how long it takes to time out. Thankfully I more-
or-less control the server (Nginx on Heroku), so I can trust that the
values are sane. Also, since it's traffic bursts I'm trying to
mitigate latency for, I don't really need it to stay open for a whole
60s.

Having the library retry would be sweet. That puts it firmly in the
"it just works, so stop thinking about it" category. :)

> I hope this makes sense!

It was one of the most helpful, timely forum posts I've ever
received. I just bought your game in response. ;)

Jimmy

Reply all
Reply to author
Forward
0 new messages