jira-auto-tool 1.3.7 → 1.3.8

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: 32efe9c3614f3e24c2287c59ba8bedf2bbded1b2bb2fa8cfdb8c9da3f4fc2def
4
- data.tar.gz: 29a9e99effd2724cefc290d4e99e0b957d0fa243eeab0647f730ce5559bd3877
3
+ metadata.gz: 65ed9f4b2be361932c1b31bb18969304c31057d1db27f29b47a5c41f0f628402
4
+ data.tar.gz: e592e726bdabb7528e23440fefb04bfe9c556f355193bf4c82187571d75a7c07
5
5
  SHA512:
6
- metadata.gz: b73cfe4d770d8633ddb5e5dd80c60e69fc38a61dcb48284852ba1c851d8811a63268c1e66e43c8df8a95b26d84250bac75dda300d57e784ad17951a4943227ba
7
- data.tar.gz: '08db225b4adac6a7ad274cff647ab2f40c5ca5ac0f1e1f6383263db44cc8458efb9e3d802a39d82f185aa8178ff44b344b09149eedbd3e737e43ee4496c08977'
6
+ metadata.gz: 5aaa9c4165021446076dc4ff58a393eb6ce64c989487c3160bc312e32fb08e1901550689005cb01243d67578a3269254b3ca415514e8784626b04710473f24c2
7
+ data.tar.gz: b8e89f123ab16ab097b9fef0398728b4c4b4395bc4dd8f7d184e0f710cfe0b9e5aafd33f79f9ea3f7b341d07e46e3abf7b40f8481cd5c4ecb1c517904915d4c4
data/.jscpd.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "minTokens": 50,
3
+ "minLines": 5,
4
+ "threshold": 6,
5
+ "reporters": ["console"],
6
+ "ignore": ["**/*.gemspec", "tmp/**", "pkg/**", "coverage/**"]
7
+ }
data/CLAUDE.md ADDED
@@ -0,0 +1,51 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ bundle install # install dependencies
9
+ bundle exec rake verify # run all checks (rubocop + spec + cucumber)
10
+ bundle exec rspec # run all unit tests
11
+ bundle exec rspec spec/path/to/file_spec.rb # run a single spec file
12
+ bundle exec cucumber # run all integration tests
13
+ bundle exec cucumber features/foo.feature # run a single feature
14
+ bundle exec guard # continuous testing while developing
15
+ bundle exec rake rubocop # lint (with autocorrect)
16
+ bundle exec rake version:bump[patch] # bump version (major|minor|patch)
17
+ bundle exec rake release # release to rubygems.org
18
+ ```
19
+
20
+ Cucumber profiles: `default` (pretty output), `guard` (rerun on failure), `rake` (progress, skips `@wip`). Tags `@in-specification` are excluded from all runs; `@wip` only from `rake`.
21
+
22
+ ## Architecture
23
+
24
+ `Jira::Auto::Tool` (`lib/jira/auto/tool.rb`) is the central orchestrator. All configuration flows through it: environment variables are read via `EnvironmentBasedValue` and optionally overridden by a YAML config at `~/.config/jira-auto-tool/jira-auto-tool.config.yml`.
25
+
26
+ ### Key layers
27
+
28
+ **CLI → Performer → Tool → RequestBuilder → Jira API**
29
+
30
+ - **CLI** (`bin/jira-auto-tool`): OptionParser-based. Options are registered in `*/options.rb` files under each controller/performer namespace.
31
+ - **Controllers** (`BoardController`, `SprintController`, `FieldController`): Orchestrate reads and present tabular output via `terminal-table`.
32
+ - **Performers** (`lib/jira/auto/tool/performer/`): Encapsulate multi-sprint write operations: `SprintRenamer`, `QuarterlySprintRenamer`, `SprintEndDateUpdater`, `SprintTimeInDatesAligner`, `PlanningIncrementSprintCreator`.
33
+ - **RequestBuilder** (`lib/jira/auto/tool/request_builder/`): Abstract base for Jira REST calls. Concrete subclasses implement `http_verb`, `request_path`, `request_payload`, `expected_response`, and message prefix methods.
34
+ - **RateLimitedJiraClient**: Subclasses `JIRA::Client`. Two implementations: `InProcessBased` (default) and `RedisBased`. Selected via the `JAT_RATE_LIMIT_IMPLEMENTATION` env var.
35
+
36
+ ### Sprint naming convention
37
+
38
+ Sprints must match `{prefix}_{YY}.{quarter}.{index}` (e.g., `Food_Delivery_25.3.1`). The regex lives in `Sprint::Name::SPRINT_NAME_REGEX`. Sprints that don't match are ignored with a warning.
39
+
40
+ ### Board caching
41
+
42
+ `Board::Cache` persists board lists for one day to avoid hammering large Jira instances. Clear it with `Board::Cache.new(tool).clear`.
43
+
44
+ ### Environment-based configuration
45
+
46
+ `EnvironmentBasedValue` dynamically generates reader, predicate (`_defined?`), writer, and `_when_defined_else` methods for every env var listed in `Tool::ENVIRONMENT_BASED_VALUE_SYMBOLS`. Config file values take precedence over env vars.
47
+
48
+ ### Testing approach
49
+
50
+ - **Unit tests** (`spec/`): RSpec with `verify_partial_doubles`, `expect` syntax only. Coverage enforced at ≥90% overall / ≥80% per file via SimpleCov. Use `jira_resource_double` (defined in `spec_helper.rb`) instead of `double` for JIRA resource objects.
51
+ - **Integration tests** (`features/`): Cucumber + Aruba, connecting to a real Jira sandbox. Each scenario resets state by deleting all sprints and tickets from the board and clearing the board cache in `Before` hooks.
data/Rakefile CHANGED
@@ -19,7 +19,12 @@ Cucumber::Rake::Task.new do |t|
19
19
  t.profile = "rake"
20
20
  end
21
21
 
22
+ desc "Detect copy-paste duplication"
23
+ task :jscpd do
24
+ sh "jscpd lib/ spec/"
25
+ end
26
+
22
27
  task default: :verify
23
28
 
24
29
  desc "Run all checks"
25
- task verify: %i[rubocop spec cucumber]
30
+ task verify: %i[rubocop spec cucumber jscpd]
data/TODO.,md ADDED
@@ -0,0 +1,3 @@
1
+ # Packwerk
2
+
3
+ Please look into implementing Packwerk so we enforce better modularity if that makes sense here.
@@ -13,17 +13,6 @@ Given(/^the following environment variables are set:$/) do |table|
13
13
  end
14
14
  end
15
15
 
16
- BUFFER_TIME_IN_SECONDS = 10
17
- Then(/^successfully running `(.*)` takes between (.*) and (.*) seconds$/) do |command_line, minimal_time, maximal_time|
18
- start_time = Time.now
19
-
20
- run_command_and_stop(command_line, fail_on_error: true, timeout: maximal_time.to_i + BUFFER_TIME_IN_SECONDS)
21
-
22
- end_time = Time.now
23
-
24
- expect(end_time - start_time).to be_between(minimal_time.to_i, maximal_time.to_i)
25
- end
26
-
27
16
  Given(/^I wait for over a day$/) do
28
17
  in_over_a_day = (Time.now + 1.day + 2.minute).to_s
29
18
 
@@ -16,10 +16,12 @@ And(/^tickets on the board have a team field named "([^"]*)" with exactly those
16
16
  expect(@team_field.values.collect(&:value)).to eq(expected_field_values)
17
17
  end
18
18
 
19
+ TICKET_INDEX_WAIT_SECONDS = 30
20
+
19
21
  Given(/^the following tickets exist:$/) do |ticket_table|
20
22
  # Summary | Description | Implementation Team | Expected Start |
21
23
  # table is a table.hashes.keys # => [:summary, :team, :expected_start]
22
- ticket_table.hashes.each do |ticket_info|
24
+ created_keys = ticket_table.hashes.map do |ticket_info|
23
25
  log.debug { ticket_info.inspect }
24
26
 
25
27
  jira_ticket = @jira_auto_tool.jira_client.Issue.build
@@ -35,6 +37,20 @@ Given(/^the following tickets exist:$/) do |ticket_table|
35
37
  } })
36
38
 
37
39
  log.debug { "created jira ticket: #{jira_ticket.key}" }
40
+ jira_ticket.key
41
+ end
42
+
43
+ # Jira search index is eventually consistent — poll until all created tickets are searchable
44
+ jql = ENV.fetch("JAT_TICKETS_FOR_TEAM_SPRINT_TICKET_DISPATCHER_JQL",
45
+ "project = #{@jira_auto_tool.board.project_key}")
46
+ deadline = Time.now + TICKET_INDEX_WAIT_SECONDS
47
+ loop do
48
+ found_keys = @jira_auto_tool.jira_client.Issue.jql(jql, fields: ["summary"]).map(&:key)
49
+ break if (created_keys - found_keys).empty?
50
+ break if Time.now >= deadline
51
+
52
+ log.debug { "Waiting for Jira search index: #{(created_keys - found_keys).join(", ")} not yet searchable" }
53
+ sleep 2
38
54
  end
39
55
  end
40
56
 
@@ -10,15 +10,45 @@ module JiraSprintToolWorld
10
10
 
11
11
  log.debug { "Removing sprints #sprints = #{sprints.size}: #{sprints.map(&:name).join(", ")}" }
12
12
 
13
- sprints.each(&:delete)
13
+ sprints.each { |sprint| delete_sprint(jira_auto_tool, sprint) }
14
14
  end
15
15
 
16
16
  def remove_existing_board_tickets(jira_auto_tool)
17
- tickets = jira_auto_tool.jira_client.Issue.jql("project = #{jira_auto_tool.board.project_key}")
17
+ tickets = jira_auto_tool.jira_client.Issue.jql("project = #{jira_auto_tool.board.project_key}",
18
+ fields: ["key"])
18
19
 
19
20
  log.debug { "Removing tickets from board #{jira_auto_tool.board.name}: #tickets = #{tickets.size}" }
20
21
 
21
- tickets.each(&:delete)
22
+ tickets.each { |ticket| delete_ticket(ticket) }
23
+ end
24
+
25
+ private
26
+
27
+ def delete_sprint(jira_auto_tool, sprint)
28
+ close_active_sprint(jira_auto_tool, sprint)
29
+ sprint.delete
30
+ rescue JIRA::HTTPError => e
31
+ raise unless e.response.code == "404"
32
+
33
+ log.warn { "Sprint #{sprint.name} not found on delete — already removed or non-deletable state" }
34
+ end
35
+
36
+ def delete_ticket(ticket)
37
+ ticket.delete
38
+ rescue JIRA::HTTPError => e
39
+ raise unless e.response.code == "404"
40
+
41
+ log.warn { "Ticket #{ticket.key} not found on delete — already removed" }
42
+ end
43
+
44
+ def close_active_sprint(jira_auto_tool, sprint)
45
+ return unless sprint.state == Jira::Auto::Tool::SprintStateController::SprintState::ACTIVE
46
+
47
+ Jira::Auto::Tool::SprintStateController
48
+ .new(jira_auto_tool.jira_client, sprint)
49
+ .transition_to(Jira::Auto::Tool::SprintStateController::SprintState::CLOSED)
50
+ rescue StandardError => e
51
+ log.warn { "Failed to close active sprint #{sprint.name} before delete: #{e.message}" }
22
52
  end
23
53
  end
24
54
 
@@ -47,10 +47,23 @@ module Jira
47
47
  end
48
48
  end
49
49
 
50
+ MAX_STATE_UPDATE_RETRIES = 3
51
+ STATE_UPDATE_RETRY_DELAY_SECONDS = 2
52
+
50
53
  def update_sprint_state(new_state)
51
- RequestBuilder::SprintStateUpdater
52
- .new(jira_client, sprint: sprint, new_state: new_state)
53
- .run
54
+ retries = 0
55
+ begin
56
+ RequestBuilder::SprintStateUpdater
57
+ .new(jira_client, sprint: sprint, new_state: new_state)
58
+ .run
59
+ rescue JIRA::HTTPError => e
60
+ raise unless e.response.code == "404" && retries < MAX_STATE_UPDATE_RETRIES
61
+
62
+ retries += 1
63
+ log.warn { "Sprint #{sprint.name} not found on state update (attempt #{retries}), retrying..." }
64
+ sleep STATE_UPDATE_RETRY_DELAY_SECONDS
65
+ retry
66
+ end
54
67
  end
55
68
  end
56
69
  end
@@ -3,7 +3,7 @@
3
3
  module Jira
4
4
  module Auto
5
5
  class Tool
6
- VERSION = "1.3.7"
6
+ VERSION = "1.3.8"
7
7
  end
8
8
  end
9
9
  end
@@ -6,12 +6,13 @@ require "active_support/core_ext/numeric/time"
6
6
  require "active_support/core_ext/date/calculations"
7
7
  require "jira-ruby"
8
8
 
9
+ require "rate_limited_jira"
10
+
9
11
  require_relative "tool/config"
10
12
  require_relative "tool/board_controller"
11
13
  require_relative "tool/environment_loader"
12
14
  require_relative "tool/helpers/environment_based_value"
13
15
  require_relative "tool/project"
14
- require_relative "tool/rate_limited_jira_client"
15
16
  require_relative "tool/request_builder"
16
17
  require_relative "tool/setup_logging"
17
18
  require_relative "tool/sprint_controller"
@@ -83,17 +84,12 @@ module Jira
83
84
  end
84
85
 
85
86
  def jira_client
86
- RateLimitedJiraClient
87
- .implementation_class_for(self)
88
- .new(jira_client_options,
89
- rate_interval_in_seconds:
90
- jat_rate_interval_in_seconds_when_defined_else(
91
- RateLimitedJiraClient::RedisBased::NO_RATE_INTERVAL_IN_SECONDS
92
- ).to_i,
93
- rate_limit_per_interval:
94
- jat_rate_limit_per_interval_when_defined_else(
95
- RateLimitedJiraClient::RedisBased::NO_RATE_LIMIT_PER_INTERVAL
96
- ).to_i)
87
+ RateLimitedJira::Client.build(
88
+ jira_client_options,
89
+ rate_interval_in_seconds: jat_rate_interval_in_seconds_when_defined_else(0).to_i,
90
+ rate_limit_per_interval: jat_rate_limit_per_interval_when_defined_else(0).to_i,
91
+ implementation: jat_rate_limit_implementation_when_defined_else("")
92
+ )
97
93
  end
98
94
 
99
95
  def jira_client_options
@@ -18,7 +18,7 @@ module Jira
18
18
  describe "#board_controller" do
19
19
  before do
20
20
  allow(tool)
21
- .to receive_messages(jira_client: instance_double(RateLimitedJiraClient))
21
+ .to receive_messages(jira_client: instance_double(RateLimitedJira::Client))
22
22
  end
23
23
 
24
24
  it { expect(tool.board_controller).to be_a(BoardController) }
@@ -122,7 +122,7 @@ module Jira
122
122
 
123
123
  describe "#project" do
124
124
  let(:jira_client) do
125
- instance_double(RateLimitedJiraClient, Project: project_query)
125
+ instance_double(RateLimitedJira::Client, Project: project_query)
126
126
  end
127
127
  let(:jira_project) { instance_double(JIRA::Resource::Project) }
128
128
  let(:jira_project_key) { "JIRA_PROJECT_KEY" }
@@ -275,7 +275,7 @@ module Jira
275
275
  }
276
276
  end
277
277
 
278
- let(:expected_jira_client) { instance_double(RateLimitedJiraClient) }
278
+ let(:expected_jira_client) { instance_double(RateLimitedJira::Client) }
279
279
 
280
280
  before do
281
281
  allow(tool)
@@ -284,16 +284,16 @@ module Jira
284
284
  jira_context_path_when_defined_else: "/context_path_value",
285
285
  jira_http_debug?: false,
286
286
  jat_rate_limit_per_interval_when_defined_else: "10",
287
- jat_rate_interval_in_seconds_when_defined_else: "60")
287
+ jat_rate_interval_in_seconds_when_defined_else: "60",
288
+ jat_rate_limit_implementation_when_defined_else: "")
288
289
  end
289
290
 
290
291
  it "has a jira client" do
291
- allow(RateLimitedJiraClient)
292
- .to receive(:implementation_class_for).with(tool).and_return(RateLimitedJiraClient::InProcessBased)
293
-
294
- allow(RateLimitedJiraClient::InProcessBased)
295
- .to receive(:new).with(client_options, rate_limit_per_interval: 10, rate_interval_in_seconds: 60)
296
- .and_return(expected_jira_client)
292
+ allow(RateLimitedJira::Client)
293
+ .to receive(:build).with(client_options, rate_limit_per_interval: 10,
294
+ rate_interval_in_seconds: 60,
295
+ implementation: "")
296
+ .and_return(expected_jira_client)
297
297
 
298
298
  expect(tool.jira_client).to equal(expected_jira_client)
299
299
  end
@@ -425,7 +425,7 @@ module Jira
425
425
 
426
426
  describe "#tickets" do
427
427
  let(:query) { jira_resource_double("query") }
428
- let(:jira_client) { instance_double(RateLimitedJiraClient, Issue: query) }
428
+ let(:jira_client) { instance_double(RateLimitedJira::Client, Issue: query) }
429
429
 
430
430
  before do
431
431
  allow(tool).to receive_messages(jira_client: jira_client)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jira-auto-tool
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.7
4
+ version: 1.3.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christophe Broult
@@ -108,7 +108,7 @@ dependencies:
108
108
  - !ruby/object:Gem::Version
109
109
  version: '0'
110
110
  - !ruby/object:Gem::Dependency
111
- name: ratelimit
111
+ name: rate-limited-jira-ruby
112
112
  requirement: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - ">="
@@ -149,20 +149,6 @@ dependencies:
149
149
  - - ">="
150
150
  - !ruby/object:Gem::Version
151
151
  version: '0'
152
- - !ruby/object:Gem::Dependency
153
- name: redis
154
- requirement: !ruby/object:Gem::Requirement
155
- requirements:
156
- - - ">="
157
- - !ruby/object:Gem::Version
158
- version: '0'
159
- type: :runtime
160
- prerelease: false
161
- version_requirements: !ruby/object:Gem::Requirement
162
- requirements:
163
- - - ">="
164
- - !ruby/object:Gem::Version
165
- version: '0'
166
152
  - !ruby/object:Gem::Dependency
167
153
  name: reline
168
154
  requirement: !ruby/object:Gem::Requirement
@@ -177,20 +163,6 @@ dependencies:
177
163
  - - ">="
178
164
  - !ruby/object:Gem::Version
179
165
  version: '0'
180
- - !ruby/object:Gem::Dependency
181
- name: ruby-limiter
182
- requirement: !ruby/object:Gem::Requirement
183
- requirements:
184
- - - ">="
185
- - !ruby/object:Gem::Version
186
- version: '0'
187
- type: :runtime
188
- prerelease: false
189
- version_requirements: !ruby/object:Gem::Requirement
190
- requirements:
191
- - - ">="
192
- - !ruby/object:Gem::Version
193
- version: '0'
194
166
  - !ruby/object:Gem::Dependency
195
167
  name: syslog
196
168
  requirement: !ruby/object:Gem::Requirement
@@ -226,15 +198,18 @@ executables:
226
198
  extensions: []
227
199
  extra_rdoc_files: []
228
200
  files:
201
+ - ".jscpd.json"
229
202
  - ".rspec"
230
203
  - ".rubocop.yml"
231
204
  - ".ruby-version"
232
205
  - CHANGELOG.md
206
+ - CLAUDE.md
233
207
  - CODE_OF_CONDUCT.md
234
208
  - Guardfile
235
209
  - LICENSE.txt
236
210
  - README.md
237
211
  - Rakefile
212
+ - TODO.,md
238
213
  - bin/jira-auto-tool
239
214
  - bin/jira-auto-tool.bat
240
215
  - config/examples/jira-auto-tool.env.yaml.erb
@@ -247,7 +222,6 @@ files:
247
222
  - features/assign_tickets_to_team_sprints.feature
248
223
  - features/cache_boards.feature
249
224
  - features/configure_environment.feature
250
- - features/control_http_request_rate_limit.feature
251
225
  - features/create_sprints_using_existing_ones_as_reference.feature
252
226
  - features/list_boards.feature
253
227
  - features/list_project_fields.feature
@@ -302,9 +276,6 @@ files:
302
276
  - lib/jira/auto/tool/project.rb
303
277
  - lib/jira/auto/tool/project/options.rb
304
278
  - lib/jira/auto/tool/project/ticket_fields.rb
305
- - lib/jira/auto/tool/rate_limited_jira_client.rb
306
- - lib/jira/auto/tool/rate_limited_jira_client/in_process_based.rb
307
- - lib/jira/auto/tool/rate_limited_jira_client/redis_based.rb
308
279
  - lib/jira/auto/tool/request_builder.rb
309
280
  - lib/jira/auto/tool/request_builder/field_context_fetcher.rb
310
281
  - lib/jira/auto/tool/request_builder/field_option_fetcher.rb
@@ -355,9 +326,6 @@ files:
355
326
  - spec/jira/auto/tool/performer/sprint_time_in_dates_aligner_spec.rb
356
327
  - spec/jira/auto/tool/project/ticket_fields_spec.rb
357
328
  - spec/jira/auto/tool/project_spec.rb
358
- - spec/jira/auto/tool/rate_limited_jira_client/in_process_based_spec.rb
359
- - spec/jira/auto/tool/rate_limited_jira_client/redis_based_spec.rb
360
- - spec/jira/auto/tool/rate_limited_jira_client_spec.rb
361
329
  - spec/jira/auto/tool/request_builder/field_context_fetcher_spec.rb
362
330
  - spec/jira/auto/tool/request_builder/field_option_fetcher_spec.rb
363
331
  - spec/jira/auto/tool/request_builder/get_spec.rb
@@ -1,35 +0,0 @@
1
- Feature: Control the HTTP request rate limit
2
- In order to work against a JIRA instance imposing API rate limits
3
- As a user
4
- I need the ability to control the HTTP request limit
5
-
6
- Scenario Outline: Limiting the request rate
7
- Given a Jira Scrum board
8
- And the board only has the following sprints:
9
- | name | length | start_date | state |
10
- | ART-16_CRM_24.4.1 | 2-week | 2024-12-01 11:00:00 UTC | closed |
11
- | ART-16_E2E-Test_24.4.2 | 4-day | 2024-12-05 11:00:00 UTC | future |
12
- | ART-32_Platform_24.4.7 | 3-week | 2024-10-07 11:00:00 UTC | future |
13
- And the following environment variables are set:
14
- | name | value |
15
- | JAT_RATE_INTERVAL_IN_SECONDS | <rate_interval_in_seconds> |
16
- | JAT_RATE_LIMIT_IMPLEMENTATION | <jat_rate_limit_implementation> |
17
- | JAT_RATE_LIMIT_PER_INTERVAL | <rate_limit_per_interval> |
18
- Then successfully running `jira-auto-tool --board-list --sprint-prefix` takes between <minimal_time> and <maximal_time> seconds
19
-
20
- Examples:
21
- | jat_rate_limit_implementation | rate_limit_per_interval | rate_interval_in_seconds | minimal_time | maximal_time |
22
- | | 0 | 0 | 0 | 5 |
23
- | in_process | 1 | 1 | 1 | 20 |
24
- | redis | 1 | 2 | 1 | 20 |
25
- | redis | 1 | 10 | 18 | 120 |
26
-
27
- Scenario: Unexpected rate limiting implementation generates an error
28
- Given the following environment variables are set:
29
- | name | value |
30
- | JAT_RATE_LIMIT_IMPLEMENTATION | UNKNOWN IMPLEMENTATION |
31
- When I run `jira-auto-tool --board-list`
32
- Then it should fail with:
33
- """
34
- RuntimeError: "UNKNOWN IMPLEMENTATION": unexpected rate limiting implementation specified!
35
- """
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "ruby-limiter"
4
-
5
- require_relative "../rate_limited_jira_client"
6
-
7
- module Jira
8
- module Auto
9
- class Tool
10
- class RateLimitedJiraClient
11
- class InProcessBased < RateLimitedJiraClient
12
- def rate_limit(&block)
13
- rate_queue.shift
14
-
15
- block.call
16
- end
17
-
18
- def rate_queue
19
- @rate_queue ||=
20
- Limiter::RateQueue.new(rate_limit_per_interval, interval: rate_interval_in_seconds)
21
- end
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../rate_limited_jira_client"
4
- require "ratelimit"
5
- require "redis"
6
-
7
- require "jira/auto/tool"
8
-
9
- module Jira
10
- module Auto
11
- class Tool
12
- class RateLimitedJiraClient
13
- class RedisBased < RateLimitedJiraClient
14
- def rate_limit(&block)
15
- rate_limiter.exec_within_threshold(rate_limiter_key, interval: rate_interval_in_seconds,
16
- threshold: rate_limit_per_interval) do
17
- response = block.call
18
-
19
- rate_limiter.add(rate_limiter_key)
20
-
21
- response
22
- end
23
- end
24
-
25
- def rate_limiter_key
26
- "jira_auto_tool_api_requests"
27
- end
28
-
29
- def rate_limiter
30
- self.class.rate_limiter(rate_limiter_key, rate_interval_in_seconds)
31
- end
32
-
33
- def self.rate_limiter(rate_limiter_key, rate_interval)
34
- @rate_limiter ||= Ratelimit.new(rate_limiter_key, bucket_interval: rate_interval)
35
- end
36
- end
37
- end
38
- end
39
- end
40
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Jira
4
- module Auto
5
- class Tool
6
- class RateLimitedJiraClient < JIRA::Client
7
- require_relative "rate_limited_jira_client/in_process_based"
8
- require_relative "rate_limited_jira_client/redis_based"
9
-
10
- def self.implementation_class_for(tool)
11
- requested_implementation = tool.jat_rate_limit_implementation_when_defined_else nil
12
-
13
- case requested_implementation
14
- when "in_process", "", nil
15
- InProcessBased
16
- when "redis"
17
- RedisBased
18
- else
19
- raise %(#{requested_implementation.inspect}: unexpected rate limiting implementation specified!")
20
- end
21
- end
22
-
23
- NO_RATE_LIMIT_PER_INTERVAL = 0
24
- NO_RATE_INTERVAL_IN_SECONDS = 0
25
-
26
- attr_reader :rate_interval_in_seconds, :rate_limit_per_interval
27
-
28
- def initialize(options, rate_interval_in_seconds: 1, rate_limit_per_interval: 1)
29
- super(options)
30
- @rate_interval_in_seconds = rate_interval_in_seconds
31
- @rate_limit_per_interval = rate_limit_per_interval
32
- end
33
-
34
- alias original_request request
35
-
36
- def request(*)
37
- if rate_limit_per_interval == NO_RATE_LIMIT_PER_INTERVAL
38
- original_request(*)
39
- else
40
- rate_limit { original_request(*) }
41
- end
42
- end
43
-
44
- def rate_limit(&)
45
- raise "rate_limit must be implemented by a subclass"
46
- end
47
- end
48
- end
49
- end
50
- end
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "jira/auto/tool/rate_limited_jira_client/in_process_based"
4
-
5
- module Jira
6
- module Auto
7
- class Tool
8
- class RateLimitedJiraClient
9
- class InProcessBased
10
- RSpec.describe InProcessBased do
11
- def build_client
12
- described_class.new({}, rate_interval_in_seconds:, rate_limit_per_interval:)
13
- end
14
-
15
- let(:client) { build_client }
16
-
17
- let(:rate_interval_in_seconds) { 2 }
18
- let(:rate_limit_per_interval) { 1 }
19
-
20
- describe "#rate_limit" do
21
- let(:rate_queue) { instance_double(Limiter::RateQueue) }
22
-
23
- it "properly initializes the rate queue" do
24
- allow(Limiter::RateQueue)
25
- .to receive(:new).with(rate_limit_per_interval, interval: rate_interval_in_seconds)
26
- .and_return(rate_queue)
27
-
28
- allow(rate_queue).to receive(:shift)
29
-
30
- expect(client.rate_limit { :do_nothing }).to eq(:do_nothing)
31
- end
32
-
33
- context "when rate limiting multiple requests" do
34
- let(:rate_limit_4_calls_to_original_request_code) do
35
- 4.times { client.rate_limit { client.original_request(:get, "/path/to/resource") } }
36
- end
37
-
38
- before do
39
- allow(client).to receive(:original_request).with(:get, "/path/to/resource")
40
- allow(client).to receive_messages(rate_queue: rate_queue)
41
- allow(rate_queue).to receive(:shift)
42
- end
43
-
44
- it "shifts the queue and performs the request call" do
45
- rate_limit_4_calls_to_original_request_code
46
-
47
- expect(rate_queue).to have_received(:shift).exactly(4).times
48
- expect(client).to have_received(:original_request).exactly(4).times
49
- end
50
- end
51
-
52
- describe "#rate_queue" do
53
- let(:another_client) { build_client }
54
-
55
- it "creating a second client will return another queue" do
56
- expect(another_client.rate_queue).not_to equal(client.rate_queue)
57
- end
58
- end
59
- end
60
- end
61
- end
62
- end
63
- end
64
- end
65
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "jira/auto/tool/rate_limited_jira_client/redis_based"
4
-
5
- module Jira
6
- module Auto
7
- class Tool
8
- class RateLimitedJiraClient
9
- class RedisBased
10
- RSpec.describe RedisBased do
11
- describe "#rate_limit" do
12
- let(:client) { described_class.new({}, rate_interval_in_seconds:, rate_limit_per_interval:) }
13
-
14
- let(:rate_interval_in_seconds) { 2 }
15
- let(:rate_limit_per_interval) { 1 }
16
- let(:rate_limiter) { instance_double(Ratelimit) }
17
-
18
- let(:rate_limit_4_calls_to_original_request_code) do
19
- 4.times { client.rate_limit { client.original_request(:get, "/path/to/resource") } }
20
- end
21
-
22
- before do
23
- allow(described_class).to receive_messages(rate_limiter: rate_limiter)
24
- allow(client).to receive(:original_request)
25
- end
26
-
27
- it "uses :exec_within_threshold to control rate limiting" do
28
- allow(rate_limiter).to receive(:exec_within_threshold)
29
-
30
- rate_limit_4_calls_to_original_request_code
31
-
32
- expect(rate_limiter)
33
- .to have_received(:exec_within_threshold)
34
- .with("jira_auto_tool_api_requests", { interval: rate_interval_in_seconds,
35
- threshold: rate_limit_per_interval })
36
- .exactly(4).times
37
- end
38
-
39
- it "adds keeps track of the rate limiter key calls" do
40
- allow(rate_limiter).to receive(:exec_within_threshold).and_yield
41
- allow(rate_limiter).to receive(:add)
42
-
43
- rate_limit_4_calls_to_original_request_code
44
-
45
- expect(rate_limiter)
46
- .to have_received(:add)
47
- .with("jira_auto_tool_api_requests")
48
- .exactly(4).times
49
- end
50
- end
51
- end
52
- end
53
- end
54
- end
55
- end
56
- end
@@ -1,103 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rspec"
4
-
5
- module Jira
6
- module Auto
7
- class Tool
8
- RSpec.describe RateLimitedJiraClient do
9
- describe ".implementation_class_for" do
10
- let(:result) { described_class.implementation_class_for(tool) }
11
- let(:tool) { instance_double(Tool) }
12
-
13
- context "when the rate limiting implementation is unspecified" do
14
- before do
15
- allow(tool).to receive_messages(jat_rate_limit_implementation_when_defined_else: nil)
16
- end
17
-
18
- it { expect(result).to eq(RateLimitedJiraClient::InProcessBased) }
19
- end
20
-
21
- context "when using in process based rate limiting" do
22
- before do
23
- allow(tool).to receive_messages(jat_rate_limit_implementation_when_defined_else: "in_process")
24
- end
25
-
26
- it { expect(result).to eq(RateLimitedJiraClient::InProcessBased) }
27
- end
28
-
29
- context "when using in Redis based rate limiting" do
30
- before do
31
- allow(tool).to receive_messages(jat_rate_limit_implementation_when_defined_else: "redis")
32
- end
33
-
34
- it { expect(result).to eq(RateLimitedJiraClient::RedisBased) }
35
- end
36
-
37
- context "when the request implementation is unexpected" do
38
- before do
39
- allow(tool)
40
- .to receive_messages(jat_rate_limit_implementation_when_defined_else: "unexpected_implementation")
41
- end
42
-
43
- it do
44
- expect { result }
45
- .to raise_error(RuntimeError,
46
- %("unexpected_implementation": unexpected rate limiting implementation specified!"))
47
- end
48
- end
49
- end
50
-
51
- RSpec.shared_examples "a rate limited client" do
52
- before do
53
- allow(client).to receive_messages(original_request: :response)
54
- end
55
-
56
- it "returns the response" do
57
- expect(client.request(:get, "/path/to/resource")).to eq(:response)
58
- end
59
-
60
- it "calls the original request method" do
61
- client.request(:get, "/path/to/resource")
62
-
63
- expect(client).to have_received(:original_request).with(:get, "/path/to/resource")
64
- end
65
- end
66
-
67
- describe "#request" do
68
- let(:client) { described_class.new({}, rate_interval_in_seconds:, rate_limit_per_interval:) }
69
-
70
- context "when the rate limiter is not needed" do
71
- let(:rate_interval_in_seconds) { 0 }
72
- let(:rate_limit_per_interval) { 0 }
73
-
74
- it_behaves_like "a rate limited client"
75
-
76
- it "does not use the rate limiter" do
77
- allow(client).to receive(:original_request).with(:get, "/path/to/resource")
78
- expect(client).not_to receive(:rate_limit)
79
-
80
- client.request(:get, "/path/to/resource")
81
- end
82
- end
83
-
84
- context "when the rate limiter is needed" do
85
- let(:rate_interval_in_seconds) { 2 }
86
- let(:rate_limit_per_interval) { 1 }
87
-
88
- it_behaves_like "a rate limited client" do
89
- before { allow(client).to receive(:rate_limit).and_yield }
90
- end
91
-
92
- it "uses the rate limiter" do
93
- allow(client).to receive(:original_request).with(:get, "/path/to/resource")
94
- expect(client).to receive(:rate_limit).and_yield
95
-
96
- client.request(:get, "/path/to/resource")
97
- end
98
- end
99
- end
100
- end
101
- end
102
- end
103
- end