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
CheckApprovedServicecaches its result in Redis under a stable key (merge_request:{id}:approval_check) with a 30-second TTL- On approval mutation,
delete_approval_mergeability_cacheexplicitly DELetes the cached entry viaCheckApprovedService#invalidate(which routes throughResultsStore#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 transientcheckingresult 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?excludestemporarily_unapproved?stateee/app/models/concerns/visible_approvable.rb—reset_approval_cache!callsdelete_approval_mergeability_cache; the method is public and self-gates on the feature flag, so callers invoke it unconditionallyapp/models/concerns/approvable.rb— CE no-opdelete_approval_mergeability_cachefor FOSS compatibilityee/app/services/merge_requests/reset_approvals_service.rb— Invalidate cache in theoptimize_reset_approvals_preloadingbranch (which intentionally bypassesreset_approval_cache!)lib/api/merge_request_approvals.rb— Calldelete_approval_mergeability_cacheafterPUT /reset_approvalsclears approvalslib/gitlab/merge_requests/mergeability/redis_interface.rb— Adddelete_check, makettl:a required parameterlib/gitlab/merge_requests/mergeability/results_store.rb— Adddelete; makettl:a required parameterapp/services/merge_requests/mergeability/check_base_service.rb— Addcache_ttlmethod returning6.hoursas defaultapp/services/merge_requests/mergeability/run_checks_service.rb— Passcache_ttlon writeconfig/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_cache — gitlab_com_derisk type, default disabled, per-project actor
Related
- !231445 (merged) — follow-up to skip cache in auto-merge process
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_approvalsendpoint, optimized push-reset branch) FOSS_ONLY=1spec run confirms the CE no-op preventsNoMethodErroron 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_approvalsAPI 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
- I have evaluated the MR acceptance checklist for this MR.