Add labkit rate limit adapter for cohort 4 keys
What this MR does
Adds the labkit rate-limit adapter path for cohort 4 of the Gitlab::ApplicationRateLimiter → Labkit::RateLimit migration.
Cohort 4 is the sole IncrementPerActionedResource caller — the EE git-abuse flow's "unique project downloads" rate limits — driven from ee/app/services/users/abuse/git_abuse/base_throttle_service.rb. This cohort migrates the keys:
| Key | Subclass | Characteristics | count_distinct |
|---|---|---|---|
unique_project_downloads_for_application |
ApplicationThrottleService |
[user] |
:project_id |
unique_project_downloads_for_namespace |
NamespaceThrottleService |
[user, namespace] |
:project_id |
Both keys count the distinct number of projects a user downloads in a window (SADD/SCARD), rather than the number of download calls (INCR). Threshold and interval for the namespace key are per-namespace (read from namespace_settings.unique_project_download_limit{,_interval_in_seconds}), not application-wide.
This is the first cohort to use labkit's set-mode rules and the first cohort to use labkit's rule_context: for per-call config. Both prerequisites are now upstream:
Labkit::RateLimit::Rule#count_distinct:(labkit-ruby v2.1.0, !292 (merged))Labkit::RateLimit::Limiter#check(..., rule_context:)(labkit-ruby v2.2.0, !300 (merged))
The Gemfile pin moves from ~> 2.0.0 to ~> 2.2 and Gemfile.lock / Gemfile.next.lock resolve to gitlab-labkit 2.2.1.
Mechanism
Spec entries opt in via two new fields
unique_project_downloads_for_application: {
...
count_distinct: :project_id,
overrides_via_rule_context: true,
flag_scope: :cohort_4,
}count_distinct: :project_idflips the labkit Rule from INCR to SADD/SCARD. The named slot is not incharacteristics:(the bucket key), and the adapter populates it explicitly fromresource.idrather than via class-routing — hence the_idsuffix to avoid collision with class-routed AR slots.overrides_via_rule_context: trueopts the entry out of theshadow_or_enforce?override short-circuit. Instead, the labkit Rule is built with one-arity callables onlimit:/period:that read from the per-callrule_context:hash. Anil/missing key falls back to the registered threshold/interval, so behaviour is identical when no override is supplied.
Strategy compatibility gate
dispatch_to_labkit now admits IncrementPerActionedResource in addition to IncrementPerAction, but only when the spec is set-mode (LabkitAdapter.set_mode?). An ad-hoc INCR-mode key called with a resource: argument would otherwise diverge silently from the legacy SADD/SCARD counter.
Peek-then-check is set-safe
BaseThrottleService#initialize calls rate_limited?(peek: true) (to suppress duplicate alert emails); #execute calls it without peek. The labkit peek is read-only (SCARD without SADD), so the Redis SET only accumulates members on the non-peek path. Covered by an explicit integration regression test:
'does not SADD on peek when both peek and check share the same call site'Feature flags
Two wip flags created in config/feature_flags/wip/, both group: group::networking and incident management, default off:
| Flag | Purpose |
|---|---|
rate_limiter_use_labkit_cohort_4 |
run labkit path alongside legacy (shadow) |
rate_limiter_use_labkit_cohort_4_enforce |
let labkit's decision win over legacy |
Rollout work item: gitlab-org/gitlab#601342 (with shadow/enforce flip steps, Q1–Q7 pass criteria, rollback playbook).
The adapter uses Feature.current_request as the actor, which resolves to a per-call UUID for non-request callers; the rollout playbook is binary on/off only, not percentage.
Tests
156 examples added / updated across:
spec/lib/gitlab/application_rate_limiter/labkit_adapter_spec.rb:.set_mode?, opt-in path through.shadow_or_enforce?,.run!SADD semantics,.run_peek!not requiringresource_id,threshold_overridepropagation viarule_context, fallback to registered values when overrides are nil.spec/lib/gitlab/application_rate_limiter_spec.rb: revisedIncrementPerActionedResourcedispatch coverage; the gate now distinguishes INCR-mode keys (stay on legacy) from set-mode keys (route to labkit withresource_id+ overrides forwarded).ee/spec/lib/ee/gitlab/application_rate_limiter/labkit_adapter/supported_rate_limits_spec.rb: registry shape for the two cohort 4 entries, end-to-end SADD routing for both, peek-then-check integration regression.
Migration context
- Parent epic: gitlab-com/gl-infra&2021
- Cohort 4 issue: gitlab-com/gl-infra/production-engineering#28811 (closed)
- Overarching Stage 2a tracker: gitlab-com/gl-infra/production-engineering#28808
- Cohort 3 (prior cohort, INCR-mode + peek): !235212 (merged)
- Labkit
count_distinct:: gitlab-org/ruby/gems/labkit-ruby!292 (merged) - Labkit
rule_context:: gitlab-com/gl-infra/production-engineering#29071 (closed)
Screenshots or screen recordings
Not applicable.
How to set up and validate locally
- Stop GDK and run
bundle installto pick up labkit-ruby 2.2.1. - In a Rails console with the cohort 4 flag enabled:
Feature.enable(:rate_limiter_use_labkit_cohort_4) user = User.first; project = Project.first Gitlab::ApplicationRateLimiter.throttled?( :unique_project_downloads_for_application, scope: user, resource: project, threshold: 5, interval: 600 ) Gitlab::Redis::RateLimiting.with { |r| r.keys("labkit:rl:applimiter_unique_project_downloads_for_application*") }should show one SET key with the project id as a member.
MR acceptance checklist
This checklist encourages you to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
- I have evaluated the MR acceptance checklist for this MR.