realm / realm-swift

Realm is a mobile database: a replacement for Core Data & SQLite
https://realm.io
Apache License 2.0
16.32k stars 2.15k forks source link

Non main thread Realm instance is not notified even with `autorefresh` turned on when changes happened in other thread(including main thread) #782

Closed dismory closed 10 years ago

dismory commented 10 years ago

Run this code in an app(so we get an main RunLoop set up by free) in iOS simulator:

// realm, realm2, token, token2 are some strong references
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    realm2 = [RLMRealm defaultRealm];
    realm2.autorefresh = YES;
    token2 = [realm2 addNotificationBlock:^(NSString *notification, RLMRealm *realm) {
        // not called
    }];
    dispatch_async(dispatch_get_main_queue(), ^{
        realm = [RLMRealm defaultRealm];
        RealmTestObjecet *object = [RealmTestObjecet new]; // RealmTestObjecet is a simple subclass of RLMObject
        object.foo = @"bar";
        [realm transactionWithBlock:^{
            [realm addObject:object];
        }];         
    });
});

Do I misunderstand something?

dismory commented 10 years ago

I find that even main thread Realm instance is not notified and refreshed when changes happened in other thread..

Check this test case:

#import <XCTest/XCTest.h>

@class RealmTestObjecet;

RLM_ARRAY_TYPE(RealmTestObjecet);

@interface RealmTestObjecet : RLMObject

@property NSString *text;

@end

@implementation RealmTestObjecet
@end

@interface JTRealmTests : XCTestCase {
    RLMRealm *_mainThreadRealm;
    RLMNotificationToken *_token;
}

@end

@implementation JTRealmTests

- (void)setUp
{
    [super setUp];
    // Put setup code here. This method is called before the invocation of each test method in the class.

    [RLMRealm useInMemoryDefaultRealm];
    _mainThreadRealm = [RLMRealm defaultRealm];
    _mainThreadRealm.autorefresh = YES;

    RealmTestObjecet *object = [RealmTestObjecet new];
    object.text = @"foo";
    [_mainThreadRealm transactionWithBlock:^{
        [_mainThreadRealm addObject:object];
    }];
}

- (void)tearDown
{
    [_mainThreadRealm removeNotification:_token];
    _token = nil;
    _mainThreadRealm = nil;

    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}

- (void)testRealm
{
    __block BOOL notifiedFlag = NO;

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    _token = [_mainThreadRealm addNotificationBlock:^(NSString *notification, RLMRealm *realm) {
        // this is not called
        notifiedFlag = YES;
    }];

    dispatch_async(queue, ^{

        RLMRealm *realm = [RLMRealm defaultRealm];
        RealmTestObjecet *object = [[RealmTestObjecet allObjectsInRealm:realm] firstObject];
        [realm transactionWithBlock:^{
            object.text = @"bar";
        }];

        dispatch_semaphore_signal(semaphore);
    });

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    RealmTestObjecet *object = [[RealmTestObjecet allObjectsInRealm:_mainThreadRealm] firstObject];
    // not refreshed
    XCTAssertEqualObjects(object.text, @"bar", @"");
    // not notified
    XCTAssertTrue(notifiedFlag, @"");
}

@end
tgoyne commented 10 years ago

Autorefresh occurs between iterations of the event loop (so that you always get a consistent view of data while working and to remove the need for synchronization on every read). I don't think this is explicitly documented anywhere, so that needs to be fixed.

dismory commented 10 years ago

@tgoyne I know that but why it does not work as expected here?

alazier commented 10 years ago

The problem is that you are using semaphores. When you wait for the semaphore, the main thread is blocked, and the notification never has a chance to run. Your thread needs to be able to process events for the notification to fire. We have a test which tests this exact scenario here: https://github.com/realm/realm-cocoa/blob/master/Realm/Tests/RealmTests.m#L193

Also please note that there are currently issues with in-memory realms which we are looking into. Might want to persist your realm for the time being until we can resole the issue.

dismory commented 10 years ago

@alazier Look at my first test case, semaphore is not used there but still not working..

alazier commented 10 years ago

In your first testcase you need to run a runloop in your background queue for notifications to fire.

bmunkholm commented 10 years ago

@dismory Is there any way you think we could document this better?

dismory commented 10 years ago

@bmunkholm IMHO, it is necessary to explicitly document RunLoop and Timer implementation details used in Realm as what @tgoyne what said.

dismory commented 10 years ago

@alazier I rewrite the test case(which use semaphores) by running a runloop manually and it works as expected. :smile:

#import <XCTest/XCTest.h>

@class RealmTestObjecet;

RLM_ARRAY_TYPE(RealmTestObjecet);

@interface RealmTestObjecet : RLMObject

@property NSString *text;

@end

@implementation RealmTestObjecet
@end

@interface JTRealmTests : XCTestCase {
    RLMRealm *_mainThreadRealm;
    RLMNotificationToken *_token;
}

@end

@implementation JTRealmTests

