Skip to content

fix(script-levels): remove unsafe @@fallback_responses cache#73409

Open
artem-vavilov wants to merge 2 commits into
stagingfrom
fallback-responses-fix
Open

fix(script-levels): remove unsafe @@fallback_responses cache#73409
artem-vavilov wants to merge 2 commits into
stagingfrom
fallback-responses-fix

Conversation

@artem-vavilov

@artem-vavilov artem-vavilov commented Jun 23, 2026

Copy link
Copy Markdown
Member

Removes the in-process @@fallback_responses cache from ScriptLevelsController.

This cache was keyed only by @script_level.id, but fallback milestone responses are not a pure function of a script level. They can depend on the selected level variant, current user, user progress, lock/hidden state, section lesson-extras settings, locale, course URL context, hint-view history, and puzzle-rating eligibility.

ScriptLevel and its associations only describe curriculum structure. User-specific and request-specific state is looked up separately through UserLevel, section hidden state, hint requests, puzzle ratings, experiments, and request context. As a result, this cache can return a fallback response built for a different user or request context, causing incorrect fallback redirects, wrong selected level_ids, stale lesson-extras behavior, incorrect puzzle-rating prompts, and possible cross-user leakage of hint-view response data.

This removes the cache instead of moving it to Rails.cache because the problem is not where the cache is stored. The cached value itself is context-dependent and has no safe, stable invalidation boundary. Recomputing it per request keeps the response scoped to the actual user, level, locale, and course context.

Examples

response = {
timestamp: DateTime.now.to_milliseconds
}

if options[:share_failure]
response[:share_failure] = response_for_share_failure(options[:share_failure])
end

private def share_failure_message(failure_type)
case failure_type
when ShareFiltering::FailureType::EMAIL
t('share_code.email_not_allowed')
when ShareFiltering::FailureType::ADDRESS
t('share_code.address_not_allowed')
when ShareFiltering::FailureType::PHONE
t('share_code.phone_number_not_allowed')
when ShareFiltering::FailureType::PROFANITY
t('share_code.profanity_not_allowed')
else
raise ArgumentError.new("Unknown share failure type #{failure_type}")
end
end

if HintViewRequest.enabled? && (script_level && current_user)
response[:hint_view_requests] = HintViewRequest.milestone_response(script_level.script, level, current_user)
response[:hint_view_request_url] = hint_view_requests_path
end

if PuzzleRating.enabled?
response[:puzzle_ratings_enabled] = script_level && PuzzleRating.can_rate?(script_level.script, level, current_user)
end

Memory leak analysis

@@fallback_responses (unbounded class variable): Grows one entry per curriculum level visited, never evicts. Bounded only by the ~69,571 levels in the database — a data ceiling, not an enforced limit. Measured cost: 364 MB per worker × 32 = 11.4 GB, 5% of host RAM. (Experiment E3, local only — not yet confirmed in production)

@artem-vavilov artem-vavilov changed the title fix(script-levels): remove unsafe fallback response cache fix(script-levels): remove unsafe @@fallback_responses cache Jun 23, 2026
@artem-vavilov artem-vavilov requested a review from Copilot June 23, 2026 09:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Removes the in-process @@fallback_responses cache in ScriptLevelsController#present_level so fallback milestone responses are computed per-request, avoiding reuse across users and request contexts.

Changes:

  • Removed the class-variable cache (@@fallback_responses) keyed by @script_level.id.
  • Always recompute @fallback_response (success/failure) for the current request context.

@artem-vavilov artem-vavilov requested review from a team June 23, 2026 09:30
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.

2 participants