So this is a fairly shitty case, caused by the interaction of GCD and RAC. Strictly speaking, there's no bug. But it is surprising and weird. We talk about this requirement in the design guidelines at https://github.com/ReactiveCocoa/ReactiveCocoa/blob/1bd47736f306befab64859602dbdea18f7f9a3f6/Documentation/DesignGuidelines.md#subscription-will-always-occur-on-a-scheduler.
The key is that subscription must always happen on a known scheduler. This is a requirement that RAC enforces internally. If you're just using plain old GCD, there is no known scheduler so RAC has to send the subscription off to a scheduler asynchronously.
So to go to your test:
[merged subscribeCompleted:^{
complete = YES;
}];
The actual subscription happens asynchronously because there is no known scheduler. The subscription ends up happening after the -sendCompleted
calls and it misses them entirely. It's really a race condition, but realistically you're probably never going to see it succeed.
The fix is to use RACScheduler
s instead of GCD if possible. If you need to use a specific GCD queue, you can use RACTargetQueueScheduler
. For example, a working, simplified version of your test:
-(void)test_merged_subjects_will_complete_if_on_gcd_queue{
__block BOOL complete = NO;
dispatch_queue_t global_default_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
RACScheduler *scheduler = [[RACTargetQueueScheduler alloc] initWithName:@"testScheduler" targetQueue:global_default_queue];
[scheduler schedule:^{
RACSubject *subject1 = [[RACSubject subject] setNameWithFormat:@"subject1"];
RACSubject *subject2 = [[RACSubject subject] setNameWithFormat:@"subject2"];
RACSignal *merged = [RACSignal merge:@[subject1, subject2]];
[merged subscribeCompleted:^{
complete = YES;
}];
[subject1 sendCompleted];
[subject2 sendCompleted];
}];
NSDate *startTime = NSDate.date;
do{
[NSRunLoop.mainRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:.5]];
}while(!complete && [NSDate.date timeIntervalSinceDate:startTime] <= 10.0);
STAssertTrue(complete, nil);
}
Since the subscription happens from within a scheduler, the subscribeCompleted:
is done synchronously, gets the completed events, and everything behaves as you'd expect.
If you don't need to use a specific GCD queue and just want it done on a non-main queue, then do something like:
[[RACScheduler scheduler] schedule:^{
RACSubject *subject1 = [[RACSubject subject] setNameWithFormat:@"subject1"];
RACSubject *subject2 = [[RACSubject subject] setNameWithFormat:@"subject2"];
RACSignal *merged = [RACSignal merge:@[subject1, subject2]];
[merged subscribeCompleted:^{
complete = YES;
}];
[subject1 sendCompleted];
[subject2 sendCompleted];
}];
I hope that clarifies what you're seeing. Let me know if I need to re-word something.