Add short-TTL Redis cache for MR approval state

Summary

Adds a Redis cache with explicit invalidation for the computed MR approval state in CheckApprovedService. This avoids repeated expensive approval computations during UI/API polling.

How it works

  • CheckApprovedService caches its result in Redis under a stable key (merge_request:{id}:approval_check) with a 30-second TTL
  • On approval mutation, delete_approval_mergeability_cache explicitly DELetes the cached entry via CheckApprovedService#invalidate (which routes through ResultsStore#delete)
  • The 30-second TTL acts as a safety net for edge cases not covered by explicit invalidation (e.g., approval rule changes, which are rare admin-level actions)
  • Cache is bypassed during actual merge operations (use_cache: false)
  • Cache is not written when merge_request.temporarily_unapproved? is true, so the transient checking result never gets stored

Cache invalidation

Event Invalidation method
Approval added Explicit DEL via reset_approval_cache!delete_approval_mergeability_cache
Approval removed Explicit DEL via reset_approval_cache!delete_approval_mergeability_cache
Approvals reset on push (standard path) Explicit DEL via reset_approval_cache!
Approvals reset on push (optimize_reset_approvals_preloading branch) Explicit DEL via delete_approval_mergeability_cache (called directly)
PUT /reset_approvals API endpoint Explicit DEL via delete_approval_mergeability_cache (called directly)
Approval rule changed 30s TTL expiry (rare admin action)

reset_approval_cache! is the funnel point for the standard approval-mutation services (ApprovalService, RemoveApprovalService, ResetApprovalsService, UpdateService, BaseService). The two non-funnel paths (optimized push-reset branch and the API reset_approvals endpoint) call delete_approval_mergeability_cache directly, since neither needs to reset AR associations.

CE/FOSS compatibility

delete_approval_mergeability_cache is defined in EE's VisibleApprovable. A no-op CE definition is added to Approvable, so calling it from a CE endpoint (lib/api/merge_request_approvals.rb) does not raise NoMethodError on FOSS deployments. EE's implementation wins in the method-resolution chain via the existing Approvable.prepend_mod injection.

Why this approach

Aspect This MR Relation tracking (!225697)
Code complexity ~40 lines ~300+ lines
Cache key computation 0 DB queries N/A
Invalidation Explicit DEL + 30s TTL 12 callback points across 10+ models
Redis calls per read 1 GET 1 GET
Max staleness (approvals) ~0 (instant DEL) ~0
Max staleness (rule changes) 30 seconds ~0

Files changed

  • ee/app/services/merge_requests/mergeability/check_approved_service.rb — Cacheable with stable key + 30s TTL; exposes #invalidate; cacheable? excludes temporarily_unapproved? state
  • ee/app/models/concerns/visible_approvable.rbreset_approval_cache! calls delete_approval_mergeability_cache; the method is public and self-gates on the feature flag, so callers invoke it unconditionally
  • app/models/concerns/approvable.rb — CE no-op delete_approval_mergeability_cache for FOSS compatibility
  • ee/app/services/merge_requests/reset_approvals_service.rb — Invalidate cache in the optimize_reset_approvals_preloading branch (which intentionally bypasses reset_approval_cache!)
  • lib/api/merge_request_approvals.rb — Call delete_approval_mergeability_cache after PUT /reset_approvals clears approvals
  • lib/gitlab/merge_requests/mergeability/redis_interface.rb — Add delete_check, make ttl: a required parameter
  • lib/gitlab/merge_requests/mergeability/results_store.rb — Add delete; make ttl: a required parameter
  • app/services/merge_requests/mergeability/check_base_service.rb — Add cache_ttl method returning 6.hours as default
  • app/services/merge_requests/mergeability/run_checks_service.rb — Pass cache_ttl on write
  • config/feature_flags/gitlab_com_derisk/short_ttl_approval_cache.yml — Feature flag

Default TTL cleanup

Moved the default cache TTL (6 hours) from RedisInterface::EXPIRATION to CheckBaseService#cache_ttl. Each check now explicitly declares its TTL — CheckApprovedService overrides with 30.seconds, all others inherit the 6.hours default. RedisInterface#save_check no longer has a fallback; it uses the TTL it receives directly.

Feature flag

:short_ttl_approval_cachegitlab_com_derisk type, default disabled, per-project actor

Testing

  • Unit tests for all changed files (cache read/write/delete paths, invalidation contract, TTL, FF gating)
  • Integration tests for all invalidation entry points (reset_approval_cache! concern, PUT /reset_approvals endpoint, optimized push-reset branch)
  • FOSS_ONLY=1 spec run confirms the CE no-op prevents NoMethodError on FOSS deployments
  • Verified on GDK:
    • Cache populates on first check (30s TTL confirmed in Redis), serves cached results on subsequent reads
    • Approval via ApprovalService → cache DELeted (invalidated)
    • Unapproval via RemoveApprovalService → cache DELeted (invalidated)
    • PUT /reset_approvals API path → cache DELeted (invalidated)
    • Optimized push-reset branch → cache DELeted (invalidated)
    • temporarily_unapproved? state → no cache written
    • Feature flag OFF → no Redis reads, writes, or DELs (zero new code runs)
    • use_cache: false (merge path) correctly bypasses cache

MR acceptance checklist

Edited by Marc Shaw

Merge request reports

Loading