Skip to content

Commit 68e83e3

Browse files
authored
Use explicit write transactions during sync (#90)
1 parent ac51f4f commit 68e83e3

29 files changed

Lines changed: 219 additions & 58 deletions
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package eu.pretix.libpretixsync.sqldelight
2+
3+
import app.cash.sqldelight.Transacter
4+
import app.cash.sqldelight.TransactionWithReturn
5+
import app.cash.sqldelight.TransactionWithoutReturn
6+
import eu.pretix.libpretixsync.SentryInterface
7+
8+
/**
9+
* Workaround to start explicit read-write transaction
10+
*
11+
* SQLDelight starts all transactions in DEFERRED mode. As a result, transactions that start with a
12+
* read-only query but perform a write further down the line start off as a read-only transaction
13+
* and are then upgraded to read-write on the fly.
14+
*
15+
* If someone else holds a write lock at the moment of this upgrade, the transaction will instantly
16+
* fail with an SQLITE_BUSY error - unlike explicit read-write transactions which would wait for a
17+
* busy timeout first (if configured).
18+
* The easiest way to avoid this would be to start the transaction with BEGIN IMMEDIATE, but the
19+
* SQLDelight APIs do not provide this option. As a workaround, issue a no-op write statement before
20+
* the original transaction contents. This will force an upgrade to a read-write transaction right
21+
* away.
22+
*/
23+
fun Transacter.writeTransaction(
24+
db: SyncDatabase,
25+
sentry: SentryInterface? = null,
26+
noEnclosing: Boolean = true,
27+
body: TransactionWithoutReturn.() -> Unit,
28+
) {
29+
try {
30+
transaction(noEnclosing) {
31+
db.compatQueries.upgradeTransaction()
32+
body()
33+
}
34+
} catch (e: IllegalStateException) {
35+
sentry?.captureException(e)
36+
e.printStackTrace()
37+
38+
transaction(false) {
39+
db.compatQueries.upgradeTransaction()
40+
body()
41+
}
42+
}
43+
}
44+
45+
fun <R> Transacter.writeTransactionWithResult(
46+
db: SyncDatabase,
47+
sentry: SentryInterface? = null,
48+
noEnclosing: Boolean = true,
49+
bodyWithReturn: TransactionWithReturn<R>.() -> R,
50+
): R {
51+
val ret = try {
52+
transactionWithResult(noEnclosing) {
53+
db.compatQueries.upgradeTransaction()
54+
bodyWithReturn()
55+
}
56+
} catch (e: IllegalStateException) {
57+
sentry?.captureException(e)
58+
e.printStackTrace()
59+
60+
transactionWithResult(false) {
61+
db.compatQueries.upgradeTransaction()
62+
bodyWithReturn()
63+
}
64+
}
65+
66+
return ret
67+
}

‎libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/AllEventsSyncAdapter.kt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package eu.pretix.libpretixsync.sync
22

33
import app.cash.sqldelight.TransactionWithoutReturn
44
import app.cash.sqldelight.db.QueryResult
5+
import eu.pretix.libpretixsync.SentryInterface
56
import eu.pretix.libpretixsync.api.PretixApi
67
import eu.pretix.libpretixsync.sqldelight.Event
78
import eu.pretix.libpretixsync.sqldelight.SyncDatabase
9+
import eu.pretix.libpretixsync.sqldelight.writeTransaction
810
import eu.pretix.libpretixsync.sync.SyncManager.ProgressFeedback
911
import org.joda.time.format.ISODateTimeFormat
1012
import org.json.JSONObject
@@ -15,6 +17,7 @@ class AllEventsSyncAdapter(
1517
api: PretixApi,
1618
syncCycleId: String,
1719
feedback: ProgressFeedback?,
20+
private val sentry: SentryInterface? = null,
1821
) : BaseDownloadSyncAdapter<Event, String>(
1922
db = db,
2023
api = api,
@@ -100,7 +103,7 @@ class AllEventsSyncAdapter(
100103
}
101104

102105
override fun runInTransaction(body: TransactionWithoutReturn.() -> Unit) {
103-
db.eventQueries.transaction(false, body)
106+
db.eventQueries.writeTransaction(db = db, sentry = sentry, body = body)
104107
}
105108

106109
override fun runBatch(parameterBatch: List<String>): List<Event> =

‎libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/AllSubEventsSyncAdapter.kt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package eu.pretix.libpretixsync.sync
22

33
import app.cash.sqldelight.TransactionWithoutReturn
44
import app.cash.sqldelight.db.QueryResult
5+
import eu.pretix.libpretixsync.SentryInterface
56
import eu.pretix.libpretixsync.api.ApiException
67
import eu.pretix.libpretixsync.api.PretixApi
78
import eu.pretix.libpretixsync.api.ResourceNotModified
89
import eu.pretix.libpretixsync.sqldelight.ResourceSyncStatus
910
import eu.pretix.libpretixsync.sqldelight.SubEvent
1011
import eu.pretix.libpretixsync.sqldelight.SyncDatabase
12+
import eu.pretix.libpretixsync.sqldelight.writeTransaction
1113
import eu.pretix.libpretixsync.sync.SyncManager.ProgressFeedback
1214
import org.joda.time.format.ISODateTimeFormat
1315
import org.json.JSONException
@@ -22,6 +24,7 @@ class AllSubEventsSyncAdapter(
2224
api: PretixApi,
2325
syncCycleId: String,
2426
feedback: ProgressFeedback?,
27+
private val sentry: SentryInterface? = null,
2528
) : BaseDownloadSyncAdapter<SubEvent, Long>(
2629
db = db,
2730
api = api,
@@ -107,7 +110,7 @@ class AllSubEventsSyncAdapter(
107110
}
108111

109112
override fun runInTransaction(body: TransactionWithoutReturn.() -> Unit) {
110-
db.subEventQueries.transaction(false, body)
113+
db.subEventQueries.writeTransaction(db = db, sentry = sentry, body = body)
111114
}
112115

113116
override fun runBatch(parameterBatch: List<Long>): List<SubEvent> =

‎libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/BadgeLayoutItemSyncAdapter.kt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package eu.pretix.libpretixsync.sync
22

33
import app.cash.sqldelight.TransactionWithoutReturn
44
import app.cash.sqldelight.db.QueryResult
5+
import eu.pretix.libpretixsync.SentryInterface
56
import eu.pretix.libpretixsync.api.PretixApi
67
import eu.pretix.libpretixsync.sqldelight.BadgeLayoutItem
78
import eu.pretix.libpretixsync.sqldelight.SyncDatabase
9+
import eu.pretix.libpretixsync.sqldelight.writeTransaction
810
import eu.pretix.libpretixsync.sync.SyncManager.ProgressFeedback
911
import org.json.JSONObject
1012

@@ -16,6 +18,7 @@ class BadgeLayoutItemSyncAdapter(
1618
api: PretixApi,
1719
syncCycleId: String,
1820
feedback: ProgressFeedback?,
21+
private val sentry: SentryInterface? = null,
1922
) : BaseDownloadSyncAdapter<BadgeLayoutItem, Long>(
2023
db = db,
2124
api = api,
@@ -111,7 +114,7 @@ class BadgeLayoutItemSyncAdapter(
111114
}
112115

113116
override fun runInTransaction(body: TransactionWithoutReturn.() -> Unit) {
114-
db.badgeLayoutItemQueries.transaction(false, body)
117+
db.badgeLayoutItemQueries.writeTransaction(db = db, sentry = sentry, body = body)
115118
}
116119

117120
override fun runBatch(parameterBatch: List<Long>): List<BadgeLayoutItem> =

‎libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/BadgeLayoutSyncAdapter.kt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package eu.pretix.libpretixsync.sync
22

33
import app.cash.sqldelight.TransactionWithoutReturn
44
import app.cash.sqldelight.db.QueryResult
5+
import eu.pretix.libpretixsync.SentryInterface
56
import eu.pretix.libpretixsync.api.ApiException
67
import eu.pretix.libpretixsync.api.PretixApi
78
import eu.pretix.libpretixsync.sqldelight.BadgeLayout
89
import eu.pretix.libpretixsync.sqldelight.SyncDatabase
10+
import eu.pretix.libpretixsync.sqldelight.writeTransaction
911
import eu.pretix.libpretixsync.sync.SyncManager.ProgressFeedback
1012
import eu.pretix.libpretixsync.utils.HashUtils
1113
import org.json.JSONObject
@@ -18,6 +20,7 @@ class BadgeLayoutSyncAdapter(
1820
api: PretixApi,
1921
syncCycleId: String,
2022
feedback: ProgressFeedback?,
23+
private val sentry: SentryInterface? = null,
2124
) : BaseDownloadSyncAdapter<BadgeLayout, Long>(
2225
db = db,
2326
api = api,
@@ -130,7 +133,7 @@ class BadgeLayoutSyncAdapter(
130133
}
131134

132135
override fun runInTransaction(body: TransactionWithoutReturn.() -> Unit) {
133-
db.badgeLayoutQueries.transaction(false, body)
136+
db.badgeLayoutQueries.writeTransaction(db = db, sentry = sentry, body = body)
134137
}
135138

136139
override fun runBatch(parameterBatch: List<Long>): List<BadgeLayout> =

‎libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/BlockedTicketSecretSyncAdapter.kt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package eu.pretix.libpretixsync.sync
22

33
import app.cash.sqldelight.TransactionWithoutReturn
44
import app.cash.sqldelight.db.QueryResult
5+
import eu.pretix.libpretixsync.SentryInterface
56
import eu.pretix.libpretixsync.api.ApiException
67
import eu.pretix.libpretixsync.api.PretixApi
78
import eu.pretix.libpretixsync.api.ResourceNotModified
89
import eu.pretix.libpretixsync.sqldelight.BlockedTicketSecret
910
import eu.pretix.libpretixsync.sqldelight.ResourceSyncStatus
1011
import eu.pretix.libpretixsync.sqldelight.SyncDatabase
12+
import eu.pretix.libpretixsync.sqldelight.writeTransaction
1113
import eu.pretix.libpretixsync.sync.SyncManager.ProgressFeedback
1214
import org.json.JSONException
1315
import org.json.JSONObject
@@ -22,6 +24,7 @@ class BlockedTicketSecretSyncAdapter(
2224
api: PretixApi,
2325
syncCycleId: String,
2426
feedback: ProgressFeedback?,
27+
private val sentry: SentryInterface? = null,
2528
) : BaseDownloadSyncAdapter<BlockedTicketSecret, Long>(
2629
db = db,
2730
api = api,
@@ -96,7 +99,7 @@ class BlockedTicketSecretSyncAdapter(
9699
}
97100

98101
override fun runInTransaction(body: TransactionWithoutReturn.() -> Unit) {
99-
db.blockedTicketSecretQueries.transaction(false, body)
102+
db.blockedTicketSecretQueries.writeTransaction(db = db, sentry = sentry, body = body)
100103
}
101104

102105
override fun runBatch(parameterBatch: List<Long>): List<BlockedTicketSecret> =

‎libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/CashierSyncAdapter.kt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package eu.pretix.libpretixsync.sync
22

33
import app.cash.sqldelight.TransactionWithoutReturn
44
import app.cash.sqldelight.db.QueryResult
5+
import eu.pretix.libpretixsync.SentryInterface
56
import eu.pretix.libpretixsync.api.PretixApi
67
import eu.pretix.libpretixsync.sqldelight.Cashier
78
import eu.pretix.libpretixsync.sqldelight.SyncDatabase
9+
import eu.pretix.libpretixsync.sqldelight.writeTransaction
810
import org.json.JSONObject
911

1012
class CashierSyncAdapter(
@@ -13,6 +15,7 @@ class CashierSyncAdapter(
1315
api: PretixApi,
1416
syncCycleId: String,
1517
feedback: SyncManager.ProgressFeedback?,
18+
private val sentry: SentryInterface? = null,
1619
) : BaseConditionalSyncAdapter<Cashier, Long>(
1720
db = db,
1821
fileStorage = fileStorage,
@@ -76,7 +79,7 @@ class CashierSyncAdapter(
7679
}
7780

7881
override fun runInTransaction(body: TransactionWithoutReturn.() -> Unit) {
79-
db.cashierQueries.transaction(false, body)
82+
db.cashierQueries.writeTransaction(db = db, sentry = sentry, body = body)
8083
}
8184

8285
override fun runBatch(parameterBatch: List<Long>): List<Cashier> =

‎libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/CheckInListSyncAdapter.kt‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ package eu.pretix.libpretixsync.sync
22

33
import app.cash.sqldelight.TransactionWithoutReturn
44
import app.cash.sqldelight.db.QueryResult
5+
import eu.pretix.libpretixsync.SentryInterface
56
import eu.pretix.libpretixsync.api.PretixApi
67
import eu.pretix.libpretixsync.sqldelight.CheckInList
78
import eu.pretix.libpretixsync.sqldelight.Migrations
89
import eu.pretix.libpretixsync.sqldelight.SyncDatabase
10+
import eu.pretix.libpretixsync.sqldelight.writeTransaction
11+
import eu.pretix.libpretixsync.sqldelight.writeTransactionWithResult
912
import eu.pretix.libpretixsync.sync.SyncManager.ProgressFeedback
1013
import eu.pretix.libpretixsync.utils.JSONUtils
1114
import org.json.JSONException
@@ -19,6 +22,7 @@ class CheckInListSyncAdapter(
1922
syncCycleId: String,
2023
feedback: ProgressFeedback?,
2124
private val subeventId: Long?,
25+
private val sentry: SentryInterface? = null,
2226
) : BaseConditionalSyncAdapter<CheckInList, Long>(
2327
db = db,
2428
fileStorage = fileStorage,
@@ -68,7 +72,9 @@ class CheckInListSyncAdapter(
6872
}
6973

7074
override fun insert(jsonobj: JSONObject) {
71-
val listId = db.checkInListQueries.transactionWithResult {
75+
// Called from within transaction in processPage
76+
// Set noEnclosing=false but keep transaction if this ever gets called directly
77+
val listId = db.checkInListQueries.writeTransactionWithResult(db, sentry, false) {
7278
db.checkInListQueries.insert(
7379
all_items = jsonobj.optBoolean("all_products"),
7480
event_slug = eventSlug,
@@ -143,7 +149,7 @@ class CheckInListSyncAdapter(
143149
}
144150

145151
override fun runInTransaction(body: TransactionWithoutReturn.() -> Unit) {
146-
db.checkInListQueries.transaction(false, body)
152+
db.checkInListQueries.writeTransaction(db = db, sentry = sentry, body = body)
147153
}
148154

149155
override fun runBatch(parameterBatch: List<Long>): List<CheckInList> =

‎libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/DiscountSyncAdapter.kt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package eu.pretix.libpretixsync.sync
22

33
import app.cash.sqldelight.TransactionWithoutReturn
44
import app.cash.sqldelight.db.QueryResult
5+
import eu.pretix.libpretixsync.SentryInterface
56
import eu.pretix.libpretixsync.api.PretixApi
67
import eu.pretix.libpretixsync.sqldelight.Discount
78
import eu.pretix.libpretixsync.sqldelight.SyncDatabase
9+
import eu.pretix.libpretixsync.sqldelight.writeTransaction
810
import eu.pretix.libpretixsync.sync.SyncManager.ProgressFeedback
911
import org.joda.time.format.ISODateTimeFormat
1012
import org.json.JSONObject
@@ -16,6 +18,7 @@ class DiscountSyncAdapter(
1618
api: PretixApi,
1719
syncCycleId: String,
1820
feedback: ProgressFeedback?,
21+
private val sentry: SentryInterface? = null,
1922
) : BaseConditionalSyncAdapter<Discount, Long>(
2023
db = db,
2124
fileStorage = fileStorage,
@@ -98,7 +101,7 @@ class DiscountSyncAdapter(
98101
}
99102

100103
override fun runInTransaction(body: TransactionWithoutReturn.() -> Unit) {
101-
db.discountQueries.transaction(false, body)
104+
db.discountQueries.writeTransaction(db = db, sentry = sentry, body = body)
102105
}
103106

104107
override fun runBatch(parameterBatch: List<Long>): List<Discount> =

‎libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/EventSyncAdapter.kt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package eu.pretix.libpretixsync.sync
22

33
import app.cash.sqldelight.TransactionWithoutReturn
4+
import eu.pretix.libpretixsync.SentryInterface
45
import eu.pretix.libpretixsync.api.PretixApi
56
import eu.pretix.libpretixsync.sqldelight.Event
67
import eu.pretix.libpretixsync.sqldelight.Migrations
78
import eu.pretix.libpretixsync.sqldelight.SyncDatabase
9+
import eu.pretix.libpretixsync.sqldelight.writeTransaction
810
import eu.pretix.libpretixsync.sync.SyncManager.ProgressFeedback
911
import eu.pretix.libpretixsync.utils.JSONUtils
1012
import org.joda.time.format.ISODateTimeFormat
@@ -19,6 +21,7 @@ class EventSyncAdapter(
1921
api: PretixApi,
2022
syncCycleId: String,
2123
feedback: ProgressFeedback? = null,
24+
private val sentry: SentryInterface? = null,
2225
) : BaseSingleObjectSyncAdapter<Event>(
2326
db = db,
2427
fileStorage = fileStorage,
@@ -94,7 +97,7 @@ class EventSyncAdapter(
9497
}
9598

9699
override fun runInTransaction(body: TransactionWithoutReturn.() -> Unit) {
97-
db.eventQueries.transaction(false, body)
100+
db.eventQueries.writeTransaction(db = db, sentry = sentry, body = body)
98101
}
99102

100103
@Throws(JSONException::class)

0 commit comments

Comments
 (0)