gitlab-labkit 5.1.1 → 5.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5dde258c9178baa3601cb15a3f3cad2b4470357c326e381a2aebb423de6f5235
4
- data.tar.gz: c5fc0c320dc08937a789f1ebefb55adebd3905bc124ca813fea5de2d73efb102
3
+ metadata.gz: d272acd766a807cc483a70b13238a55b91dbffd0de51435c3e754df64562176d
4
+ data.tar.gz: 2fbc2ff23bbedca43ef1e5c650b3b6c78a0aa4c7ecfec85fec8eae79ef2d2752
5
5
  SHA512:
6
- metadata.gz: f6dbd1b24464bdecc03635e2f9d3ebafe33003d755c9173d785a195982a0144dd7038db2078935925aa4aaa795f4f3bfba100a35a62a0daf0f457e3c12ee4113
7
- data.tar.gz: 951f021de3fa71b9b61dbc44c2a6f057c5aedb21afc4367e1d2a7756e2f2bdcaee7edfbc44552113fe9358665f6ed94a118f6e5595fe01bc502b982fbc150e7b
6
+ metadata.gz: b469df64589bfe387e3d725d1cc6c6a3e769bc078ccb043b8afdf5f3591bae10a5730ecaa87069b1dc49041f50e6c6e92e145989a2a644e2d2f33fe56abd614f
7
+ data.tar.gz: 212bf5a04d9a38b4cd64f6b7319179dba2379f3b89b6b1c6070bf64f9f256ea899f6a02ddc35b271dcb6bfb300100627decd388b7b5e3ae5c45f289d2fdd4bfe
@@ -1,5 +1,5 @@
1
1
  # DO NOT MANUALLY EDIT; Run ./scripts/update-asdf-version-variables.sh to update this
2
2
  variables:
3
- GL_ASDF_RUBY_VERSION: "4.0.6"
3
+ GL_ASDF_RUBY_VERSION: "4.0.7"
4
4
  GL_ASDF_SHELLCHECK_VERSION: "0.11"
5
- GL_ASDF_SHFMT_VERSION: "3.13"
5
+ GL_ASDF_SHFMT_VERSION: "3.14"
data/.gitlab-ci.yml CHANGED
@@ -19,13 +19,13 @@ include:
19
19
  # It includes standard checks, gitlab-scanners, validations and release processes
20
20
  # common to all projects using this template library.
21
21
  # see https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/blob/main/templates/standard.md
22
- - component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/standard-build@v5.15
22
+ - component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/standard-build@v5.24
23
23
 
24
24
  # Runs rspec tests and rubocop on the project
25
25
  # see https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/blob/main/templates/ruby.md
26
- - component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/ruby-build@v5.15
26
+ - component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/ruby-build@v5.24
27
27
 
28
- - component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/danger@v5.15
28
+ - component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/danger@v5.24
29
29
 
30
30
  # Attach a redis service to the rspec job from common-ci-tasks/ruby-build.
31
31
  # GitLab merges keys when a local job has the same name as an included one,
@@ -67,48 +67,21 @@ deploy:
67
67
  - Gemfile
68
68
  - .gitlab-ci.yml
69
69
 
70
- # Run the labkit-spec conformance harness with its Ruby server built from this
71
- # checkout. labkit-spec pins released SDKs, so this is the only pre-merge check
72
- # against the spec.
73
- conformance:
70
+ # Gate SDK changes on the labkit-spec conformance suite, run remotely: trigger
71
+ # a downstream pipeline in labkit-spec that checks out this pipeline's commit
72
+ # (LABKIT_RUBY_SHA) and runs the Ruby conformance suite against it, verifying
73
+ # structured-logging, lifecycle, and rate-limit parity with the spec.
74
+ # strategy:depend mirrors the downstream result here, so a red run blocks the
75
+ # merge. See https://gitlab.com/gitlab-org/theseus/theseus/-/work_items/30
76
+ conformance-spec:
74
77
  stage: validate
75
- image:
76
- name: jdxcode/mise:2026.9.7
77
- entrypoint: [""]
78
78
  needs: []
79
- services:
80
- - name: redis:8-alpine
81
- alias: redis
79
+ variables:
80
+ LABKIT_RUBY_SHA: $CI_COMMIT_SHA
82
81
  rules:
83
- - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
84
- - if: $CI_COMMIT_BRANCH
85
- changes: !reference [.conformance-trigger-paths, changes]
86
82
  - if: $CI_PIPELINE_SOURCE == "merge_request_event"
