Skip to content

Long write operation locks reads in ValueObservation (depending on maximumReaderCount) #1647

@theMishania

Description

@theMishania

What did you do?

Setup DatabasePool with config that has maximumReaderCount == 2 (can be any number)

Started long write operation in DatabasePool. Created 2 ValueObservations for read. Canceled (or not) them and started 3rd ValueObservation.

What did you expect to happen?

3rd ValueObservation performs read and publishes notifications about change in tracked region/records, etc because Pool's semaphore value drops down eventually (when another concurrent reads are performed) its value and doesn't block reads forever.

What happened instead?

3rd ValueObservation is blocked, because Pool.get() is locked by semaphore (itemsSemaphore). This read will never be executed while long write is still in progress. Even if previous ValueObservations was cancelled, 3rd ValueObservation does not starts publishing anything. stack trace shows that Thread is blocked by Pool.get().
Снимок экрана 2024-10-01 в 18 05 42

Environment

GRDB flavor(s): (GRDB, SQLCipher, Custom SQLite build?)
GRDB version: 6.16.0
Installation method: (CocoaPods, SPM, manual?) manual
Xcode version: 15.4
Swift version: 5.7
Platform(s) running GRDB: (iOS, macOS, watchOS?)
macOS version running Xcode: 14.6.1

Demo Project

    func test_WriteLocksReads() throws {
        let configuration = Configuration()
        configuration.maximumReaderCount = 2
        let databasePool = try DatabasePool(
            path: path,
            configuration: configuration
        )
        
        var writeSubscriptions: Set<AnyCancellable> = []
        var readSubscriptions: Set<AnyCancellable> = []
        // write
        databasePool.writePublisher { _ in
            sleep(100000)
        }
        .sink()
        .store(in: &writeSubscriptions)

        let firstReadExpectation = XCTestExpectation()
        ValueObservation.tracking { database in
            defer { firstReadExpectation.fulfill() }
            return try SomeRecord.fetchOne(database, key: "record id")
        }
        .publisher(in: databasePool)
        .sink()
        .store(in: &readSubscriptions)

        wait(for: [firstReadExpectation])

        let secondReadExpectation = XCTestExpectation()
        ValueObservation.tracking { database in
            defer { secondReadExpectation.fulfill() }
            return try SomeRecord.fetchOne(database, key: "record id")
        }
        .publisher(in: databasePool)
        .sink()
        .store(in: &readSubscriptions)

        wait(for: [secondReadExpectation])

        readSubscriptions.removeAll() // can comment this line, doesn't change the problem

        let thirdReadExpectation = XCTestExpectation()
        ValueObservation.tracking { database in
            defer { thirdReadExpectation.fulfill() }
            return try SomeRecord.fetchOne(database, key: "record id")
        }
        .publisher(in: databasePool)
        .sink()
        .store(in: &readSubscriptions)

        wait(for: [thirdReadExpectation])
        XCTAssertTrue(true) // will not be executed until write's sleep(100000) is done
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions