Skip to content

Commit 77dba30

Browse files
committed
fix(EAV-737): prevent latency when running long callbacks after updating the timeline
moves timeline generation to `saveAllToDatabase`
1 parent d469cbd commit 77dba30

3 files changed

Lines changed: 64 additions & 26 deletions

File tree

packages/job-worker/src/playout/model/PlayoutModel.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,12 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa
387387
*/
388388
getNowInPlayout(): Time
389389

390+
/**
391+
* Mark the playlist as needing a timeline update.
392+
* The timeline will be generated and published when model is ready to be saved.
393+
*/
394+
markTimelineNeedsUpdate(): void
395+
390396
/** Lifecycle */
391397

392398
/**

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout
7171
import { NotificationsModelHelper } from '../../../notifications/NotificationsModelHelper.js'
7272
import { getExpectedLatency } from '@sofie-automation/corelib/dist/studio/playout'
7373
import { ExpectedPackage } from '@sofie-automation/blueprints-integration'
74+
import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError'
75+
import {
76+
getTimelineRundown,
77+
flattenAndProcessTimelineObjects,
78+
preserveOrReplaceNowTimesInObjects,
79+
logAnyRemainingNowTimes,
80+
} from '../../timeline/generate.js'
81+
import { deNowifyMultiGatewayTimeline } from '../../timeline/multi-gateway.js'
7482

7583
export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly {
7684
public readonly playlistId: RundownPlaylistId
@@ -315,6 +323,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
315323

316324
#playlistHasChanged = false
317325
#timelineHasChanged = false
326+
#timelineNeedsRegeneration = false
318327

319328
#pendingPartInstanceTimingEvents = new Set<PartInstanceId>()
320329

@@ -694,6 +703,12 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
694703
}
695704
this.#deferredBeforeSaveFunctions.length = 0 // clear the array
696705

706+
// Generate timeline if needed
707+
if (this.#timelineNeedsRegeneration) {
708+
await this.#regenerateTimeline()
709+
this.#timelineNeedsRegeneration = false
710+
}
711+
697712
// Prioritise the timeline for publication reasons
698713
if (this.#timelineHasChanged && this.timelineImpl) {
699714
// Do a fast-track for the timeline to be published faster:
@@ -889,6 +904,10 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
889904
return result
890905
}
891906

907+
markTimelineNeedsUpdate(): void {
908+
this.#timelineNeedsRegeneration = true
909+
}
910+
892911
/** Notifications */
893912

