Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
Backends that process uploads asynchronously can handle checkpoint requests themselves by
implementing `CustomCheckpointRequestConnector` on their connector.
These APIs are in alpha and may change in future releases.
* Fix races around `PowerSyncDatabase.close()` crashing the process.

## 1.15.1

Expand Down
2 changes: 1 addition & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Sources/PowerSync/Implementation/AsyncConnectionPool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ final class AsyncConnectionPool: SQLiteConnectionPoolProtocol {
}

private func configureConnection(connection: borrowing RawSqliteConnection, isWriter: Bool) throws {
let context = connection.asLease()
let context = try connection.asLease()
for stmt in initialStatements {
let _ = try context.execute(sql: stmt, parameters: [])
}
Expand Down
13 changes: 10 additions & 3 deletions Sources/PowerSync/Implementation/PowerSyncDatabaseImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ private actor DatabaseInitializationAction {

func ensureInitialized(db: PowerSyncDatabaseImpl) async throws {
if closed {
throw PowerSyncError.operationFailed(message: "Attempted to use closed PowerSync database")
throw PowerSyncError.databaseClosedError()
}
if isInitialized {
return
Expand All @@ -269,9 +269,16 @@ private actor DatabaseInitializationAction {
}

func close(action: () async throws -> ()) async rethrows {
if !closed {
closed = true
if closed {
return
}

closed = true
do {
try await action()
} catch {
closed = false
throw error
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,21 @@ final class NativeConnectionPool: Sendable {
// No dedicated readers? Acquire write connection for this then
let semaphore = readers ?? writer
let connection = try await semaphore.acquire(count: 1)
let lease = connection.acquiredItems[0].asLease()
let lease = try connection.acquiredItems[0].asLease()
return try await onConnection(lease)
}

func write<T>(onConnection: (NativeConnectionLease) async throws -> T) async throws -> T {
let connection = try await writer.acquire(count: 1)
let lease = connection.acquiredItems[0].asLease()
let lease = try connection.acquiredItems[0].asLease()
defer { dispatchWrites(lease: lease) }
let result = try await onConnection(lease)
return result
}

func withAllConnections<T>(onConnection: (NativeConnectionLease, [NativeConnectionLease]) async throws -> T) async throws -> T {
let write = try await writer.acquire(count: 1)
let writeLease = write.acquiredItems[0].asLease()
let writeLease = try write.acquiredItems[0].asLease()
defer { dispatchWrites(lease: writeLease) }

let result: T
Expand All @@ -80,7 +80,7 @@ final class NativeConnectionPool: Sendable {

let span = acquiredReaders.acquiredItems.span
for idx in span.indices {
readerLeases.append(span[idx].asLease())
readerLeases.append(try span[idx].asLease())
}
result = try await onConnection(writeLease, readerLeases)
} else {
Expand Down Expand Up @@ -128,8 +128,11 @@ struct RawSqliteConnection: ~Copyable {
sqlite3_close_v2(connection)
}

func asLease() -> NativeConnectionLease {
precondition(!closed)
func asLease() throws(PowerSyncError) -> NativeConnectionLease {
if closed {
throw .databaseClosedError()
}

return NativeConnectionLease(pointer: self.connection)
}
}
Expand Down
4 changes: 4 additions & 0 deletions Sources/PowerSync/Protocol/PowerSyncError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,8 @@ public enum PowerSyncError: Error, LocalizedError {
return msg
}
}

internal static func databaseClosedError() -> Self {
.operationFailed(message: "Attempted to use closed PowerSync database")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import Testing
/// retry with backoff. Reproduced here in-process with a system-SQLite connection holding
/// a reserved lock, which is byte-for-byte the same file-level contention two processes
/// produce.
@Suite("Concurrent open")
///
/// We run these tests without parallelization as they can block threads (while waiting for
/// a busy lock), which interferes with other concurrent tests.
@Suite("Concurrent open", .serialized)
struct ConcurrentOpenTests {
/// Sendable wrapper for the lock-holding SQLite connection used across tasks.
private final class LockHolder: @unchecked Sendable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,24 @@ struct DatabaseImplementationTests {
try #require(results == [2, 4, 6])
try await db.close()
}

@Test func canCancelClose() async throws {
let db = PowerSyncDatabase(
schema: Schema(),
dbFilename: "cancel-close-test",
logger: DefaultLogger()
)

let result = try await db.readLock { reader in
// Try to close the database, this can't work because of the busy read connection.
let task = Task { try await db.close() };
task.cancel();
return task
}.result
#expect(throws: CancellationError.self) { try result.get() }

// Verify that the database is not in a half-closed state by updating the schema, which runs statements
// on all connections.
try await db.updateSchema(schema: Schema(Table(name: "users", columns: [.text("name")])))
}
}
2 changes: 1 addition & 1 deletion Tests/PowerSyncTests/Implementation/StatementTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Testing
struct StatementTests {
@Test func bindValues() throws {
let connection = try DatabaseLocation.inMemory.openConnection(writer: true)
let lease = connection.asLease()
let lease = try connection.asLease()
try lease.withIterator(
sql: "SELECT ?, ?, ?, ?, ?, ?, typeof(?), typeof(?)",
parameters: [
Expand Down