Skip to content

Commit 7e79970

Browse files
committed
fix(sqlite): avoid shared transaction depth race in subscribers
sqlite reuses one query runner per process, so concurrent scanner saves race on its shared transaction depth counter and wedge it, silently leaving later writes uncommitted. Related-request work now runs directly on event.manager on sqlite instead of nesting a transaction.
1 parent ff50af9 commit 7e79970

3 files changed

Lines changed: 18 additions & 8 deletions

File tree

‎server/subscriber/MediaRequestSubscriber.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import SeasonRequest from '@server/entity/SeasonRequest';
2020
import notificationManager, { Notification } from '@server/lib/notifications';
2121
import { getSettings } from '@server/lib/settings';
2222
import logger from '@server/logger';
23+
import { withNestedTransaction } from '@server/utils/nestedTransaction';
2324
import { isEqual, truncate } from 'lodash';
2425
import type {
2526
EntityManager,
@@ -1034,8 +1035,7 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
10341035
}
10351036

10361037
try {
1037-
// Savepoint on the save's own connection so failures don't abort the request save
1038-
await event.manager.transaction(async (manager) => {
1038+
await withNestedTransaction(event.manager, async (manager) => {
10391039
await this.updateParentStatus(manager, event.entity as MediaRequest);
10401040
});
10411041

@@ -1076,7 +1076,7 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
10761076
}
10771077

10781078
try {
1079-
await event.manager.transaction(async (manager) => {
1079+
await withNestedTransaction(event.manager, async (manager) => {
10801080
await this.updateParentStatus(manager, event.entity as MediaRequest);
10811081
});
10821082
} catch (e) {

‎server/subscriber/MediaSubscriber.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { MediaRequest } from '@server/entity/MediaRequest';
88
import Season from '@server/entity/Season';
99
import SeasonRequest from '@server/entity/SeasonRequest';
1010
import logger from '@server/logger';
11+
import { withNestedTransaction } from '@server/utils/nestedTransaction';
1112
import type {
1213
EntityManager,
1314
EntitySubscriberInterface,
@@ -143,8 +144,7 @@ export class MediaSubscriber implements EntitySubscriberInterface<Media> {
143144
event.entity.status === MediaStatus.AVAILABLE &&
144145
event.databaseEntity?.status === MediaStatus.PENDING
145146
) {
146-
// Savepoint on the save's own connection so failures don't abort the media save
147-
await event.manager.transaction(async (manager) => {
147+
await withNestedTransaction(event.manager, async (manager) => {
148148
await this.updateChildRequestStatus(
149149
manager,
150150
event.entity as Media,
@@ -169,7 +169,7 @@ export class MediaSubscriber implements EntitySubscriberInterface<Media> {
169169
event.entity.status4k === MediaStatus.AVAILABLE &&
170170
event.databaseEntity?.status4k === MediaStatus.PENDING
171171
) {
172-
await event.manager.transaction(async (manager) => {
172+
await withNestedTransaction(event.manager, async (manager) => {
173173
await this.updateChildRequestStatus(
174174
manager,
175175
event.entity as Media,
@@ -230,7 +230,7 @@ export class MediaSubscriber implements EntitySubscriberInterface<Media> {
230230
seasonStatusCheck(false))) &&
231231
validStatuses.includes(event.entity.status)
232232
) {
233-
await event.manager.transaction(async (manager) => {
233+
await withNestedTransaction(event.manager, async (manager) => {
234234
await this.updateRelatedMediaRequest(
235235
manager,
236236
event.entity as Media,
@@ -258,7 +258,7 @@ export class MediaSubscriber implements EntitySubscriberInterface<Media> {
258258
seasonStatusCheck(true))) &&
259259
validStatuses.includes(event.entity.status4k)
260260
) {
261-
await event.manager.transaction(async (manager) => {
261+
await withNestedTransaction(event.manager, async (manager) => {
262262
await this.updateRelatedMediaRequest(
263263
manager,
264264
event.entity as Media,

‎server/utils/nestedTransaction.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { EntityManager } from 'typeorm';
2+
3+
export const withNestedTransaction = <T>(
4+
manager: EntityManager,
5+
run: (manager: EntityManager) => Promise<T>
6+
): Promise<T> =>
7+
// sqlite shares one query runner process-wide, so nesting collides on the savepoint counter
8+
manager.connection.options.type === 'sqlite'
9+
? run(manager)
10+
: manager.transaction(run);

0 commit comments

Comments
 (0)