Skip to content

Commit 7b49119

Browse files
authored
Rehydrate shortly upcoming talks from PentaDb to ensure short-notice changes are picked up (#168)
1 parent 61caeb8 commit 7b49119

File tree

6 files changed

+97
-2
lines changed

6 files changed

+97
-2
lines changed

src/Scheduler.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,16 @@ export class Scheduler {
191191
LogService.info("Scheduler", "Scheduling tasks");
192192
try {
193193
const minVar = config.conference.lookaheadMinutes;
194+
try {
195+
// Refresh upcoming parts of our schedule to ensure it's really up to date.
196+
// Rationale: Sometimes schedules get changed at short notice, so we try our best to accommodate that.
197+
// Rationale for adding 1 minute: so we don't cut it too close to the wire; whilst processing the refresh,
198+
// time may slip forward.
199+
await this.conference.backend.refreshShortTerm((minVar + 1) * 60);
200+
} catch (e) {
201+
LogService.error("Scheduler", `Failed short-term schedule refresh: ${e.message ?? e}\n${e.stack ?? '?'}`);
202+
}
203+
194204
const upcomingTalks = await this.conference.getUpcomingTalkStarts(minVar, minVar);
195205
const upcomingQA = await this.conference.getUpcomingQAStarts(minVar, minVar);
196206
const upcomingEnds = await this.conference.getUpcomingTalkEnds(minVar, minVar);

src/backends/CachingBackend.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ export class CachingBackend implements IScheduleBackend {
4242

4343
private wasCached: boolean = true;
4444

45+
/**
46+
* We hang on to the last backend for refreshing short-notice alterations.
47+
* (I don't particularly like it, but there isn't time to reason through a better approach at the moment.)
48+
*/
49+
private lastUsedBackend: IScheduleBackend | null = null;
50+
4551
/**
4652
* @param underlyingBackend A factory for the underlying backend, which will be reconstructed each time we try to refresh.
4753
*/
@@ -76,6 +82,7 @@ export class CachingBackend implements IScheduleBackend {
7682

7783
async refresh(): Promise<void> {
7884
const backend = await this.underlyingBackend();
85+
this.lastUsedBackend = backend;
7986

8087
this.conference = backend.conference;
8188
this.talks = backend.talks;
@@ -91,6 +98,15 @@ export class CachingBackend implements IScheduleBackend {
9198
}
9299
}
93100

101+
async refreshShortTerm(lookaheadSeconds: number): Promise<void> {
102+
if (this.lastUsedBackend !== null) {
103+
// It's notable that we don't save any changes to disk.
104+
// It wouldn't be a bad idea to persist the changes, but introducing a lot of disk I/O into this frequent operation
105+
// made me uncomfortable.
106+
await this.lastUsedBackend.refreshShortTerm(lookaheadSeconds);
107+
}
108+
}
109+
94110
private async saveCacheToDisk(): Promise<void> {
95111
// Save a cached copy.
96112
// Do it atomically so that there's very little chance of anything going wrong: write to a file first, then move into place.

src/backends/IScheduleBackend.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ export interface IScheduleBackend {
1919
*/
2020
refresh(): Promise<void>;
2121

22+
/**
23+
* Calling this function requests the schedule backend to look ahead `lookaheadSeconds` seconds into the future
24+
* and try its best to ensure whatever talks in that window of time are up-to-date in the backend's view of the schedule.
25+
*
26+
* This is an ugly hack to support short-notice changes to the conference schedule, as happens in real life.
27+
* It is principally expected to be called by the Scheduler when scheduling tasks in the short-term future.
28+
*/
29+
refreshShortTerm(lookaheadSeconds: number): Promise<void>;
30+
2231
/**
2332
* Returns true iff the current schedule was loaded from cache, rather than from the intended source.
2433
* This happens if there was a problem loading the schedule from the intended source for some reason.

src/backends/json/JsonScheduleBackend.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ export class JsonScheduleBackend implements IScheduleBackend {
7171
this.wasFromCache = false;
7272
}
7373

74+
async refreshShortTerm(_lookaheadSeconds: number): Promise<void> {
75+
// NOP: There's no way to partially refresh a JSON schedule.
76+
// Short-term changes to a JSON schedule are therefore currently unimplemented.
77+
// This hack was intended for Penta anyway.
78+
}
79+
7480
get conference(): IConference {
7581
return this.loader.conference;
7682
};

src/backends/penta/PentaBackend.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { PentaDb } from "./db/PentaDb";
55
import { PentabarfParser } from "./PentabarfParser";
66
import * as fetch from "node-fetch";
77
import { LogService } from "matrix-bot-sdk";
8+
import { IDbTalk } from "./db/DbTalk";
89

910

1011
export class PentaBackend implements IScheduleBackend {
@@ -69,12 +70,23 @@ export class PentaBackend implements IScheduleBackend {
6970
private async hydrateTalk(talk: ITalk): Promise<void> {
7071
const dbTalk = await this.db.getTalk(talk.id);
7172
if (dbTalk === null) return;
73+
this.rehydrateTalkFrom(talk, dbTalk);
74+
}
7275

76+
private rehydrateTalkFrom(talk: ITalk, dbTalk: IDbTalk): void {
7377
if (talk.qa_startTime !== null) {
7478
// hydrate Q&A time if enabled
79+
// Rationale for hydrating Q&A time: it's not available in the Pentabarf XML.
7580
talk.qa_startTime = dbTalk.qa_start_datetime;
7681
}
82+
7783
talk.livestream_endTime = dbTalk.livestream_end_datetime;
84+
85+
// Rationale for hydrating talk start & end time: there can be short-notice alterations to the schedule
86+
// (and rehydrating talks is how `refreshShortTerm` is implemented)
87+
// and during testing, the PentaDB can have a time shift set which changes the time of talks compared to the XML.
88+
talk.startTime = dbTalk.start_datetime;
89+
talk.endTime = dbTalk.end_datetime;
7890
}
7991

8092
private async hydratePerson(person: IPerson): Promise<void> {
@@ -114,6 +126,23 @@ export class PentaBackend implements IScheduleBackend {
114126
throw new Error("refresh() not implemented for Penta backend.");
115127
}
116128

129+
/**
130+
* See description on `IScheduleBackend`.
131+
*
132+
* For the penta backend, we consult the database for short-notice alterations and rehydrate any affected talks.
133+
*/
134+
async refreshShortTerm(lookaheadSeconds: number): Promise<void> {
135+
const talksOfInterest = await this.db.getTalksWithUpcomingEvents(lookaheadSeconds / 60);
136+
for (const dbTalk of talksOfInterest) {
137+
const talk = this.talks.get(dbTalk.event_id);
138+
if (talk === undefined) {
139+
LogService.warn("PentaBackend", `refreshShortTerm: DB talk '${dbTalk.event_id}' is upcoming but has no talk entry to hydrate.`);
140+
continue;
141+
}
142+
this.rehydrateTalkFrom(talk, dbTalk);
143+
}
144+
}
145+
117146
conference: IConference;
118147
talks: Map<TalkId, ITalk>;
119148
auditoriums: Map<AuditoriumId, IAuditorium>;

src/backends/penta/db/PentaDb.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,32 @@ export class PentaDb {
115115
return this.getTalksWithin(END_QUERY, inNextMinutes, minBefore);
116116
}
117117

118+
/**
119+
* Returns a list of database entries for all talks who have some kind of event in the next `lookaheadSeconds` seconds.
120+
*
121+
* A suggested implementation might be to return all talks overlapping this window, but for now the simple implementation
122+
* is just to return talks with a start, end or Q&A start within the time window.
123+
*
124+
* @param lookaheadMinutes Number of minutes to look ahead into the future.
125+
*/
126+
public async getTalksWithUpcomingEvents(lookaheadMinutes: number): Promise<IDbTalk[]> {
127+
const talksStarting = await this.getUpcomingTalkStarts(lookaheadMinutes, lookaheadMinutes);
128+
const talksEnding = await this.getUpcomingTalkEnds(lookaheadMinutes, lookaheadMinutes);
129+
const talksQaStarting = await this.getUpcomingQAStarts(lookaheadMinutes, lookaheadMinutes);
130+
const result: IDbTalk[] = [];
131+
const seenTalkIds = new Set();
132+
133+
for (const talk of talksStarting.concat(talksEnding, talksQaStarting)) {
134+
if (seenTalkIds.has(talk.event_id)) {
135+
continue;
136+
}
137+
seenTalkIds.add(talk.event_id);
138+
result.push(talk);
139+
}
140+
141+
return result;
142+
}
143+
118144
/**
119145
* Gets the record for a talk.
120146
* @param talkId The talk ID.
@@ -136,7 +162,6 @@ export class PentaDb {
136162
}
137163

138164
private postprocessDbTalk(talk: IRawDbTalk): IDbTalk {
139-
140165
const qaStartDatetime = talk.qa_start_datetime + this.config.schedulePreBufferSeconds * 1000;
141166
let livestreamStartDatetime: number;
142167
if (talk.prerecorded) {
@@ -159,7 +184,7 @@ export class PentaDb {
159184
}
160185

161186
private postprocessDbTalks(rows: IRawDbTalk[]): IDbTalk[] {
162-
return rows.map(this.postprocessDbTalk);
187+
return rows.map(this.postprocessDbTalk.bind(this));
163188
}
164189

165190
private sanitizeRecords(rows: IDbPerson[]): IDbPerson[] {

0 commit comments

Comments
 (0)