894913
async getAllNotifications(
@@ -925,6 +944,38 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
925944

926945
/** BaseModel */
927946

947+
async #regenerateTimeline(): Promise<void> {
948+
const span = this.context.startSpan('PlayoutModelImpl.regenerateTimeline')
949+
logger.debug('Regenerating timeline...')
950+
951+
try {
952+
const {
953+
versions,
954+
objs: timelineObjs,
955+
timingContext: timingInfo,
956+
regenerateTimelineToken
957+
} = await getTimelineRundown(this.context, this as PlayoutModel)
958+
959+
flattenAndProcessTimelineObjects(this.context, timelineObjs)
960+
961+
preserveOrReplaceNowTimesInObjects(this, timelineObjs)
962+
963+
if (this.isMultiGatewayMode) {
964+
deNowifyMultiGatewayTimeline(this as PlayoutModel, timelineObjs, timingInfo)
965+
966+
logAnyRemainingNowTimes(this.context, timelineObjs)
967+
}
968+
969+
const timelineHash = this.setTimeline(timelineObjs, versions, regenerateTimelineToken).timelineHash
970+
logger.verbose(`Timeline regeneration done, hash: "${timelineHash}"`)
971+
} catch (err) {
972+
logger.error(`Error regenerating timeline: ${stringifyError(err)}`)
973+
throw err
974+
} finally {
975+
if (span) span.end()
976+
}
977+
}
978+
928979
/**
929980
* Assert that no changes should have been made to the model, will throw an Error otherwise. This can be used in
930981
* place of `saveAllToDatabase()`, when the code controlling the model expects no changes to have been made and any

packages/job-worker/src/playout/timeline/generate.ts

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import { deserializePieceTimelineObjectsBlob } from '@sofie-automation/corelib/d
3737
import { convertResolvedPieceInstanceToBlueprints } from '../../blueprints/context/lib.js'
3838
import { buildTimelineObjsForRundown, RundownTimelineTimingContext } from './rundown.js'
3939
import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase'
40-
import { deNowifyMultiGatewayTimeline } from './multi-gateway.js'
4140
import { validateTimeline } from 'superfly-timeline'
4241
import { getPartTimingsOrDefaults, PartCalculatedTimings } from '@sofie-automation/corelib/dist/playout/timings'
4342
import { applyAbPlaybackForTimeline } from '../abPlayback/index.js'
@@ -140,39 +139,21 @@ export async function updateStudioTimeline(
140139

141140
export async function updateTimeline(context: JobContext, playoutModel: PlayoutModel): Promise<void> {
142141
const span = context.startSpan('updateTimeline')
143-
logger.debug('updateTimeline running...')
142+
logger.debug('updateTimeline: marking playlist as needing timeline update')
144143

145144
if (!playoutModel.playlist.activationId) {
146145
throw new Error(`RundownPlaylist ("${playoutModel.playlist._id}") is not active")`)
147146
}
148147

149-
const {
150-
versions,
151-
objs: timelineObjs,
152-
timingContext: timingInfo,
153-
regenerateTimelineToken,
154-
} = await getTimelineRundown(context, playoutModel)
155-
156-
flattenAndProcessTimelineObjects(context, timelineObjs)
157-
158-
preserveOrReplaceNowTimesInObjects(playoutModel, timelineObjs)
159-
160-
if (playoutModel.isMultiGatewayMode) {
161-
deNowifyMultiGatewayTimeline(playoutModel, timelineObjs, timingInfo)
162-
163-
logAnyRemainingNowTimes(context, timelineObjs)
164-
}
165-
166-
const timelineHash = playoutModel.setTimeline(timelineObjs, versions, regenerateTimelineToken).timelineHash
167-
logger.verbose(`updateTimeline done, hash: "${timelineHash}"`)
148+
playoutModel.markTimelineNeedsUpdate()
168149

169150
if (span) span.end()
170151
}
171152

172-
function preserveOrReplaceNowTimesInObjects(
153+
export function preserveOrReplaceNowTimesInObjects(
173154
studioPlayoutModel: StudioPlayoutModelBase,
174155
timelineObjs: Array<TimelineObjGeneric>
175-
) {
156+
): void {
176157
const timeline = studioPlayoutModel.timeline
177158
const oldTimelineObjsMap = normalizeArray(
178159
(timeline?.timelineBlob !== undefined && deserializeTimelineBlob(timeline.timelineBlob)) || [],
@@ -202,7 +183,7 @@ function preserveOrReplaceNowTimesInObjects(
202183
})
203184
}
204185

205-
function logAnyRemainingNowTimes(_context: JobContext, timelineObjs: Array<TimelineObjGeneric>): void {
186+
export function logAnyRemainingNowTimes(_context: JobContext, timelineObjs: Array<TimelineObjGeneric>): void {
206187
const badTimelineObjs: any[] = []
207188

208189
for (const obj of timelineObjs) {
@@ -287,7 +268,7 @@ function getPartInstanceTimelineInfo(
287268
/**
288269
* Returns timeline objects related to rundowns in a studio
289270
*/
290-
async function getTimelineRundown(
271+
export async function getTimelineRundown(
291272
context: JobContext,
292273
playoutModel: PlayoutModel
293274
): Promise<{
@@ -541,7 +522,7 @@ function createRegenerateTimelineObj(
541522
* @param context
542523
* @param timelineObjs Array of timeline objects
543524
*/
544-
function flattenAndProcessTimelineObjects(context: JobContext, timelineObjs: Array<TimelineObjGeneric>): void {
525+
export function flattenAndProcessTimelineObjects(context: JobContext, timelineObjs: Array<TimelineObjGeneric>): void {
545526
const span = context.startSpan('processTimelineObjects')
546527

547528
// first, split out any grouped objects, to make the timeline shallow:

0 commit comments

Comments
 (0)