Skip to content

Commit 3373b62

Browse files
fix: avoiding IDs override/conflict while creating concurrent runtime notifications (#342)
* fix: avoiding IDs override while creating conturrent runtime notifications * create record in 'getNextIdForEntity' function in overlay for entityname, if it does not exist * fix: notifications unit tests
1 parent 6167139 commit 3373b62

File tree

6 files changed

+85
-55
lines changed

6 files changed

+85
-55
lines changed

src/mappings/token/utils.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,8 @@ export async function getHolderAccountsForToken(
327327
const holders = await em.getRepository(TokenAccount).findBy({ tokenId })
328328

329329
const holdersMemberIds = holders
330-
.filter((follower) => follower?.memberId)
331-
.map((follower) => follower.memberId as string)
330+
.filter((holder) => holder?.memberId)
331+
.map((holder) => holder.memberId as string)
332332

333333
const limit = pLimit(10) // Limit to 10 concurrent promises
334334
const holdersAccounts: (Account | null)[] = await Promise.all(
@@ -347,17 +347,17 @@ export async function notifyTokenHolders(
347347
event?: Event,
348348
dispatchBlock?: number
349349
) {
350-
const holdersAccounts = await getHolderAccountsForToken(em, tokenId)
350+
const holderAccounts = await getHolderAccountsForToken(em, tokenId)
351351

352352
const limit = pLimit(10) // Limit to 10 concurrent promises
353353

354354
await Promise.all(
355-
holdersAccounts.map((holdersAccount) =>
355+
holderAccounts.map((holderAccount) =>
356356
limit(() =>
357357
addNotification(
358358
em,
359-
holdersAccount,
360-
new MemberRecipient({ membership: holdersAccount.membershipId }),
359+
holderAccount,
360+
new MemberRecipient({ membership: holderAccount.membershipId }),
361361
notificationType,
362362
event,
363363
dispatchBlock

src/tests/integration/notifications.test.ts

+39-31
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import { EntityManager } from 'typeorm'
2-
import { IMemberRemarked, ReactVideo, MemberRemarked } from '@joystream/metadata-protobuf'
1+
import { IMemberRemarked, MemberRemarked, ReactVideo } from '@joystream/metadata-protobuf'
32
import { AnyMetadataClass } from '@joystream/metadata-protobuf/types'
4-
import { defaultTestBlock, populateDbWithSeedData } from './testUtils'
5-
import { globalEm } from '../../utils/globalEm'
6-
import {
7-
excludeChannelService,
8-
verifyChannelService,
9-
} from '../../server-extension/resolvers/ChannelsResolver'
3+
import { Store } from '@subsquid/typeorm-store'
4+
import { expect } from 'chai'
5+
import { config as dontenvConfig } from 'dotenv'
6+
import Long from 'long'
7+
import path from 'path'
8+
import { EntityManager } from 'typeorm'
9+
import { auctionBidMadeInner } from '../../mappings/content/nft'
10+
import { processMemberRemarkedEvent } from '../../mappings/membership'
11+
import { backwardCompatibleMetaID } from '../../mappings/utils'
1012
import {
1113
Account,
1214
Channel,
@@ -26,21 +28,19 @@ import {
2628
Video,
2729
VideoLiked,
2830
} from '../../model'
29-
import { expect } from 'chai'
31+
import { setFeaturedNftsInner } from '../../server-extension/resolvers/AdminResolver'
32+
import {
33+
excludeChannelService,
34+
verifyChannelService,
35+
} from '../../server-extension/resolvers/ChannelsResolver'
36+
import { excludeVideoService } from '../../server-extension/resolvers/VideosResolver'
37+
import { globalEm } from '../../utils/globalEm'
3038
import {
3139
OFFCHAIN_NOTIFICATION_ID_TAG,
3240
RUNTIME_NOTIFICATION_ID_TAG,
3341
} from '../../utils/notification/helpers'
34-
import { setFeaturedNftsInner } from '../../server-extension/resolvers/AdminResolver'
35-
import { auctionBidMadeInner } from '../../mappings/content/nft'
36-
import { EntityManagerOverlay } from '../../utils/overlay'
37-
import { Store } from '@subsquid/typeorm-store'
38-
import { processMemberRemarkedEvent } from '../../mappings/membership'
39-
import Long from 'long'
40-
import { backwardCompatibleMetaID } from '../../mappings/utils'
41-
import { config as dontenvConfig } from 'dotenv'
42-
import path from 'path'
43-
import { excludeVideoService } from '../../server-extension/resolvers/VideosResolver'
42+
import { AnyEntity, Constructor, EntityManagerOverlay } from '../../utils/overlay'
43+
import { defaultTestBlock, populateDbWithSeedData } from './testUtils'
4444

4545
dontenvConfig({
4646
path: path.resolve(__dirname, './.env'),
@@ -50,9 +50,19 @@ const metadataToBytes = <T>(metaClass: AnyMetadataClass<T>, obj: T): Uint8Array
5050
return Buffer.from(metaClass.encode(obj).finish())
5151
}
5252

53-
const getNextNotificationId = async (em: EntityManager, onchain: boolean) => {
53+
const getNextNotificationId = async (
54+
store: EntityManager | EntityManagerOverlay,
55+
onchain: boolean
56+
) => {
5457
const tag = onchain ? RUNTIME_NOTIFICATION_ID_TAG : OFFCHAIN_NOTIFICATION_ID_TAG
55-
const row = await em.getRepository(NextEntityId).findOneBy({ entityName: tag })
58+
if (store instanceof EntityManagerOverlay) {
59+
const row = await store
60+
.getRepository(NextEntityId as Constructor<NextEntityId & AnyEntity>)
61+
.getOneBy({ entityName: tag })
62+
const id = parseInt(row?.nextId.toString() || '1')
63+
return id
64+
}
65+
const row = await store.getRepository(NextEntityId).findOneBy({ entityName: tag })
5666
const id = parseInt(row?.nextId.toString() || '1')
5767
return id
5868
}
@@ -69,6 +79,7 @@ describe('notifications tests', () => {
6979
let em: EntityManager
7080
before(async () => {
7181
em = await globalEm
82+
overlay = await createOverlay()
7283
await populateDbWithSeedData()
7384
})
7485
describe('👉 YPP Verify channel', () => {
@@ -281,10 +292,9 @@ describe('notifications tests', () => {
281292

282293
before(async () => {
283294
const bidAmount = BigInt(100000)
284-
nextNotificationIdPre = await getNextNotificationId(em, true)
295+
nextNotificationIdPre = await getNextNotificationId(overlay, true)
285296
notificationId = RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre
286297
nft = await em.getRepository(OwnedNft).findOneByOrFail({ videoId })
287-
overlay = await createOverlay()
288298

289299
await auctionBidMadeInner(
290300
overlay,
@@ -368,8 +378,7 @@ describe('notifications tests', () => {
368378
asV2001: ['3', metadataToBytes(MemberRemarked, metadataMessage), undefined],
369379
} as any
370380
before(async () => {
371-
overlay = await createOverlay()
372-
nextNotificationIdPre = await getNextNotificationId(em, true)
381+
nextNotificationIdPre = await getNextNotificationId(overlay, true)
373382
notificationId = RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre.toString()
374383
})
375384
it('should process video liked and deposit notification', async () => {
@@ -381,7 +390,7 @@ describe('notifications tests', () => {
381390
event,
382391
})
383392

384-
const nextNotificationId = await getNextNotificationId(em, true)
393+
const nextNotificationId = await getNextNotificationId(overlay, true)
385394
notification = (await overlay
386395
.getRepository(Notification)
387396
.getByIdOrFail(notificationId)) as Notification
@@ -422,8 +431,7 @@ describe('notifications tests', () => {
422431
asV2001: ['2', metadataToBytes(MemberRemarked, metadataMessage), undefined], // avoid comment author == creator
423432
} as any
424433
before(async () => {
425-
overlay = await createOverlay()
426-
nextNotificationIdPre = await getNextNotificationId(em, true)
434+
nextNotificationIdPre = await getNextNotificationId(overlay, true)
427435
notificationId = RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre.toString()
428436
})
429437
it('should process comment to video and deposit notification', async () => {
@@ -435,7 +443,7 @@ describe('notifications tests', () => {
435443
event,
436444
})
437445

438-
const nextNotificationId = await getNextNotificationId(em, true)
446+
const nextNotificationId = await getNextNotificationId(overlay, true)
439447
notification = (await overlay
440448
.getRepository(Notification)
441449
.getByIdOrFail(notificationId)) as Notification | null
@@ -485,7 +493,7 @@ describe('notifications tests', () => {
485493
} as any
486494

487495
before(async () => {
488-
nextNotificationIdPre = await getNextNotificationId(em, true)
496+
nextNotificationIdPre = await getNextNotificationId(overlay, true)
489497
notificationId = RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre.toString()
490498

491499
await processMemberRemarkedEvent({
@@ -500,7 +508,7 @@ describe('notifications tests', () => {
500508
describe('should process reply to comment and deposit notification', () => {
501509
let nextNotificationId: number
502510
before(async () => {
503-
nextNotificationId = await getNextNotificationId(em, true)
511+
nextNotificationId = await getNextNotificationId(overlay, true)
504512
notification = (await overlay
505513
.getRepository(Notification)
506514
.getByIdOrFail(notificationId)) as Notification | null

src/utils/nextEntityId.ts

+29-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,40 @@
11
import { EntityManager } from 'typeorm'
22
import { NextEntityId } from '../model'
3+
import { AnyEntity, Constructor, EntityManagerOverlay } from './overlay'
34

4-
// used to retrieve the next id for an entity
5-
export async function getNextIdForEntity(em: EntityManager, entityName: string): Promise<number> {
5+
// used to retrieve the next id for an entity from NextEntityId table using either EntityManager or Overlay
6+
export async function getNextIdForEntity(
7+
store: EntityManager | EntityManagerOverlay,
8+
entityName: string
9+
): Promise<number> {
10+
// Get next entity id from overlay (this will mostly be used in the mappings context)
11+
if (store instanceof EntityManagerOverlay) {
12+
const row = await store
13+
.getRepository(NextEntityId as Constructor<NextEntityId & AnyEntity>)
14+
.getOneBy({ entityName: entityName })
15+
16+
const id = parseInt(row?.nextId.toString() || '1')
17+
18+
// Update the id to be the next one in the overlay
19+
if (row) {
20+
row.nextId++
21+
} else {
22+
store
23+
.getRepository(NextEntityId as Constructor<NextEntityId & AnyEntity>)
24+
.new({ entityName, nextId: id + 1 })
25+
}
26+
27+
return id
28+
}
29+
30+
// Get next entity id from EntityManager (this will mostly be used in the graphql-server/auth-api context)
631
let row: NextEntityId | null
732
if (process.env.TESTING === 'true' || process.env.TESTING === '1') {
8-
row = await em.getRepository(NextEntityId).findOne({
33+
row = await store.getRepository(NextEntityId).findOne({
934
where: { entityName },
1035
})
1136
} else {
12-
row = await em.getRepository(NextEntityId).findOne({
37+
row = await store.getRepository(NextEntityId).findOne({
1338
where: { entityName },
1439
lock: { mode: 'pessimistic_write' },
1540
})

src/utils/notification/helpers.ts

+6-7
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ async function addOffChainNotification(
159159
const nextNotificationId = await getNextIdForEntity(em, OFFCHAIN_NOTIFICATION_ID_TAG)
160160

161161
const notification = createNotification(
162-
OFFCHAIN_NOTIFICATION_ID_TAG + '-' + nextNotificationId.toString(),
162+
`${OFFCHAIN_NOTIFICATION_ID_TAG}-${nextNotificationId}`,
163163
account.id,
164164
recipient,
165165
notificationType,
@@ -186,21 +186,22 @@ async function addRuntimeNotification(
186186
event: Event,
187187
dispatchBlock?: number
188188
) {
189-
const em = overlay.getEm()
190189
// get notification Id from orion_db in any case
191-
const nextNotificationId = await getNextIdForEntity(em, RUNTIME_NOTIFICATION_ID_TAG)
190+
const nextNotificationId = await getNextIdForEntity(overlay, RUNTIME_NOTIFICATION_ID_TAG)
191+
192+
const runtimeNotificationId = `${RUNTIME_NOTIFICATION_ID_TAG}-${nextNotificationId}`
192193

193194
// check that on-notification is not already present in orion_db in case the processor has been restarted (but not orion_db)
194195
const existingNotification = await overlay
195196
.getRepository(Notification)
196-
.getById(nextNotificationId.toString())
197+
.getById(runtimeNotificationId)
197198

198199
if (existingNotification) {
199200
return
200201
}
201202

202203
const notification = createNotification(
203-
RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationId.toString(),
204+
runtimeNotificationId,
204205
account.id,
205206
recipient,
206207
notificationType,
@@ -216,8 +217,6 @@ async function addRuntimeNotification(
216217
await createEmailNotification(overlay, notification)
217218
}
218219

219-
await saveNextNotificationId(em, nextNotificationId + 1, RUNTIME_NOTIFICATION_ID_TAG)
220-
221220
return notification.id
222221
}
223222

src/utils/notification/index.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
export {
2+
addNotification,
23
defaultNotificationPreferences,
34
preferencesForNotification,
4-
addNotification,
55
} from './helpers'
6-
// export * from './notificationTexts'
7-
// export * from './notificationLinks'

src/utils/overlay.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -345,16 +345,16 @@ export class EntityManagerOverlay {
345345
constructor(
346346
private em: EntityManager,
347347
private nextEntityIds: NextEntityId[],
348-
private afterDbUpdte: (em: EntityManager) => Promise<void>
348+
private afterDbUpdate: (em: EntityManager) => Promise<void>
349349
) {}
350350

351-
public static async create(store: Store, afterDbUpdte: (em: EntityManager) => Promise<void>) {
351+
public static async create(store: Store, afterDbUpdate: (em: EntityManager) => Promise<void>) {
352352
// FIXME: This is a little hacky, but we really need to access the underlying EntityManager
353353
const em = await (store as unknown as { em: () => Promise<EntityManager> }).em()
354354
// Add "admin" schema to search path in order to be able to access "hidden" entities
355355
await em.query('SET search_path TO admin,public')
356356
const nextEntityIds = await em.find(NextEntityId, {})
357-
return new EntityManagerOverlay(em, nextEntityIds, afterDbUpdte)
357+
return new EntityManagerOverlay(em, nextEntityIds, afterDbUpdate)
358358
}
359359

360360
public totalCacheSize() {
@@ -400,6 +400,6 @@ export class EntityManagerOverlay {
400400
})
401401
)
402402
await this.em.save(nextIds)
403-
await this.afterDbUpdte(this.em)
403+
await this.afterDbUpdate(this.em)
404404
}
405405
}

0 commit comments

Comments
 (0)