Repository navigation
fix(scheduler): reject illegal calendar timestamps instead of rolling them #8632
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2d5e289
80ed6f2
0461def
451246f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ import { | |
| normalizeSchedulerHostUpdateFailures, | ||
| normalizeSchedulerRrule, | ||
| retainedSchedulerHostUpdateFailures, | ||
| requireSchedulerTimestampMilliseconds, | ||
| rruleForMinutes, | ||
| schedulerRruleIntervalMinutes, | ||
| schedulerTimestampMilliseconds, | ||
|
|
@@ -359,17 +360,40 @@ function schedulerProgressionIntervalElapsed( | |
| schedulerState: JsonObject, | ||
| currentTime: string, | ||
| ): boolean { | ||
| const updatedAt = schedulerTimestampMilliseconds(schedulerState.updated_at); | ||
| const current = schedulerTimestampMilliseconds(currentTime); | ||
| const appliedInterval = schedulerRruleIntervalMinutes( | ||
| schedulerState.last_applied_rrule, | ||
| ); | ||
| if (updatedAt === null || current === null || appliedInterval === null) { | ||
| return true; | ||
| } | ||
| // Missing interval or blank updated_at stays elapsed. A present illegal | ||
| // calendar is rejected instead of rolled or treated as elapsed. | ||
| if (appliedInterval === null) return true; | ||
| const updatedAt = requireSchedulerTimestampMilliseconds( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] 旧身份的时间不要阻断重置。 既有 30 分钟调度收据带无效 updated_at,而监控到期界限将 reset token 改为新身份,需重置到 15 分钟。 真实 store 写入并读回旧收据后,基点返回 reset_required、15 分钟且需要宿主更新和 ACK;当前直接报 updated_at must be an ISO timestamp,整个调度提示无法返回。只把旧时间换为合法值的同头对照正常重置。 按当前 cadence 分支惰性验证旧 updated_at:只有同一身份需要判断是否到间隔时才读取。新身份的初始间隔独立计算;现用非法时间仍显式拒绝。 |
||
| schedulerState.updated_at, | ||
| "updated_at", | ||
| ); | ||
| if (updatedAt === null) return true; | ||
| const current = requireSchedulerTimestampMilliseconds( | ||
| currentTime, | ||
| "current_time", | ||
| ); | ||
| if (current === null) return true; | ||
| return current >= updatedAt + appliedInterval * 60_000; | ||
| } | ||
|
|
||
| // evaluateCadence reads the old clock only after identity matches and the | ||
| // same-identity path is comparing the applied interval. An obsolete receipt | ||
| // must not block identity_reset, an unacknowledged retry, or a held initial. | ||
| function cadenceConsumesAppliedInterval( | ||
| statePresent: boolean, | ||
| identityMatches: boolean, | ||
| hasHostUpdateFailures: boolean, | ||
| currentCadenceAcknowledged: boolean, | ||
| advanceSameIdentity: boolean, | ||
| ): boolean { | ||
| if (!statePresent || !identityMatches) return false; | ||
| if (hasHostUpdateFailures && !currentCadenceAcknowledged) return false; | ||
| return advanceSameIdentity; | ||
| } | ||
|
|
||
| function monitorRruleAppliedWithinStaleTolerance( | ||
| cadenceClass: string, | ||
| lastAppliedRrule: string, | ||
|
|
@@ -400,6 +424,7 @@ function evaluateBackoff(request: JsonObject): SchedulerBackoffTransitionResult | |
| "identity_signature", | ||
| ); | ||
| const currentTime = requiredString(request.current_time, "current_time"); | ||
| requireSchedulerTimestampMilliseconds(currentTime, "current_time"); | ||
| const cadenceClass = requiredString(request.cadence_class, "cadence_class"); | ||
| const staleToleranceMinutes = requiredInteger( | ||
| request.stale_tolerance_minutes, | ||
|
|
@@ -428,23 +453,33 @@ function evaluateBackoff(request: JsonObject): SchedulerBackoffTransitionResult | |
| : ""; | ||
| const currentCadenceAcknowledged = Boolean(appliedTargetRrule) && | ||
| normalizeSchedulerRrule(lastAppliedRrule) === appliedTargetRrule; | ||
| const statePresent = Object.keys(schedulerState).length > 0; | ||
| const identityMatches = statePresent && | ||
| schedulerState.reset_token === resetToken && | ||
| schedulerState.identity_signature === identitySignature; | ||
| const advanceSameIdentity = requiredBoolean( | ||
| request.advance_same_identity, | ||
| "advance_same_identity", | ||
| ); | ||
| const hasHostUpdateFailures = allHostUpdateFailures.length > 0; | ||
| const appliedIntervalElapsed = cadenceConsumesAppliedInterval( | ||
| statePresent, | ||
| identityMatches, | ||
| hasHostUpdateFailures, | ||
| currentCadenceAcknowledged, | ||
| advanceSameIdentity, | ||
| ) | ||
| ? schedulerProgressionIntervalElapsed(schedulerState, currentTime) | ||
| : false; | ||
| const cadence = evaluateCadence({ | ||
| progression_size: progressionMinutes.length, | ||
| state_present: Object.keys(schedulerState).length > 0, | ||
| identity_matches: Object.keys(schedulerState).length > 0 && | ||
| schedulerState.reset_token === resetToken && | ||
| schedulerState.identity_signature === identitySignature, | ||
| state_present: statePresent, | ||
| identity_matches: identityMatches, | ||
| applied_index: appliedIndex, | ||
| current_cadence_acknowledged: currentCadenceAcknowledged, | ||
| advance_same_identity: requiredBoolean( | ||
| request.advance_same_identity, | ||
| "advance_same_identity", | ||
| ), | ||
| applied_interval_elapsed: schedulerProgressionIntervalElapsed( | ||
| schedulerState, | ||
| currentTime, | ||
| ), | ||
| has_host_update_failures: allHostUpdateFailures.length > 0, | ||
| advance_same_identity: advanceSameIdentity, | ||
| applied_interval_elapsed: appliedIntervalElapsed, | ||
| has_host_update_failures: hasHostUpdateFailures, | ||
| }); | ||
| const currentIndex = cadence.current_index; | ||
| const currentInterval = progressionMinutes[currentIndex]; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
对已到期的隔离 SQLite Monitor 执行
quota monitor-poll --next-due-at 2026-12-04t01:15:00z --execute。base 退出 0,实际保存该到期时间;本提交退出 1(monitor_poll_rejected),到期时间留在原值。同一时刻的大写写法两版都能保存,11 月 31 日两版均拒绝。新 parser 的日期时间分隔符只允许大写 T 或空格;RFC 3339 §5.6 明确允许小写。在已有校验前接受/归一化这个分隔符,不放宽日历校验。