Skip to content

Commit a6cbf80

Browse files
authored
Merge pull request #1358 from nrkno/fix/sofie-3626/delete-next-partInstance
fix: Next PartInstance can become invalid if it's Part gets deleted, resulting in a critical crash in worker
2 parents 255c27c + 872c528 commit a6cbf80

File tree

8 files changed

+116
-40
lines changed

8 files changed

+116
-40
lines changed

packages/job-worker/src/ingest/commit.ts

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,13 @@ export async function CommitIngestOperation(
178178
// Ensure any adlibbed parts are updated to follow the segmentId of the previous part
179179
await updateSegmentIdsForAdlibbedPartInstances(context, ingestModel, beforePartMap)
180180

181+
// TODO: This whole section can probably be removed later, it's really unneccessary in the grand scheme of
182+
// things, it's here only to debug some problems
181183
if (data.renamedSegments && data.renamedSegments.size > 0) {
182-
logger.debug(`Renamed segments: ${JSON.stringify(Array.from(data.renamedSegments.entries()))}`)
184+
logger.verbose(`Renamed segments: ${JSON.stringify(Array.from(data.renamedSegments.entries()))}`)
183185
}
186+
// End of temporary section
187+
184188
// ensure instances have matching segmentIds with the parts
185189
await updatePartInstancesSegmentIds(context, ingestModel, data.renamedSegments, beforePartMap)
186190

@@ -220,6 +224,8 @@ export async function CommitIngestOperation(
220224
const pSaveIngest = ingestModel.saveAllToDatabase()
221225
pSaveIngest.catch(() => null) // Ensure promise isn't reported as unhandled
222226

227+
ensureNextPartInstanceIsNotDeleted(playoutModel)
228+
223229
await validateAdlibTestingSegment(context, playoutModel)
224230

225231
try {
@@ -273,14 +279,18 @@ function canRemoveSegment(
273279
logger.warn(`Not allowing removal of previous playing segment "${segmentId}", making segment unsynced instead`)
274280
return false
275281
}
276-
if (
277-
currentPartInstance?.segmentId === segmentId ||
278-
(nextPartInstance?.segmentId === segmentId && isTooCloseToAutonext(currentPartInstance, false))
279-
) {
282+
if (currentPartInstance?.segmentId === segmentId) {
280283
// Don't allow removing an active rundown
281284
logger.warn(`Not allowing removal of current playing segment "${segmentId}", making segment unsynced instead`)
282285
return false
283286
}
287+
if (nextPartInstance?.segmentId === segmentId && isTooCloseToAutonext(currentPartInstance, false)) {
288+
// Don't allow removing an active rundown
289+
logger.warn(
290+
`Not allowing removal of nexted segment "${segmentId}", because it's too close to an auto-next, making segment unsynced instead`
291+
)
292+
return false
293+
}
284294

285295
if (nextPartInstance?.segmentId === segmentId && nextPartInstance.orphaned === 'adlib-part') {
286296
// Don't allow removing an active rundown
@@ -365,6 +375,8 @@ async function updatePartInstancesSegmentIds(
365375

366376
const writeOps: AnyBulkWriteOperation<DBPartInstance>[] = []
367377

378+
logger.debug(`updatePartInstancesSegmentIds: renameRules: ${JSON.stringify(renameRules)}`)
379+
368380
for (const [newSegmentId, rule] of rulesInOrder) {
369381
if (rule.fromSegmentIds.length) {
370382
writeOps.push({
@@ -402,32 +414,24 @@ async function updatePartInstancesSegmentIds(
402414
if (writeOps.length) await context.directCollections.PartInstances.bulkWrite(writeOps)
403415

404416
// Double check that there are no parts using the old segment ids:
405-
const oldSegmentIds = Array.from(renameRules.keys())
406-
const [badPartInstances, badParts] = await Promise.all([
407-
await context.directCollections.PartInstances.findFetch({
408-
rundownId: ingestModel.rundownId,
409-
segmentId: { $in: oldSegmentIds },
410-
}),
411-
await context.directCollections.Parts.findFetch({
412-
rundownId: ingestModel.rundownId,
413-
segmentId: { $in: oldSegmentIds },
414-
}),
415-
])
417+
// TODO: This whole section can probably be removed later, it's really unneccessary in the grand scheme of
418+
// things, it's here only to debug some problems
419+
const oldSegmentIds: SegmentId[] = []
420+
for (const renameRule of renameRules.values()) {
421+
oldSegmentIds.push(...renameRule.fromSegmentIds)
422+
}
423+
const badPartInstances = await context.directCollections.PartInstances.findFetch({
424+
rundownId: ingestModel.rundownId,
425+
segmentId: { $in: oldSegmentIds },
426+
})
416427
if (badPartInstances.length > 0) {
417428
logger.error(
418429
`updatePartInstancesSegmentIds: Failed to update all PartInstances using old SegmentIds "${JSON.stringify(
419430
oldSegmentIds
420431
)}": ${JSON.stringify(badPartInstances)}, writeOps: ${JSON.stringify(writeOps)}`
421432
)
422433
}
423-
424-
if (badParts.length > 0) {
425-
logger.error(
426-
`updatePartInstancesSegmentIds: Failed to update all Parts using old SegmentIds "${JSON.stringify(
427-
oldSegmentIds
428-
)}": ${JSON.stringify(badParts)}, writeOps: ${JSON.stringify(writeOps)}`
429-
)
430-
}
434+
// End of the temporary section
431435
}
432436
}
433437

@@ -662,10 +666,27 @@ async function getSelectedPartInstances(
662666
})
663667
: []
664668

669+
const currentPartInstance = instances.find((inst) => inst._id === playlist.currentPartInfo?.partInstanceId)
670+
const nextPartInstance = instances.find((inst) => inst._id === playlist.nextPartInfo?.partInstanceId)
671+
const previousPartInstance = instances.find((inst) => inst._id === playlist.previousPartInfo?.partInstanceId)
672+
673+
if (playlist.currentPartInfo?.partInstanceId && !currentPartInstance)
674+
logger.error(
675+
`playlist.currentPartInfo is set, but PartInstance "${playlist.currentPartInfo?.partInstanceId}" was not found!`
676+
)
677+
if (playlist.nextPartInfo?.partInstanceId && !nextPartInstance)
678+
logger.error(
679+
`playlist.nextPartInfo is set, but PartInstance "${playlist.nextPartInfo?.partInstanceId}" was not found!`
680+
)
681+
if (playlist.previousPartInfo?.partInstanceId && !previousPartInstance)
682+
logger.error(
683+
`playlist.previousPartInfo is set, but PartInstance "${playlist.previousPartInfo?.partInstanceId}" was not found!`
684+
)
685+
665686
return {
666-
currentPartInstance: instances.find((inst) => inst._id === playlist.currentPartInfo?.partInstanceId),
667-
nextPartInstance: instances.find((inst) => inst._id === playlist.nextPartInfo?.partInstanceId),
668-
previousPartInstance: instances.find((inst) => inst._id === playlist.previousPartInfo?.partInstanceId),
687+
currentPartInstance,
688+
nextPartInstance,
689+
previousPartInstance,
669690
}
670691
}
671692

@@ -815,6 +836,16 @@ async function removeSegments(
815836
})
816837
}
817838
for (const segmentId of purgeSegmentIds) {
839+
logger.debug(
840+
`IngestModel: Removing segment "${segmentId}" (` +
841+
`previousPartInfo?.partInstanceId: ${newPlaylist.previousPartInfo?.partInstanceId},` +
842+
`currentPartInfo?.partInstanceId: ${newPlaylist.currentPartInfo?.partInstanceId},` +
843+
`nextPartInfo?.partInstanceId: ${newPlaylist.nextPartInfo?.partInstanceId},` +
844+
`previousPartInstance.segmentId: ${!previousPartInstance ? 'N/A' : previousPartInstance.segmentId},` +
845+
`currentPartInstance.segmentId: ${!currentPartInstance ? 'N/A' : currentPartInstance.segmentId},` +
846+
`nextPartInstance.segmentId: ${!nextPartInstance ? 'N/A' : nextPartInstance.segmentId}` +
847+
`)`
848+
)
818849
ingestModel.removeSegment(segmentId)
819850
}
820851
}
@@ -824,3 +855,12 @@ async function validateAdlibTestingSegment(_context: JobContext, playoutModel: P
824855
rundown.updateAdlibTestingSegmentRank()
825856
}
826857
}
858+
function ensureNextPartInstanceIsNotDeleted(playoutModel: PlayoutModel) {
859+
if (playoutModel.nextPartInstance) {
860+
// Check if the segment of the nextPartInstance exists
861+
if (!playoutModel.findSegment(playoutModel.nextPartInstance.partInstance.segmentId)) {
862+
// The segment doesn't exist, set nextPartInstance to null, it'll be set by ensureNextPartIsValid() later.
863+
playoutModel.setPartInstanceAsNext(null, false, false)
864+
}
865+
}
866+
}

packages/job-worker/src/ingest/lock.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ export interface CommitIngestData {
2222
removedSegmentIds: SegmentId[]
2323
/**
2424
* Segments that had their ids changed. This helps then be orphaned in the correct place
25-
* eg, whole segment is renamed and middle part deleted
26-
* Note: Only supported for MOS, not 'normal' ingest operations
25+
* eg, whole segment is renamed and middle part deleted.
26+
*
27+
* Maps fromSegmentId to toSegmentId.
28+
*
29+
* _Note: Only supported for MOS, not 'normal' ingest operations_
2730
*/
2831
renamedSegments: Map<SegmentId, SegmentId> | null
2932

packages/job-worker/src/ingest/mosDevice/diff.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { IngestSegment } from '@sofie-automation/blueprints-integration'
1111
import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment'
1212
import { CommitIngestData } from '../lock'
1313
import { IngestSegmentModel } from '../model/IngestSegmentModel'
14+
import { logger } from '../../logging'
1415

1516
/**
1617
* Update the Ids of Segments based on new Ingest data
@@ -158,9 +159,11 @@ function applyExternalIdDiff(
158159
}
159160

160161
// Remove the old Segment and it's contents, the new one will be generated shortly
162+
logger.debug(`applyExternalIdDiff: Marking Segment for removing "${oldSegmentId}"`)
161163
ingestModel.removeSegment(oldSegmentId)
162164
} else {
163165
// Perform the rename
166+
logger.debug(`applyExternalIdDiff: Marking Segment for renaming "${oldSegmentId}" -> "${newSegmentId}"`)
164167
ingestModel.changeSegmentId(oldSegmentId, newSegmentId)
165168
}
166169
}

packages/job-worker/src/ingest/syncChangesToPartInstance.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { validateAdlibTestingPartInstanceProperties } from '../playout/adlibTest
3333
import { ReadonlyDeep } from 'type-fest'
3434
import { convertIngestModelToPlayoutRundownWithSegments } from './commit'
3535
import { PlayoutRundownModel } from '../playout/model/PlayoutRundownModel'
36+
import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance'
3637

3738
type PlayStatus = 'previous' | 'current' | 'next'
3839
type SyncedInstance = {
@@ -143,15 +144,29 @@ export async function syncChangesToPartInstances(
143144
if (!playoutRundownModelForPart)
144145
throw new Error(`Internal Error: playoutRundownModelForPart is undefined (it should never be)`)
145146

146-
const proposedPieceInstances = getPieceInstancesForPart(
147-
context,
148-
playoutModel,
149-
previousPartInstance,
150-
playoutRundownModelForPart,
151-
part,
152-
await piecesThatMayBeActive,
153-
existingPartInstance.partInstance._id
154-
)
147+
// TMP: wrap in try/catch for troubleshooting:
148+
let proposedPieceInstances: PieceInstance[] = []
149+
try {
150+
proposedPieceInstances = getPieceInstancesForPart(
151+
context,
152+
playoutModel,
153+
previousPartInstance,
154+
playoutRundownModelForPart,
155+
part,
156+
await piecesThatMayBeActive,
157+
existingPartInstance.partInstance._id
158+
)
159+
} catch (e) {
160+
logger.error(
161+
`TROUBLESHOOTING: currentPartInstance: ${JSON.stringify(playoutModel.currentPartInstance)}`
162+
)
163+
logger.error(`TROUBLESHOOTING: nextPartInstance: ${JSON.stringify(playoutModel.nextPartInstance)}`)
164+
logger.error(
165+
`TROUBLESHOOTING: previousPartInstance: ${JSON.stringify(playoutModel.previousPartInstance)}`
166+
)
167+
168+
throw e
169+
}
155170

156171
logger.info(`Syncing ingest changes for part: ${partId} (orphaned: ${!!newPart})`)
157172

packages/job-worker/src/playout/adlibJobs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ export async function handleDisableNextPiece(context: JobContext, data: DisableN
438438

439439
return sortedPieces.find((piece) => {
440440
return (
441+
piece.pieceInstance.piece.enable.start !== 'now' &&
441442
piece.pieceInstance.piece.enable.start >= nowInPart &&
442443
((!data.undo && !piece.pieceInstance.disabled) || (data.undo && piece.pieceInstance.disabled))
443444
)

packages/job-worker/src/playout/infinites.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,9 @@ export function getPieceInstancesForPart(
332332

333333
playingSegment = playingRundown.getSegment(playingPartInstance.partInstance.segmentId)
334334
if (!playingSegment) {
335+
// Double check that there are no parts using the old segment ids:
336+
// TODO: This whole section can probably be removed later, it's really unneccessary in the grand scheme of
337+
// things, it's here only to debug some problems
335338
const rundownId = playingRundown.rundown._id
336339
context.directCollections.Segments.findFetch({
337340
rundownId: rundownId,
@@ -344,11 +347,14 @@ export function getPieceInstancesForPart(
344347
)
345348
})
346349
.catch((e) => logger.error(e))
350+
// End of temporary section
347351

348352
throw new Error(
349353
`Segment "${playingPartInstance.partInstance.segmentId}" in Rundown "${
350354
playingRundown.rundown._id
351-
}" not found! (other segments: ${JSON.stringify(playingRundown.segments.map((s) => s.segment._id))})`
355+
}" not found! (partInstanceId: "${
356+
playingPartInstance.partInstance._id
357+
}", other segments: ${JSON.stringify(playingRundown.segments.map((s) => s.segment._id))})`
352358
)
353359
}
354360
}

packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,12 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
567567
this.#baselineHelper.saveAllToDatabase(),
568568
])
569569

570+
// Clean up deleted partInstances, since they have now been deleted by writePartInstancesAndPieceInstances
571+
for (const [partInstanceId, partInstance] of this.allPartInstances) {
572+
if (partInstance !== null) continue
573+
this.allPartInstances.delete(partInstanceId)
574+
}
575+
570576
this.#playlistHasChanged = false
571577

572578
// Execute deferAfterSave()'s

packages/job-worker/src/playout/setNext.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,9 +279,11 @@ async function cleanupOrphanedItems(context: JobContext, playoutModel: PlayoutMo
279279

280280
const selectedPartInstancesSegmentIds = new Set<SegmentId>()
281281

282+
const previousPartInstance = playoutModel.previousPartInstance?.partInstance
282283
const currentPartInstance = playoutModel.currentPartInstance?.partInstance
283284
const nextPartInstance = playoutModel.nextPartInstance?.partInstance
284285

286+
if (previousPartInstance) selectedPartInstancesSegmentIds.add(previousPartInstance.segmentId)
285287
if (currentPartInstance) selectedPartInstancesSegmentIds.add(currentPartInstance.segmentId)
286288
if (nextPartInstance) selectedPartInstancesSegmentIds.add(nextPartInstance.segmentId)
287289

@@ -291,7 +293,7 @@ async function cleanupOrphanedItems(context: JobContext, playoutModel: PlayoutMo
291293

292294
const alterSegmentsFromRundowns = new Map<RundownId, { deleted: SegmentId[]; hidden: SegmentId[] }>()
293295
for (const segment of segments) {
294-
// If the segment is orphaned and not the segment for the next or current partinstance
296+
// If the segment is orphaned and not the segment for the previous, current or next partInstance
295297
if (!selectedPartInstancesSegmentIds.has(segment.segment._id)) {
296298
let rundownSegments = alterSegmentsFromRundowns.get(segment.segment.rundownId)
297299
if (!rundownSegments) {

0 commit comments

Comments
 (0)