NSURLRequest and NSOperationQueue

  • Hi everyone,

    I'm building an object that communicates with a server.  For various reasons, I'd like to queue up all the NSURLRequests in an NSOperationQueue so that I never have more than one connection open at a time.

    However, I'm running into a weird issue.  If I create my NSURLRequest and open an NSURLConnection directly, then the connection works and everything proceeds as expected.  However, if I create an NSInvocationOperation to delay the creation of the connection until the queue is idle, then the connection is created (and is non-nil), but the URLRequest never triggers any of its delegate methods.

    After some investigation, I realized that the operation was executing on a different thread, so I scheduled the URLConnection on the mainRunLoop in the default mode (after unscheduling from the currentRunLoop).  I'm also retaining the URLConnection in an ivar, but it's still not firing any delegate methods (on any thread).

    Any ideas why my URL connection isn't working?

    Thanks,

    Dave DeLong
  • Dave,

    If you are setting up NSURLConnection on an NSOperation, I would suggest you keep the operation around as you get the data back.  The symptom you describe, sounds like you are starting the NSURLConnection, but then you leave your main method in the NSOperation you created.  This essentially orphans the thread that the NSURLConnection needs to report back progress to your delegate.  If that Thread no longer exists, the NSURLConnection will never report back.

    Solution 1 would be to have you do a synchronous NSURLConnection in your main method.  This will show you everything is working in that thread and that you do indeed get data back from your server.

    Solution 2 would be to change your NSOperation main method such that you start the NSURLConnection asynchronously, and you will need to loop until either you receive the connection:didFailWithError: delegate callback or you receive the connectionDidFinishLoading: delegate method callback.

    Since you're creating a NSOperation anyway, you probably want threaded access to your data. So I would work on solution #2.  Your main method in your NSOperation subclass should be something simple like the following:

    NSAutoreleasePool*    pool = [[NSAutoreleasePool alloc] init];

    NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop];
    if ( currentRunLoop )
    {
    // 1. Start the URLConnection!
    mURLConnection = [NSURLConnection connectionWithRequest:mURLRequest delegate:self];
    [mURLConnection start];

    // 2. We have a run Loop, so wait until the connection is finished
    while ( !done && ![self isCancelled] )
    {
      // Run the RunLoop!
      NSDate* dateLimit = [[NSDate date] addTimeInterval:0.1];
      [currentRunLoop runUntilDate:dateLimit];
    }

    // 3. Report your results to your main thread!
    ...
    }

    Scott Tury

    On Jan 17, 2010, at 12:08 AM, <cocoa-dev-request...> wrote:

    > Subject: NSURLRequest and NSOperationQueue
    >
    > Hi everyone,
    >
    > I'm building an object that communicates with a server.  For various reasons, I'd like to queue up all the NSURLRequests in an NSOperationQueue so that I never have more than one connection open at a time.
    >
    > However, I'm running into a weird issue.  If I create my NSURLRequest and open an NSURLConnection directly, then the connection works and everything proceeds as expected.  However, if I create an NSInvocationOperation to delay the creation of the connection until the queue is idle, then the connection is created (and is non-nil), but the URLRequest never triggers any of its delegate methods.
    >
    > After some investigation, I realized that the operation was executing on a different thread, so I scheduled the URLConnection on the mainRunLoop in the default mode (after unscheduling from the currentRunLoop).  I'm also retaining the URLConnection in an ivar, but it's still not firing any delegate methods (on any thread).
    >
    > Any ideas why my URL connection isn't working?
    >
    > Thanks,
    >
    > Dave DeLong
  • > Solution 1 would be to have you do a synchronous NSURLConnection in your main method.  This will show you everything is working in that thread and that you do indeed get data back from your server.

    This isn't a good idea since it limits the cancelabilty of your operation.

    > NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop];
    > if ( currentRunLoop )
    > {
    > // 1. Start the URLConnection!
    > mURLConnection = [NSURLConnection connectionWithRequest:mURLRequest delegate:self];
    > [mURLConnection start];
    >
    > // 2. We have a run Loop, so wait until the connection is finished
    > while ( !done && ![self isCancelled] )
    > {
    > // Run the RunLoop!
    > NSDate* dateLimit = [[NSDate date] addTimeInterval:0.1];
    > [currentRunLoop runUntilDate:dateLimit];
    > }
    >
    > // 3. Report your results to your main thread!
    > …
    > }

    This is polling and is generally a bad idea, also with such a low timeout your thread will thrash. Furthermore it ties the worker thread up until the operation is complete.

    You should instead make a 'concurrent' NSOperation subclass as it's described in NSOperation parlance. What it really means is an asynchronous one.

    Implement all the required 'concurrent operation' methods, and in -start you do as you were doing, create an NSURLConnection and schedule it in the +[NSRunLoop mainRunLoop]. In the completed callbacks (under error or success conditions) you mark the operation as isFinished.

    This makes your operation cancellable, and frees the worker thread up to service other work units.

    Keith
  • Thanks for the responses!  It hadn't occurred to me to spin the runloop myself.

    My main reason for using an NSOperationQueue for the connections was because the spawner of the connections was also the connection delegate, and it would've taken some interesting code dancing to handle the delegate callbacks of all the possible connections, the model objects they're attempting to create, etc in the same object.

    What I ended up doing was creating a new class, "DDURLConnectionDelegate", that is init'd with the object spawning the connections.  This object exists solely to encapsulate the delegate callbacks of the NSURLConnection, and then, when finished, reports back to the connection spawner with the results, and is destroyed by the runloop.  Now I can freely spawn as many connections as I need, all on the main thread, and have them all handled on a single thread, without having to worry about which connection is supposed to be manipulating which object.

    Cheers,

    Dave

    On Jan 17, 2010, at 9:20 AM, Keith Duncan wrote:

    >> Solution 1 would be to have you do a synchronous NSURLConnection in your main method.  This will show you everything is working in that thread and that you do indeed get data back from your server.
    >
    > This isn't a good idea since it limits the cancelabilty of your operation.
    >
    >> NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop];
    >> if ( currentRunLoop )
    >> {
    >> // 1. Start the URLConnection!
    >> mURLConnection = [NSURLConnection connectionWithRequest:mURLRequest delegate:self];
    >> [mURLConnection start];
    >>
    >> // 2. We have a run Loop, so wait until the connection is finished
    >> while ( !done && ![self isCancelled] )
    >> {
    >> // Run the RunLoop!
    >> NSDate* dateLimit = [[NSDate date] addTimeInterval:0.1];
    >> [currentRunLoop runUntilDate:dateLimit];
    >> }
    >>
    >> // 3. Report your results to your main thread!
    >> …
    >> }
    >
    > This is polling and is generally a bad idea, also with such a low timeout your thread will thrash. Furthermore it ties the worker thread up until the operation is complete.
    >
    > You should instead make a 'concurrent' NSOperation subclass as it's described in NSOperation parlance. What it really means is an asynchronous one.
    >
    > Implement all the required 'concurrent operation' methods, and in -start you do as you were doing, create an NSURLConnection and schedule it in the +[NSRunLoop mainRunLoop]. In the completed callbacks (under error or success conditions) you mark the operation as isFinished.
    >
    > This makes your operation cancellable, and frees the worker thread up to service other work units.
    >
    > Keith
    >
  • The issue that Dave has run into is that when you call the asynchronous NSURLConnection call, NSURLConnection looks to see what thread you are calling it on, and it will only call your delegate back on that Thread (if it exists).  If you exit your NSOperation main method, your thread is going to be cleaned up, and you will never get the delegate callbacks you want.

    Re-reading Dave's original email, I think what's probably happening to him is that he may be switching the runLoops of ht eNSURLConnection BEFORE it has actually started.  Here's the comment from the documentation:

    You may call these methods after the connection has started. However, if the connection is scheduled on multiple threads or if you are not calling these methods from the thread where the connection is scheduled, there is a race between these methods and the delivery of delegate methods on the other threads. The caller must either be prepared for additional delegation messages on the other threads, or must halt the run loops on the other threads before calling these methods to guarantee that no further callbacks will occur.

    Scott
  • I had the same issue in the past using NSOperationQueue and NSURLConnection,
    and I resolved it by simply telling NSURLConnection to send using
    synchronously using sendSynchronousRequest:returningResponse:error:

    Would that not take care of the issue?

    +Clint

    On Sun, Jan 17, 2010 at 1:55 PM, J. Scott Tury <stury...> wrote:

    > The issue that Dave has run into is that when you call the asynchronous
    > NSURLConnection call, NSURLConnection looks to see what thread you are
    > calling it on, and it will only call your delegate back on that Thread (if
    > it exists).  If you exit your NSOperation main method, your thread is going
    > to be cleaned up, and you will never get the delegate callbacks you want.
    >
    > Re-reading Dave's original email, I think what's probably happening to him
    > is that he may be switching the runLoops of ht eNSURLConnection BEFORE it
    > has actually started.  Here's the comment from the documentation:
    >
    > You may call these methods after the connection has started. However, if
    > the connection is scheduled on multiple threads or if you are not calling
    > these methods from the thread where the connection is scheduled, there is a
    > race between these methods and the delivery of delegate methods on the other
    > threads. The caller must either be prepared for additional delegation
    > messages on the other threads, or must halt the run loops on the other
    > threads before calling these methods to guarantee that no further callbacks
    > will occur.
    >
    > Scott
    >
  • On 18 Jan 2010, at 16:37, Clint Shryock wrote:

    > I had the same issue in the past using NSOperationQueue and NSURLConnection,
    > and I resolved it by simply telling NSURLConnection to send using
    > synchronously using sendSynchronousRequest:returningResponse:error:
    >
    > Would that not take care of the issue?

    That is no different to servicing the run loop for an 'asynchronous' operation and is generally a bad idea in the worker threads; it needlessly removes them from the worker pool until the response has arrived.

    Please see my previous post for alternatives.

    Keith
previous month january 2010 next month
MTWTFSS
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Go to today