- (void)setUp
{
    [super setUp];
    // Put setup code here. This method is called before the invocation of each test method in the class.

    // This will cause crash 
//    [RLMRealm useInMemoryDefaultRealm];
    _mainThreadRealm = [RLMRealm defaultRealm];

    RealmTestObjecet *object = [RealmTestObjecet new];
    object.text = @"foo";
    [_mainThreadRealm transactionWithBlock:^{
        [_mainThreadRealm addObject:object];
    }];
}

- (void)tearDown
{
    [_mainThreadRealm removeNotification:_token];
    _token = nil;
    _mainThreadRealm = nil;

    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}

- (void)testRealm
{
    __block BOOL notifiedFlag = NO;

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    _token = [_mainThreadRealm addNotificationBlock:^(NSString *notification, RLMRealm *realm) {
        // this is not called
        notifiedFlag = YES;
    }];

    dispatch_async(queue, ^{

        RLMRealm * realm = [RLMRealm defaultRealm];
        RealmTestObjecet *object = [[RealmTestObjecet allObjectsInRealm:realm] firstObject];
        [realm transactionWithBlock:^{
            object.text = @"bar";
        }];

        dispatch_semaphore_signal(semaphore);
    });

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    // run main runloop manually
    [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.2]];

    RealmTestObjecet *object = [[RealmTestObjecet allObjectsInRealm:_mainThreadRealm] firstObject];
    // not refreshed
    XCTAssertEqualObjects(object.text, @"bar", @"");
    // not notified
    XCTAssertTrue(notifiedFlag, @"");
}

@end

Also as you said, it will crash in Realm -sendNotifications message when using in memory default Realm.

FYI:

Tests`-[RLMRealm refresh] at RLMRealm.mm:512:
0x1066d903e:  pushq  %rbp
0x1066d903f:  movq   %rsp, %rbp
0x1066d9042:  pushq  %r14
0x1066d9044:  pushq  %rbx
0x1066d9045:  movq   %rdi, %rbx
0x1066d9048:  movq   0x2f6aa1(%rip), %rsi      ; "inWriteTransaction"
0x1066d904f:  callq  *0x2c1c33(%rip)           ; (void *)0x000000010086afc0: objc_msgSend
0x1066d9055:  testb  %al, %al
0x1066d9057:  jne    0x1066d9091               ; -[RLMRealm refresh] + 83 at RLMRealm.mm:530
0x1066d9059:  movq   0x2ff6d8(%rip), %r14      ; RLMRealm._sharedGroup
0x1066d9060:  movq   (%rbx,%r14), %rdi
0x1066d9064:  callq  0x1067c2430               ; tightdb::SharedGroup::has_changed()
0x1066d9069:  testb  %al, %al
0x1066d906b:  je     0x1066d9091               ; -[RLMRealm refresh] + 83 at RLMRealm.mm:530
0x1066d906d:  movq   (%rbx,%r14), %rdi
0x1066d9071:  movq   0x2ff6c8(%rip), %rax      ; RLMRealm._writeLogs
0x1066d9078:  movq   (%rbx,%rax), %rsi
0x1066d907c:  callq  0x1067c2a40               ; tightdb::SharedGroup::advance_read(tightdb::SharedGroup::TransactLogRegistry&)
0x1066d9081:  movq   0x2f6a70(%rip), %rsi      ; "sendNotifications"
---------------------------------------------------------------------------------------------
0x1066d9088:  movq   %rbx, %rdi
0x1066d908b:  callq  *0x2c1bf7(%rip)           ; (void *)0x000000010086afc0: objc_msgSend
0x1066d9091:  popq   %rbx
0x1066d9092:  popq   %r14
0x1066d9094:  popq   %rbp
0x1066d9095:  ret    
0x1066d9096:  movq   %rdx, %rcx
0x1066d9099:  movq   %rax, %rbx
0x1066d909c:  cmpl   $0x1, %ecx
0x1066d909f:  jne    0x1066d90c2               ; -[RLMRealm refresh] + 132 at RLMRealm.mm:529
0x1066d90a1:  movq   %rbx, %rdi
0x1066d90a4:  callq  0x106913dd0               ; symbol stub for: __cxa_begin_catch
0x1066d90a9:  movq   %rax, %rdi
0x1066d90ac:  callq  0x1066d8b2b               ; (anonymous namespace)::throw_objc_exception(std::exception&) at RLMRealm.mm:63
0x1066d90b1:  popq   %rbx
0x1066d90b2:  popq   %r14
0x1066d90b4:  popq   %rbp
0x1066d90b5:  jmpq   0x106913ddc               ; symbol stub for: __cxa_end_catch
0x1066d90ba:  movq   %rax, %rbx
0x1066d90bd:  callq  0x106913ddc               ; symbol stub for: __cxa_end_catch
0x1066d90c2:  movq   %rbx, %rdi
0x1066d90c5:  callq  0x106915984               ; symbol stub for: _Unwind_Resume
0x1066d90ca:  movq   %rax, %rdi
0x1066d90cd:  callq  0x1066ce9c0               ; __clang_call_terminate
jpsim commented 10 years ago

@dismory closing this issue. Please reopen if you feel there's still something we can improve here.