Skip to content

fix(api): validate time slot schedules before handing them to the scheduler - #2013

Open
chrisbenincasa wants to merge 1 commit into
mainfrom
fix/time-slot-schedule-validation
Open

fix(api): validate time slot schedules before handing them to the scheduler#2013
chrisbenincasa wants to merge 1 commit into
mainfrom
fix/time-slot-schedule-validation

Conversation

@chrisbenincasa

Copy link
Copy Markdown
Owner

Problem

Every numeric field on TimeSlotScheduleSchema is a bare z.number(), and slots has no
minimum length. Several inputs therefore reach the scheduler, which mishandles rather than
rejects them. Each row below was reproduced:

Input Scheduler behaviour
startTime: 1234.567 infinite loop — the worker thread hangs permanently
padMs: 0 reports success, returns a lineup whose durations are all NaN
maxDays: -1 reports success, returns zero lineup items
maxDays: 0 returns 16 items where 32 are expected
slots: [] throws Could not find a suitable slot — the same message an out-of-period offset produces, so the log can't tell them apart

The first is the serious one. The scheduling loop is synchronous despite the enclosing
async, so it blocks the event loop outright: vitest's own 15-second test timeout never
fired and the process had to be killed at 60 seconds. The worker pool's timeoutPromise
rejects the caller's promise after 60s while the thread keeps burning a core forever, so N
such requests permanently exhaust an N-worker pool.

Fix

Add StrictTimeSlotScheduleSchema and use it for the request body at channelsApi.ts:743.

TimeSlotScheduleSchema itself is deliberately left permissive. It feeds
LineupScheduleSchema inside CondensedChannelProgrammingSchema, which is both persisted
and used to serialize channel responses — so tightening it would make a channel already
holding one of these values fail response serialization and stop loading entirely, which is
worse than the bug. This is the same split already used for StrictChannelIconSchema and
ChannelIconSchema.

Bounds: startTime a whole number within the period (via superRefine, since the valid
range depends on period), padMs and maxDays positive, latenessMs non-negative, at
least one slot.

Note for review

This rejects requests the API currently accepts. That should only affect payloads that
already produce a broken schedule, but it is a behaviour change at the boundary and is the
reason this is split from the scheduler fix in #2012, which stands on its own.

Test plan

  • Accepts a well-formed schedule, the last representable offset in the period, and a weekly schedule using the wider period
  • Rejects all ten malformed cases above
  • The permissive schema still accepts every rejected case — asserted, so the distinction can't be erased later and existing channels keep loading
  • NaN was already excluded by z.number(); recorded as such rather than claimed as a fix
  • 24 tests, typecheck 5/5

🤖 Generated with Claude Code

…eduler

Every numeric field on a time slot schedule was a bare z.number() and slots had
no minimum length, so several inputs reached the scheduler that it mishandles
rather than rejects. Probing each one:

  startTime: 1234.567   an infinite loop. The scheduling loop is synchronous
                        despite the enclosing async, so it blocks the event
                        loop outright -- the test runner's own timeout never
                        fires and the process has to be killed. The worker
                        pool's timeoutPromise rejects the caller while the
                        thread keeps burning a core, so N such requests
                        permanently exhaust an N worker pool.
  padMs: 0              reports success and returns a lineup whose durations
                        are all NaN.
  maxDays: -1           reports success and returns zero lineup items.
  maxDays: 0            returns half the expected items.
  slots: []             throws "Could not find a suitable slot", the same
                        message an out-of-period offset produces, so the log
                        cannot tell the two apart.

Add StrictTimeSlotScheduleSchema and use it for the request body.

TimeSlotScheduleSchema itself has to stay permissive. It also feeds
LineupScheduleSchema inside CondensedChannelProgrammingSchema, which is both
persisted and used to serialize channel responses, so tightening it would make
a channel already holding one of these values fail response serialization and
stop loading entirely -- worse than the bug. This is the same split already
used for StrictChannelIconSchema.

The bounds are startTime a whole number within the period, padMs and maxDays
positive, latenessMs non-negative, and at least one slot. A test asserts the
permissive schema still accepts every case the strict one rejects, so the
distinction cannot be erased by accident later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant