Skip to content

Commit b169c57

Browse files
committed
test(sidebar): lock the catalog read counter that concurrent schema reads crashed
1 parent a184480 commit b169c57

1 file changed

Lines changed: 58 additions & 17 deletions

File tree

TableProTests/Services/SchemaRefreshCommitCostTests.swift

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import TableProPluginKit
99
import Testing
1010

1111
/// Records every catalog read it answers, so a test can count the queries a refresh costs.
12+
///
13+
/// A schema load reads tables, routines, triggers and types concurrently, so the read log and the
14+
/// pause gate are locked rather than isolated: an unlocked append from those reads raced and
15+
/// crashed the test host.
1216
final class CatalogReadCountingDriver: DatabaseDriver, @unchecked Sendable {
1317
let connection: DatabaseConnection
1418
var status: ConnectionStatus = .connected
@@ -18,23 +22,62 @@ final class CatalogReadCountingDriver: DatabaseDriver, @unchecked Sendable {
1822
var tablesBySchema: [String: [TableInfo]] = [:]
1923
var tablesError: Error?
2024
var allSchemaTables: [TableInfo]?
21-
private(set) var reads: [String] = []
2225

23-
var pausesNextTableFetch = false
24-
var onTableFetchPaused: (@Sendable () -> Void)?
26+
private let lock = NSLock()
27+
private var readLog: [String] = []
28+
private var pauseNextTableFetch = false
29+
private var tableFetchPausedHandler: (@Sendable () -> Void)?
2530
private var tableFetchGate: CheckedContinuation<Void, Never>?
2631

2732
init(connection: DatabaseConnection) {
2833
self.connection = connection
2934
}
3035

36+
var reads: [String] {
37+
lock.withLock { readLog }
38+
}
39+
40+
var pausesNextTableFetch: Bool {
41+
get { lock.withLock { pauseNextTableFetch } }
42+
set { lock.withLock { pauseNextTableFetch = newValue } }
43+
}
44+
45+
var onTableFetchPaused: (@Sendable () -> Void)? {
46+
get { lock.withLock { tableFetchPausedHandler } }
47+
set { lock.withLock { tableFetchPausedHandler = newValue } }
48+
}
49+
3150
func resumeTableFetch() {
32-
tableFetchGate?.resume()
33-
tableFetchGate = nil
51+
let gate = lock.withLock {
52+
let gate = tableFetchGate
53+
tableFetchGate = nil
54+
return gate
55+
}
56+
gate?.resume()
3457
}
3558

3659
func forgetReads() {
37-
reads.removeAll()
60+
lock.withLock { readLog.removeAll() }
61+
}
62+
63+
private func record(_ read: String) {
64+
lock.withLock { readLog.append(read) }
65+
}
66+
67+
private func takeTableFetchPause() -> Bool {
68+
lock.withLock {
69+
let pauses = pauseNextTableFetch
70+
pauseNextTableFetch = false
71+
return pauses
72+
}
73+
}
74+
75+
private func park(_ continuation: CheckedContinuation<Void, Never>) {
76+
let handler = lock.withLock {
77+
tableFetchGate = continuation
78+
return tableFetchPausedHandler
79+
}
80+
handler?()
3881
}
3982

4083
func reads(ofSchema schema: String) -> [String] {
@@ -63,48 +106,46 @@ final class CatalogReadCountingDriver: DatabaseDriver, @unchecked Sendable {
63106
}
64107

65108
func fetchSchemas() async throws -> [String] {
66-
reads.append("schemas")
109+
record("schemas")
67110
return schemasToReturn
68111
}
69112

70113
func fetchTables() async throws -> [TableInfo] {
71-
reads.append("tables")
114+
record("tables")
72115
return []
73116
}
74117

75118
func fetchTables(schema: String?) async throws -> [TableInfo] {
76119
let schema = schema ?? ""
77-
reads.append("tables:\(schema)")
120+
record("tables:\(schema)")
78121
if let tablesError { throw tablesError }
79122
let snapshot = tablesBySchema[schema] ?? []
80-
if pausesNextTableFetch {
81-
pausesNextTableFetch = false
123+
if takeTableFetchPause() {
82124
await withCheckedContinuation { continuation in
83-
tableFetchGate = continuation
84-
onTableFetchPaused?()
125+
park(continuation)
85126
}
86127
try Task.checkCancellation()
87128
}
88129
return snapshot
89130
}
90131

91132
func fetchTablesInAllSchemas() async throws -> [TableInfo]? {
92-
reads.append("allSchemaTables")
133+
record("allSchemaTables")
93134
return allSchemaTables
94135
}
95136

96137
func fetchRoutines(schema: String?) async throws -> [RoutineInfo] {
97-
reads.append(schema.map { "routines:\($0)" } ?? "routines")
138+
record(schema.map { "routines:\($0)" } ?? "routines")
98139
return []
99140
}
100141

101142
func fetchAllTriggers(schema: String?) async throws -> [TriggerInfo] {
102-
reads.append(schema.map { "triggers:\($0)" } ?? "triggers")
143+
record(schema.map { "triggers:\($0)" } ?? "triggers")
103144
return []
104145
}
105146

106147
func fetchUserDefinedTypes(schema: String?) async throws -> [UserDefinedTypeInfo] {
107-
reads.append(schema.map { "types:\($0)" } ?? "types")
148+
record(schema.map { "types:\($0)" } ?? "types")
108149
return []
109150
}
110151

0 commit comments

Comments
 (0)