87
83
  changes: !reference [.conformance-trigger-paths, changes]
88
- variables:
89
- LABKIT_SPEC_REPO: https://gitlab.com/gitlab-org/quality/tooling/labkit-spec.git
90
- LABKIT_SPEC_REF: main
91
- LABKIT_SPEC_DIR: $CI_PROJECT_DIR/.labkit-spec
92
- MISE_TRUSTED_CONFIG_PATHS: $LABKIT_SPEC_DIR
93
- MISE_DATA_DIR: $CI_PROJECT_DIR/.mise-cache
94
- BUNDLE_PATH: vendor/bundle
95
- CONFORMANCE_REDIS_ADDR: redis:6379
96
- cache:
97
- key: conformance-servers
98
- paths:
99
- - .labkit-spec/conformance/servers/ruby/vendor/
100
- - .mise-cache/
101
- script:
102
- # Not clone: the restored cache makes LABKIT_SPEC_DIR non-empty.
103
- - git init -q "$LABKIT_SPEC_DIR"
104
- - git -C "$LABKIT_SPEC_DIR" fetch -q --depth 1 "$LABKIT_SPEC_REPO" "$LABKIT_SPEC_REF"
105
- - git -C "$LABKIT_SPEC_DIR" checkout -q FETCH_HEAD
106
- - cd "$LABKIT_SPEC_DIR"
107
- - mise install
108
- # The gemspec lists files via `git ls-files`, so the path must be a git work tree.
109
- # grep fails the job if the Gemfile line moved and sed matched nothing.
110
- - |
111
- sed -i 's|^gem "gitlab-labkit".*|gem "gitlab-labkit", path: "'"$CI_PROJECT_DIR"'"|' conformance/servers/ruby/Gemfile
112
- grep -q 'gem "gitlab-labkit", path: "'"$CI_PROJECT_DIR"'"' conformance/servers/ruby/Gemfile
113
- - mise run conformance:ruby
114
- timeout: 15m
84
+ trigger:
85
+ project: gitlab-org/quality/tooling/labkit-spec
86
+ branch: main
87
+ strategy: depend
@@ -25,7 +25,7 @@ repos:
25
25
  # Documentation available at
26
26
  # https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/blob/main/docs/pre-commit.md
27
27
  - repo: https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks
28
- rev: v5.15 # renovate:managed
28
+ rev: v5.24 # renovate:managed
29
29
 
30
30
  hooks:
31
31
  - id: shellcheck # Run shellcheck for changed Shell files
data/.tool-versions CHANGED
@@ -1,3 +1,3 @@
1
- ruby 4.0.6
2
- shfmt 3.13
1
+ ruby 4.0.7
2
+ shfmt 3.14
3
3
  shellcheck 0.11
