Skip to content

Commit fef3854

Browse files
cubehouseclaude
andcommitted
fix(plopsa): trust wait-times feed over stale temporarily_closed hint
Diagnostic logs show a subset of rides (8 at a time) flapping OPERATING ↔ CLOSED every ~3 minutes — independent of `parkOpenNow`, which stays steady. Pattern is per-ride, lockstep across the same set of rides each cycle. Cause: the per-ride `closedById` map (built from POI's `temporarily_closed` flag) is the deciding bit when it disagrees with the live wait-times feed. POI is HTTP-cached for 12 hours, and on a multi-instance deployment two collectors that fetched POI at slightly different moments end up with different cached snapshots — one says "ride X is temp-closed", the other says "open" — and they alternate writes on every poll cycle. The wait-times feed is the ground truth for "is this ride currently taking guests": if it gives us a numeric value, the ride is operating regardless of what POI's cached `temporarily_closed` hint says. The hint is only authoritative when the wait-times feed has no number for the ride. Decision logic extracted into a pure `plopsaDecideOperating(parkOpenNow, tempClosed, hasWait)` helper. Unit-tested matrix in `__tests__/plopsa.test.ts` covers all 8 input combinations and pins the specific pre-fix bug case (open + temp-closed + has-wait → was CLOSED, now OPERATING). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 29ef35a commit fef3854

2 files changed

Lines changed: 63 additions & 2 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Plopsa decision-logic regression tests.
3+
*
4+
* The full decision matrix for whether a ride emits OPERATING vs CLOSED.
5+
* The interesting case is row 3: a numeric wait time + a stale
6+
* `temporarily_closed: true` from POI must NOT downgrade to CLOSED, or
7+
* multi-collector deployments will flap any ride whose POI snapshot
8+
* disagrees between instances.
9+
*/
10+
import {describe, test, expect} from 'vitest';
11+
import {plopsaDecideOperating} from '../plopsa.js';
12+
13+
describe('plopsaDecideOperating', () => {
14+
test('park closed → ride always CLOSED regardless of other inputs', () => {
15+
expect(plopsaDecideOperating(false, false, false)).toBe(false);
16+
expect(plopsaDecideOperating(false, false, true)).toBe(false);
17+
expect(plopsaDecideOperating(false, true, false)).toBe(false);
18+
expect(plopsaDecideOperating(false, true, true)).toBe(false);
19+
});
20+
21+
test('park open + ride open + has wait → OPERATING', () => {
22+
expect(plopsaDecideOperating(true, false, true)).toBe(true);
23+
});
24+
25+
test('park open + ride open + no wait → OPERATING (e.g. brand-new ride before first reading)', () => {
26+
expect(plopsaDecideOperating(true, false, false)).toBe(true);
27+
});
28+
29+
test('park open + POI says temp-closed + has wait → OPERATING (wait-times feed wins over stale POI hint)', () => {
30+
// This is the case the bug report depends on: stale POI says closed,
31+
// but the wait-times feed has a real number. Trust the live number.
32+
expect(plopsaDecideOperating(true, true, true)).toBe(true);
33+
});
34+
35+
test('park open + POI says temp-closed + no wait → CLOSED (the hint is authoritative when no live signal)', () => {
36+
expect(plopsaDecideOperating(true, true, false)).toBe(false);
37+
});
38+
});

src/parks/plopsa/plopsa.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,27 @@ function formatTodayInTimezone(tz: string): string {
9797
return `${yyyy}-${mm}-${dd}`;
9898
}
9999

100+
/**
101+
* Decide whether a Plopsa ride should emit as OPERATING right now.
102+
*
103+
* Inputs are intentionally flat booleans + a primitive — pure function so
104+
* the matrix is easy to unit-test (see `__tests__/plopsa.test.ts`).
105+
*
106+
* The wait-times feed is treated as the ground truth: a numeric wait
107+
* means the ride is taking guests right now. POI's `temporarily_closed`
108+
* is a hint we use only when the wait-times feed has no number for the
109+
* ride. Without that priority, a stale 12h-cached POI snapshot on one
110+
* collector instance disagreeing with another instance's cached snapshot
111+
* causes lockstep OPERATING ↔ CLOSED flapping for the affected rides.
112+
*/
113+
export function plopsaDecideOperating(
114+
parkOpenNow: boolean,
115+
tempClosed: boolean,
116+
hasWait: boolean,
117+
): boolean {
118+
return parkOpenNow && (hasWait || !tempClosed);
119+
}
120+
100121
/** Is the park currently within an "open" timeslot from today's hours? */
101122
function isParkOpenNow(hours: PlopsaTodayHours | null, tz: string): boolean {
102123
if (!hours?.timeslots?.length) return false;
@@ -432,7 +453,9 @@ class PlopsaBase extends Destination {
432453
const lastUpdated = new Date().toISOString();
433454
return Object.entries(waitTimes).map(([attractionId, waitTime]) => {
434455
const id = String(attractionId);
435-
const operating = parkOpenNow && closedById.get(id) !== true;
456+
const tempClosed = closedById.get(id) === true;
457+
const hasWait = typeof waitTime === 'number';
458+
const operating = plopsaDecideOperating(parkOpenNow, tempClosed, hasWait);
436459

437460
if (!operating) {
438461
return {id, status: 'CLOSED', lastUpdated} as unknown as LiveData;
@@ -441,7 +464,7 @@ class PlopsaBase extends Destination {
441464
id,
442465
status: 'OPERATING',
443466
queue: {
444-
STANDBY: {waitTime: typeof waitTime === 'number' ? waitTime : null},
467+
STANDBY: {waitTime: hasWait ? waitTime : null},
445468
},
446469
lastUpdated,
447470
} as unknown as LiveData;

0 commit comments

Comments
 (0)