Search code examples
objective-cnstimernsrunloopnsoutputstreamsocketrocket

NSTimer doesn't work


Main problem

I'm implementing bandwidth management for Socket Rocket. To reduce amount of alteration in Socket Rocket I've decided to create subclass of NSOutputStream which will wrap NSOutputStream created for a socket. Concept is quite nice and should work like charm.

Encounter problem

In - (NSInteger)write:(const uint8_t *)buffer maxLength:(NSUInteger)len method I'm queering from bandwidth manager if data can be send or not. If not bandwidth manager gives me required delay for next write operation.

So my code looks more or less like this:

- (NSInteger)write:(const uint8_t *)buffer maxLength:(NSUInteger)len
{
    auto bandwithInfo = [_bandwidthManager bandwidthInfoForSize: len];
    if (bandwithInfo.m_bOKtoSendNow)
    {
        auto actualWritten = [self.throttledStream write: buffer maxLength: len];
        if (actualWritten<0)
        {
            // report failure of sending data
            [_bandwidthManager reportNetworkBackPressure: len];
        }
        else
        {
            [_bandwidthManager ReportConsumedSendBandwidth: actualWritten];
            int bytesNotSentCount = len - actualWritten;
            if (bytesNotSentCount>0)
            {
                [_bandwidthManager ReportNetworkBackPressure: bytesNotSentCount];
            }
            [self enqueueEvent: NSStreamEventHasSpaceAvailable];
        }
        return actualWritten;
    }
    else
    {
        auto delay = bandwithInfo.m_nWaitTimeMilliseconds;
        NSASSERT(delay>0);
        [self scheduleNotifyReadyToWriteAfterMs: delay];
        return 0;
    }
}

- (void)scheduleNotifyReadyToWriteAfterMs: (int)miliseconds
{
    if (!isReadyToWriteScheduled)
    {
        isReadyToWriteScheduled = YES;
        static const NSTimeInterval kTimeIntervalMilisecond = 0.001;
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval: miliseconds * kTimeIntervalMilisecond
                                                          target: self
                                                        selector: @selector(notifyReadyToWrite:)
                                                        userInfo: nil
                                                         repeats: false];
        NSLog(@"timer=%@\nRunLoop=%@", timer, [NSRunLoop currentRunLoop]);
// this alternative also doesn't work:
//      [self performSelector: @selector(notifyReadyToWrite:)
//                 withObject: nil
//                 afterDelay: miliseconds * kTimeIntervalMilisecond];
    }
}

- (void)notifyReadyToWrite: (NSTimer *)timer
{
    [timer invalidate];

    if (self.hasSpaceAvailable) {
        isReadyToWriteScheduled = NO;
        [self enqueueEvent: NSStreamEventHasSpaceAvailable];
    }
}

In logs I can see, (there is a run loop and it contains created timer):

timer=<__NSCFTimer: 0x109a96180>
RunLoop=<CFRunLoop 0x109a962d0 [0x7fff7208f440]>{wakeup port = 0x4507, stopped = false, ignoreWakeUps = true, 
current mode = (none),
common modes = <CFBasicHash 0x109a96390 [0x7fff7208f440]>{type = mutable set, count = 1,
entries =>
    2 : <CFString 0x7fff71fa1940 [0x7fff7208f440]>{contents = "kCFRunLoopDefaultMode"}
}
,
common mode items = (null),
modes = <CFBasicHash 0x109a963d0 [0x7fff7208f440]>{type = mutable set, count = 1,
entries =>
    2 : <CFRunLoopMode 0x109a96410 [0x7fff7208f440]>{name = kCFRunLoopDefaultMode, port set = 0x440b, queue = 0x109a964e0, source = 0x109a96570 (not fired), timer port = 0x4b03, 
    sources0 = (null),
    sources1 = (null),
    observers = (null),
    timers = <CFArray 0x109a95b10 [0x7fff7208f440]>{type = mutable-small, count = 1, values = (
    0 : <CFRunLoopTimer 0x109a96180 [0x7fff7208f440]>{valid = Yes, firing = No, interval = 0, tolerance = 0, next fire date = 485078914 (-0.00818103552 @ 3808422033916), callout = (NSTimer) [SRSendStreamWithThrottling notifyReadyToWrite:] (0x7fff87025d0d / 0x10264b100) (/Users/maru/Library/Developer/Xcode/DerivedData/AvayaCommonWorkspace-cnweundajqjciphewynfexnutumh/Build/Products/Debug/testrunner), context = <CFRunLoopTimer context 0x109a93370>}
)},
    currently 485078914 (3808417241824) / soft deadline in: 0.00479207 sec (@ 3808422033916) / hard deadline in: 0.004791892 sec (@ 3808422033916)
},

}
}

Now I have a run loop my custom stream and wrapped stream are scheduled in this run loop, so socket rocket and my wrapping class are reciveing all notifications, so I've rule out all obvious mistakes.

Still for some mysterious reason notifyReadyToWrite: is never called. Does anyone have a clue why?


Solution

  • Ok I've found source of problems.

    When Socket Rocket receives notification that space is available it doesn't process this notification immediately, but dispatches notification to working queue and does processing there.
    So when - (NSInteger)write:(const uint8_t *)buffer maxLength:(NSUInteger)len method is invoked current event loop is associated with dispatch queue and this queue is not fully functional. You can't add timer to such run loop. So instead adding timer to current event loop I've added timer to event loop associated with the stream and timer start working:

    - (void)scheduleNotifyReadyToWriteAfterMs: (clientsdk::MillisecondTime)miliseconds
    {
        if (!isReadyToWriteScheduled)
        {
            isReadyToWriteScheduled = YES;
            static const NSTimeInterval kTimeIntervalMilisecond = 0.001;
    
            NSTimer *timer = [NSTimer timerWithTimeInterval: miliseconds * kTimeIntervalMilisecond
                                                     target: self
                                                   selector: @selector(notifyReadyToWrite:)
                                                   userInfo: nil
                                                    repeats: NO];
    
            [self enumerateRunLoopsUsingBlock:^(CFRunLoopRef runLoop) {
                CFRunLoopAddTimer(runLoop, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes);
            }];
        }
    }
    

    This solves issue with a NSTimer.