data/README.md CHANGED
@@ -55,13 +55,16 @@ To opt out of autostart (e.g. you've started Redis some other way), set
55
55
  `LABKIT_TEST_REDIS_URL` to a reachable instance, or
56
56
  `LABKIT_TEST_REDIS_NO_AUTOSTART=1` to fail loudly instead of spawning.
57
57
 
58
- The `conformance` CI job runs the
58
+ The `conformance-spec` CI job gates merge requests on the
59
59
  [labkit-spec](https://gitlab.com/gitlab-org/quality/tooling/labkit-spec)
60
- conformance harness against the spec's Ruby server, with the `gitlab-labkit`
61
- gem installed from your branch instead of the released version. A change to
62
- logging, lifecycle, or rate limiting is therefore checked against the
63
- cross-SDK scenarios before it merges. To run it locally, clone labkit-spec and
64
- replace the `gitlab-labkit` line in `conformance/servers/ruby/Gemfile` with
60
+ conformance harness. It triggers a downstream pipeline in labkit-spec that
61
+ checks out the MR's commit and runs the Ruby conformance suite against it,
62
+ with the `gitlab-labkit` gem installed from that checkout instead of the
63
+ released version. A change to logging, lifecycle, or rate limiting is
64
+ therefore checked against the cross-SDK scenarios before it merges, and the
65
+ downstream result is mirrored here, so a red run blocks the merge. To run it
66
+ locally, clone labkit-spec and replace the `gitlab-labkit` line in
67
+ `conformance/servers/ruby/Gemfile` with
65
68
 
66
69
  ```ruby
67
70
  gem "gitlab-labkit", path: "/path/to/labkit-ruby"
@@ -71,10 +74,9 @@ then run `mise run conformance:ruby` from the spec checkout (the rate-limit
71
74
  aspect needs Docker or `CONFORMANCE_REDIS_ADDR`).
72
75
 
73
76
  If your change also needs a labkit-spec change (a new scenario, or an update
74
- to its Ruby server), open that MR first and point `LABKIT_SPEC_REF` in
75
- `.gitlab-ci.yml` at its branch so this job validates both together. Set it
76
- back to `main` once the spec MR merges; a non-`main` ref must not reach
77
- `master`.
77
+ to its Ruby server), open and merge that MR first. The trigger always runs
78
+ labkit-spec `main`, so the spec side has to land before the gate here can
79
+ pass.
78
80
 
79
81
  Please also review the [development section of the LabKit (go) README](https://gitlab.com/gitlab-org/labkit#developing-labkit) for details of the LabKit architectural philosophy.
80
82
 
@@ -469,7 +469,7 @@ result.to_response_headers
469
469
  | `resolved_limit` | The evaluated `Integer` limit for this check. |
470
470
  | `resolved_period` | The evaluated `Integer` period in seconds for this check. |
471
471
  | `count` | Raw INCR value; useful for utilization-ratio metrics. |
472
- | `remaining` | `[resolved_limit - count, 0].max`. |
472
+ | `remaining` | `resolved_limit - count`, clamped to `[0, max(resolved_limit, 0)]`. |
473
473
  | `reset_at` | Best-effort UTC `Time` when the window resets (advisory only). |
474
474
 
475
475
  `to_response_headers` returns `{}` for an unmatched or error result, so it is
@@ -483,6 +483,10 @@ logged at WARN with `error_type: "rate_limit_error"` and returned as an error
483
483
  `Result` (`matched?` false, `error?` true, `action` `:allow`). The caller
484
484
  should treat the request as allowed.
485
485
 
486
+ A negative `cost:` takes the same path: it would credit quota back, so `check`
487
+ refuses it before touching any rule. Nothing is counted and the WARN line
488
+ carries `rule: null`.
489
+
486
490
  A failed-open `check` is still counted: it emits
487
491
  `checks_total{action="allow", matched="false", error="true"}`, so the fraction
488
492
  of checks that encountered an error is `checks_total{error="true"}` over
@@ -514,7 +518,6 @@ flooding).
514
518
  | `gitlab_labkit_rate_limiter_checks_total` | counter | `rate_limiter`, `action`, `matched`, `error` | Exactly one increment per `check` call, including fail-open. `action` is what the caller should do (`"allow"` or `"block"`); `matched` and `error` are `"true"`/`"false"`. |
515
519
  | `gitlab_labkit_rate_limiter_rule_evaluations_total` | counter | `rate_limiter`, `rule`, `action`, `result` | One increment per evaluated rule (plus one per matched `:skip` rule). `action` is the configured rule action (`"limit"`, `"log"`, `"skip"`); `result` is what the evaluation decided (`"allow"`, `"block"`, `"log"`, `"skip"`, `"banned"` — see the Actions table). |
516
520
  | `gitlab_labkit_rate_limiter_peeks_total` | counter | `rate_limiter`, `error` | Exactly one increment per `peek` call, including fail-open. `error` is `"true"`/`"false"`. |
517
- | `gitlab_labkit_rate_limiter_enforced_total` | counter | `rate_limiter`, `rule` | One increment per request the caller rejected on a `:block`. Emitted by the caller, not the evaluator: a rollout gate may observe a block without enforcing it. |
518
521
  | `gitlab_labkit_rate_limiter_limit` | gauge | `rate_limiter`, `rule` | Resolved limit at the last check (useful when `limit:` is callable). |
519
522
  | `gitlab_labkit_rate_limiter_period_seconds` | gauge | `rate_limiter`, `rule` | Resolved period at the last check. |
520
523
 
@@ -120,6 +120,9 @@ module Labkit
120
120
 
121
121
  def check(identifier, cost: 1, rule_context: nil)
122
122
  cursor = RuleCursor.new
123
+ # A negative cost would credit quota back and open the window for good.
124
+ raise ArgumentError, "cost is #{cost}, must not be negative" if cost.negative?
125
+
123
126
  result = check_rules(identifier, cost, rule_context, cursor)
124
127
  rescue StandardError => e
125
128
  # Intentionally broad: fail-open applies to any unexpected error (network,
@@ -372,7 +375,7 @@ module Labkit
372
375
  count: count,
373
376
  # A ban outlives its counter, so count reads 0 once the window has
374
377
  # gone. Nothing is remaining while the ban still blocks.
375
- remaining: banned ? 0 : [resolved_limit - count, 0].max,
378
+ remaining: banned ? 0 : remaining_of(resolved_limit, count),
376
379
  reset_at: Time.now.utc + (banned ? ban_ttl : window_remaining)
377
380
  )
378
381
 
@@ -383,13 +386,21 @@ module Labkit
383
386
  info = Result::Info.new(
384
387
  resolved_limit: resolved_limit, resolved_period: resolved_period,
385
388
  count: count,
386
- remaining: [resolved_limit - count, 0].max,
389
+ remaining: remaining_of(resolved_limit, count),
387
390
  reset_at: Time.now.utc + (ttl >= 0 ? ttl : resolved_period)
388
391
  )
389
392
 
390
393
  Result::Evaluation.new(rule: rule, exceeded: count > resolved_limit, info: info)
391
394
  end
392
395
 
396
+ # RateLimit-Remaining must not exceed RateLimit-Limit, and a counter can
397
+ # sit below zero if an older client credited it. A limit resolving to a
398
+ # non-positive value means "deny all", so the ceiling floors at zero too.
399
+ def remaining_of(resolved_limit, count)
400
+ ceiling = [resolved_limit, 0].max
401
+ (ceiling - count).clamp(0, ceiling)
402
+ end
403
+
393
404
  # The braces are the cluster hash tag: a counter and its ban must share a
394
405
  # slot because BAN_SCRIPT touches both in one call. The rule name stays
395
406
  # inside it so one identifier's keys spread across nodes instead of piling
@@ -35,6 +35,8 @@ module Labkit
35
35
  # @param cost [Numeric] amount to add to the counter. Defaults to 1
36
36
  # (count-mode). Pass a non-1 Numeric for cost-mode counters such as
37
37
  # resource-usage limits; passing 0 reads the counter without writing.
38
+ # A negative cost would credit quota back, so the check fails open and
39
+ # logs instead of counting.
38
40
  # @param rule_context [Hash, nil] optional per-request context passed to
39
41
  # one-arity callables on +limit+/+period+. Lets rules resolve dynamic
40
42
  # configuration (e.g. per-namespace settings) without rebuilding the
@@ -71,16 +71,6 @@ module Labkit
71
71
  )
72
72
  end
73
73
 
74
- # Emitted by the caller, once per rejected request: only the caller knows
75
- # whether it acted on a :block (a rollout gate may observe without enforcing).
76
- def enforced_total
77
- Labkit::Metrics::Client.counter(
78
- :gitlab_labkit_rate_limiter_enforced_total,
79
- 'Total number of requests rejected on a rate limit block',
80
- { rate_limiter: nil, rule: nil }
81
- )
82
- end
83
-
84
74
  def limit_gauge
85
75
  Labkit::Metrics::Client.gauge(
86
76
  :gitlab_labkit_rate_limiter_limit,
@@ -169,8 +169,10 @@ module Labkit
169
169
  # for default cost=1 callers, fractional for cost-mode callers.
170
170
  # Pre-2.x releases exposed this as Integer; see the migration
171
171
  # note in the cost-aware Lua script change.
172
- # remaining - requests remaining before the limit is hit (floors at 0).
173
- # Inherits Float typing from count when count is fractional.
172
+ # remaining - requests remaining before the limit is hit, clamped to
173
+ # [0, resolved_limit]; 0 throughout when the limit is
174
+ # non-positive. Inherits Float typing from count when
175
+ # count is fractional.
174
176
  # reset_at - best-effort UTC Time when the counter window resets
175
177
  Result::Info = Data.define(:resolved_limit, :resolved_period, :count, :remaining, :reset_at)
176
178
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-labkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.1
4
+ version: 5.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Newdigate
@@ -722,7 +722,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
722
722
  - !ruby/object:Gem::Version
723
723
  version: '0'
724
724
  requirements: []
725
- rubygems_version: 4.0.16
725
+ rubygems_version: 4.0.20
726
726
  specification_version: 4
727
727
  summary: Instrumentation for GitLab
728
728
  test_files: []