Skip to content

Commit 94af13a

Browse files
authored
Merge pull request #1365 from nrkno/fix/sofie-3618/casparcg-restarts-when-rundown-active
fix(Cronjob): don't restart CasparCG using the nightly cronjob if there's a Rundown active in the Studio (SOFIE-3618)
2 parents b51ee26 + 98e199c commit 94af13a

File tree

2 files changed

+155
-5
lines changed

2 files changed

+155
-5
lines changed

meteor/server/__tests__/cronjobs.test.ts

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import { MeteorMock } from '../../__mocks__/meteor'
44
import { logger } from '../logging'
55
import { getRandomId, getRandomString, literal, protectString } from '../lib/tempLib'
66
import { SnapshotType } from '@sofie-automation/meteor-lib/dist/collections/Snapshots'
7-
import { IBlueprintPieceType, PieceLifespan, StatusCode, TSR } from '@sofie-automation/blueprints-integration'
7+
import {
8+
IBlueprintPieceType,
9+
PieceLifespan,
10+
PlaylistTimingType,
11+
StatusCode,
12+
TSR,
13+
} from '@sofie-automation/blueprints-integration'
814
import {
915
PeripheralDeviceType,
1016
PeripheralDeviceCategory,
@@ -27,6 +33,8 @@ import {
2733
SegmentId,
2834
SnapshotId,
2935
UserActionsLogItemId,
36+
StudioId,
37+
RundownPlaylistId,
3038
} from '@sofie-automation/corelib/dist/dataModel/Ids'
3139

3240
// Set up mocks for tests in this suite
@@ -53,6 +61,8 @@ import {
5361
UserActionsLog,
5462
Segments,
5563
SofieIngestDataCache,
64+
Studios,
65+
RundownPlaylists,
5666
} from '../collections'
5767
import { NrcsIngestCacheType } from '@sofie-automation/corelib/dist/dataModel/NrcsIngestDataCache'
5868
import { JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlob'
@@ -64,7 +74,7 @@ import {
6474
import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment'
6575
import { Settings } from '../Settings'
6676
import { SofieIngestCacheType } from '@sofie-automation/corelib/dist/dataModel/SofieIngestDataCache'
67-
import { ObjectOverrideSetOp } from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
77+
import { ObjectOverrideSetOp, ObjectWithOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
6878

6979
describe('cronjobs', () => {
7080
let env: DefaultEnvironment
@@ -476,7 +486,10 @@ describe('cronjobs', () => {
476486
expect(await Snapshots.findOneAsync(snapshot1)).toBeUndefined()
477487
})
478488
async function insertPlayoutDevice(
479-
props: Pick<PeripheralDevice, 'subType' | 'deviceName' | 'lastSeen' | 'parentDeviceId'> &
489+
props: Pick<
490+
PeripheralDevice,
491+
'subType' | 'deviceName' | 'lastSeen' | 'parentDeviceId' | 'studioAndConfigId'
492+
> &
480493
Partial<Pick<PeripheralDevice, 'token'>>
481494
): Promise<PeripheralDeviceId> {
482495
const deviceId = protectString<PeripheralDeviceId>(getRandomString())
@@ -504,7 +517,10 @@ describe('cronjobs', () => {
504517
return deviceId
505518
}
506519

507-
async function createMockPlayoutGatewayAndDevices(lastSeen: number): Promise<{
520+
async function createMockPlayoutGatewayAndDevices(
521+
lastSeen: number,
522+
studioId?: StudioId
523+
): Promise<{
508524
deviceToken: string
509525
mockPlayoutGw: PeripheralDeviceId
510526
mockCasparCg: PeripheralDeviceId
@@ -516,6 +532,12 @@ describe('cronjobs', () => {
516532
lastSeen: lastSeen,
517533
subType: PERIPHERAL_SUBTYPE_PROCESS,
518534
token: deviceToken,
535+
studioAndConfigId: studioId
536+
? {
537+
configId: '',
538+
studioId,
539+
}
540+
: undefined,
519541
})
520542
const mockCasparCg = await insertPlayoutDevice({
521543
deviceName: 'CasparCG',
@@ -540,6 +562,73 @@ describe('cronjobs', () => {
540562
}
541563
}
542564

565+
async function createMockStudioAndRundown(): Promise<{
566+
studioId: StudioId
567+
rundownPlaylistId: RundownPlaylistId
568+
}> {
569+
function newObjectWithOverrides<T extends {}>(defaults: T): ObjectWithOverrides<T> {
570+
return {
571+
defaults,
572+
overrides: [],
573+
}
574+
}
575+
const studioId = protectString<StudioId>(getRandomString())
576+
await Studios.insertAsync({
577+
_id: studioId,
578+
organizationId: null,
579+
name: 'Studio',
580+
blueprintConfigWithOverrides: newObjectWithOverrides({}),
581+
_rundownVersionHash: '',
582+
lastBlueprintConfig: undefined,
583+
lastBlueprintFixUpHash: undefined,
584+
mappingsWithOverrides: newObjectWithOverrides({}),
585+
supportedShowStyleBase: [],
586+
settingsWithOverrides: newObjectWithOverrides({
587+
allowHold: true,
588+
allowPieceDirectPlay: true,
589+
enableBuckets: true,
590+
enableEvaluationForm: true,
591+
frameRate: 25,
592+
mediaPreviewsUrl: '',
593+
minimumTakeSpan: 1000,
594+
}),
595+
routeSetsWithOverrides: newObjectWithOverrides({}),
596+
routeSetExclusivityGroupsWithOverrides: newObjectWithOverrides({}),
597+
packageContainersWithOverrides: newObjectWithOverrides({}),
598+
previewContainerIds: [],
599+
thumbnailContainerIds: [],
600+
peripheralDeviceSettings: {
601+
deviceSettings: newObjectWithOverrides({}),
602+
ingestDevices: newObjectWithOverrides({}),
603+
inputDevices: newObjectWithOverrides({}),
604+
playoutDevices: newObjectWithOverrides({}),
605+
},
606+
})
607+
608+
const rundownPlaylistId = protectString<RundownPlaylistId>(getRandomString())
609+
await RundownPlaylists.mutableCollection.insertAsync({
610+
_id: rundownPlaylistId,
611+
created: Date.now(),
612+
currentPartInfo: null,
613+
nextPartInfo: null,
614+
externalId: '',
615+
modified: Date.now(),
616+
name: 'Rundown',
617+
previousPartInfo: null,
618+
rundownIdsInOrder: [],
619+
studioId,
620+
timing: {
621+
type: PlaylistTimingType.None,
622+
},
623+
activationId: protectString(''),
624+
})
625+
626+
return {
627+
studioId,
628+
rundownPlaylistId,
629+
}
630+
}
631+
543632
test('Attempts to restart CasparCG when job is enabled', async () => {
544633
const { mockCasparCg, deviceToken } = await createMockPlayoutGatewayAndDevices(Date.now()) // Some time after the threshold
545634

@@ -605,6 +694,28 @@ describe('cronjobs', () => {
605694
expect(logger.info).toHaveBeenLastCalledWith('Nightly cronjob: done')
606695
}, MAX_WAIT_TIME)
607696
})
697+
test('Skips CasparCG in Studios with active Playlists when job is enabled', async () => {
698+
const { studioId } = await createMockStudioAndRundown()
699+
await createMockPlayoutGatewayAndDevices(Date.now(), studioId) // Some time after the threshold
700+
;(logger.info as jest.Mock).mockClear()
701+
// set time to 2020/07/{date} 04:05 Local Time, should be more than 24 hours after 2020/07/19 00:00 UTC
702+
mockCurrentTime = new Date(2020, 6, date++, 4, 5, 0).getTime()
703+
// cronjob is checked every 5 minutes, so advance 6 minutes
704+
await jest.advanceTimersByTimeAsync(6 * 60 * 1000)
705+
706+
await waitUntil(async () => {
707+
// Run timers, so that all promises in the cronjob has a chance to resolve:
708+
const pendingCommands = await PeripheralDeviceCommands.findFetchAsync({})
709+
expect(pendingCommands).toHaveLength(0)
710+
}, MAX_WAIT_TIME)
711+
712+
// make sure that the cronjob ends
713+
await waitUntil(async () => {
714+
// Run timers, so that all promises in the cronjob has a chance to resolve:
715+
await runAllTimers()
716+
expect(logger.info).toHaveBeenLastCalledWith('Nightly cronjob: done')
717+
}, MAX_WAIT_TIME)
718+
})
608719
test('Does not attempt to restart CasparCG when job is disabled', async () => {
609720
await createMockPlayoutGatewayAndDevices(Date.now()) // Some time after the threshold
610721
await setCasparCGCronEnabled(false)

meteor/server/cronjobs.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
translateMessage,
2727
} from '@sofie-automation/corelib/dist/TranslatableMessage'
2828
import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
29+
import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
30+
import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids'
2931

3032
const lowPrioFcn = (fcn: () => any) => {
3133
// Do it at a random time in the future:
@@ -101,9 +103,35 @@ async function restartCasparCG(systemSettings: ICoreSystemSettings | undefined,
101103
subType: 1,
102104
parentDeviceId: 1,
103105
lastSeen: 1,
106+
studioAndConfigId: 1,
104107
},
105108
}
106-
)) as Array<Pick<PeripheralDevice, '_id' | 'subType' | 'parentDeviceId' | 'lastSeen'>>
109+
)) as Array<Pick<PeripheralDevice, '_id' | 'subType' | 'parentDeviceId' | 'lastSeen' | 'studioAndConfigId'>>
110+
111+
const relevantStudioIds = Array.from(
112+
new Set(
113+
casparcgAndParentDevices
114+
.map((device) => device.studioAndConfigId?.studioId)
115+
.filter((id) => id !== undefined)
116+
)
117+
) as StudioId[]
118+
119+
const activePlaylists = (await RundownPlaylists.findFetchAsync(
120+
{
121+
activationId: {
122+
$exists: true,
123+
},
124+
studioId: {
125+
$in: relevantStudioIds,
126+
},
127+
},
128+
{
129+
projection: {
130+
_id: 1,
131+
studioId: 1,
132+
},
133+
}
134+
)) as Array<Pick<DBRundownPlaylist, '_id' | 'studioId'>>
107135

108136
const deviceMap = normalizeArrayToMap(casparcgAndParentDevices, '_id')
109137

@@ -128,6 +156,17 @@ async function restartCasparCG(systemSettings: ICoreSystemSettings | undefined,
128156
continue
129157
}
130158

159+
const activePlaylistUsingDevice = activePlaylists.find(
160+
(playlist) => playlist.studioId === parentDevice.studioAndConfigId?.studioId
161+
)
162+
if (activePlaylistUsingDevice) {
163+
logger.info(
164+
`Cronjob: Skipping CasparCG device "${device._id}" with a parent device belonging to a Studio ("${activePlaylistUsingDevice.studioId}") with an active RundownPlaylist: "${activePlaylistUsingDevice._id}"`
165+
)
166+
// If a Rundown is active during "low season", it's proably best to just let it go until next "low season" the following day, don't retry
167+
continue
168+
}
169+
131170
if (parentDevice.lastSeen < getCurrentTime() - CASPARCG_LAST_SEEN_PERIOD_MS) {
132171
logger.info(`Cronjob: Skipping CasparCG device "${device._id}" with offline parent device`)
133172
shouldRetryAttempt = true

0 commit comments

Comments
 (0)