jira-auto-tool 1.1.5 → 1.2.0
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 +4 -4
- data/README.md +30 -14
- data/config/examples/jira-auto-tool.env.yaml.erb +4 -1
- data/features/configure_environment.feature +11 -6
- data/features/control_http_request_rate_limit.feature +19 -7
- data/features/create_sprints_using_existing_ones_as_reference.feature +1 -1
- data/lib/jira/auto/tool/rate_limited_jira_client/in_process_based.rb +26 -0
- data/lib/jira/auto/tool/rate_limited_jira_client/redis_based.rb +40 -0
- data/lib/jira/auto/tool/rate_limited_jira_client.rb +28 -28
- data/lib/jira/auto/tool/version.rb +1 -1
- data/lib/jira/auto/tool.rb +19 -13
- data/spec/jira/auto/tool/rate_limited_jira_client/in_process_based_spec.rb +65 -0
- data/spec/jira/auto/tool/rate_limited_jira_client/redis_based_spec.rb +56 -0
- data/spec/jira/auto/tool/rate_limited_jira_client_spec.rb +69 -48
- data/spec/jira/auto/tool_spec.rb +15 -21
- metadata +19 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f848715468127f8c2d342bdd77fd68f78b2e9684c67d506c0761e6982ce325d2
|
4
|
+
data.tar.gz: 3c578b782fce257b587e09c0261fe60c8fcb59293b6419e097b8f9fdd52e1bd4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 98c0d515b13ecf5f7f145eec5729435c7378d7325dae29bd250822461badcd47081a58ace1450b51c5b68eeb59d3552ff3905ae968c3e41fdc082c7af3141f37
|
7
|
+
data.tar.gz: 94871345d52b7809ad2267bf12f0d62114d0d10b1032fc93856a2fd78fef6c5c6308951fa6804e073ea3a0df046d81f3c648a6caadcacaac091ec1a98babe2b9
|
data/README.md
CHANGED
@@ -7,15 +7,23 @@
|
|
7
7
|
The purpose of this tool it support managing the sprints of multiple teams so it is easier to adjust to changes.
|
8
8
|
See the [feature files](./features) for some behavior examples.
|
9
9
|
|
10
|
-
##
|
10
|
+
## Table of Contents
|
11
11
|
|
12
|
-
|
13
|
-
That can be done easily by [creating a free Atlassian account](https://www.atlassian.com/software)
|
14
|
-
like it has been done to document [this tool features](./features) using executable specifications.
|
12
|
+
<!-- Generated using jekyll-toc -->
|
15
13
|
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
* TOC
|
15
|
+
{:toc}
|
16
|
+
|
17
|
+
## Principles
|
18
|
+
|
19
|
+
Following a convention over configuration approach:
|
20
|
+
* All Scrum boards from the Jira instance are scanned to identify the ones matching the search criteria
|
21
|
+
(the list is [cached for a day for performance reasons](./features/cache_boards.feature) to deal with Jira
|
22
|
+
instances having thousands of boards);
|
23
|
+
* For each board, only the unclosed sprints are considered;
|
24
|
+
* Sprint manipulations only apply to those sprints whose names match the following format: `sprint_prefix_25.4.3`
|
25
|
+
* [Creating new sprints](./features/create_sprints_using_existing_ones_as_reference.feature)
|
26
|
+
will use the existing ones as a reference for the prefix and the length of the sprint.
|
19
27
|
|
20
28
|
## Installation
|
21
29
|
|
@@ -41,11 +49,8 @@ in such a cloud sandbox. Though, if the sandbox belongs to the target context
|
|
41
49
|
```
|
42
50
|
2. Adjust the file to your context.
|
43
51
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
While we strive to use convention over configuration as a principle, the following environment variables have to be set
|
48
|
-
in order to use this tool:
|
52
|
+
The following environment variables have to be set to use this tool. **Except** for te `JIRA_API_TOKEN` that should
|
53
|
+
be done via the configuration file.
|
49
54
|
|
50
55
|
Some explanations:
|
51
56
|
|
@@ -67,8 +72,9 @@ Optional environment variables:
|
|
67
72
|
See [sprint filtering](./features/sprint_filtering.feature).
|
68
73
|
- `JIRA_CONTEXT_PATH` - Context path for Jira instance (if needed typically "/jira").
|
69
74
|
- `JIRA_HTTP_DEBUG` - Enable HTTP debug logging (set to "true" or "false").
|
70
|
-
- `
|
71
|
-
|
75
|
+
- `JAT_RATE_LIMIT_PER_INTERVAL` - Rate limit for Jira API calls (e.g., "1")
|
76
|
+
See [Control Jira HTTP request rate.](./features/control_http_request_rate_limit.feature).
|
77
|
+
- `JAT_RATE_INTERVAL_IN_SECONDS` - Interval for rate limiting in seconds (e.g., "1").
|
72
78
|
|
73
79
|
## Usage
|
74
80
|
|
@@ -79,6 +85,16 @@ See [sprint filtering](./features/sprint_filtering.feature).
|
|
79
85
|
* Leverage the [specification by examples](./features) for a detailled understand of the features.
|
80
86
|
* Note that usually the long option names have a short version equivalent to reduce typing.
|
81
87
|
|
88
|
+
### Warning
|
89
|
+
|
90
|
+
1. You should familiarize yourself with this tool in a Jira sandbox project **before applying it to your context**.
|
91
|
+
That can be done easily by [creating a free Atlassian account](https://www.atlassian.com/software)
|
92
|
+
like it has been done to document [this tool features](./features) using executable specifications.
|
93
|
+
|
94
|
+
1. Remember that you are **not allowed** to use confidential/sensitive information when familiarizing with this tool
|
95
|
+
in such a cloud sandbox. Though, if the sandbox belongs to the target context
|
96
|
+
(e.g., sandbox project on the client Jira instance) you can experiment with the parameters you intend to use later.
|
97
|
+
|
82
98
|
Below are a few examples.
|
83
99
|
|
84
100
|
### Add Sprints
|
@@ -10,12 +10,15 @@ DISABLE_COVERAGE: true
|
|
10
10
|
EXPECTED_START_DATE_FIELD_NAME: Expected Start
|
11
11
|
IMPLEMENTATION_TEAM_FIELD_NAME: "Implementation Team"
|
12
12
|
JAT_RATE_INTERVAL_IN_SECONDS:
|
13
|
-
|
13
|
+
JAT_RATE_LIMIT_IMPLEMENTATION:
|
14
|
+
JAT_RATE_LIMIT_PER_INTERVAL:
|
14
15
|
JAT_TICKETS_FOR_TEAM_SPRINT_TICKET_DISPATCHER_JQL: "project = <%= project_key %> AND <%= sprint_field_name %> IS EMPTY"
|
16
|
+
# JIRA_BOARD_NAME: "<<<Name of one board if the project>>>"
|
15
17
|
JIRA_BOARD_NAME: "<%= project_key %> - Delivery"
|
16
18
|
JIRA_BOARD_NAME_REGEX: "<%= project_key %>|ART 16|unconventional board name"
|
17
19
|
#JIRA_CONTEXT_PATH: /jira
|
18
20
|
JIRA_CONTEXT_PATH:
|
21
|
+
JIRA_HTTP_DEBUG:
|
19
22
|
JIRA_PROJECT_KEY: <%= project_key %>
|
20
23
|
JIRA_SITE_URL: http://cbroult.atlassian.net:443/
|
21
24
|
JIRA_SPRINT_FIELD_NAME: "<%= sprint_field_name %>"
|
@@ -17,7 +17,7 @@ Feature: Environment Configuration Management
|
|
17
17
|
"""
|
18
18
|
---
|
19
19
|
<%
|
20
|
-
project_key = "
|
20
|
+
project_key = "JATCIDEVLX"
|
21
21
|
sprint_field_name = "Sprint"
|
22
22
|
jira_username = "cbroult@yahoo.com"
|
23
23
|
%>
|
@@ -27,11 +27,14 @@ Feature: Environment Configuration Management
|
|
27
27
|
EXPECTED_START_DATE_FIELD_NAME: Expected Start
|
28
28
|
IMPLEMENTATION_TEAM_FIELD_NAME: "Implementation Team"
|
29
29
|
JAT_RATE_INTERVAL_IN_SECONDS:
|
30
|
-
|
30
|
+
JAT_RATE_LIMIT_IMPLEMENTATION:
|
31
|
+
JAT_RATE_LIMIT_PER_INTERVAL:
|
31
32
|
JAT_TICKETS_FOR_TEAM_SPRINT_TICKET_DISPATCHER_JQL: "project = <%= project_key %> AND <%= sprint_field_name %> IS EMPTY"
|
32
|
-
JIRA_BOARD_NAME: "
|
33
|
+
# JIRA_BOARD_NAME: "<<<Name of one board if the project>>>"
|
34
|
+
JIRA_BOARD_NAME: "<%= project_key %> - Delivery"
|
33
35
|
JIRA_BOARD_NAME_REGEX: "<%= project_key %>|ART 16|unconventional board name"
|
34
|
-
JIRA_CONTEXT_PATH: /jira
|
36
|
+
#JIRA_CONTEXT_PATH: /jira
|
37
|
+
JIRA_CONTEXT_PATH:
|
35
38
|
JIRA_HTTP_DEBUG:
|
36
39
|
JIRA_PROJECT_KEY: <%= project_key %>
|
37
40
|
JIRA_SITE_URL: http://cbroult.atlassian.net:443/
|
@@ -74,7 +77,8 @@ Feature: Environment Configuration Management
|
|
74
77
|
EXPECTED_START_DATE_FIELD_NAME: Expected Start
|
75
78
|
IMPLEMENTATION_TEAM_FIELD_NAME: "Implementation Team"
|
76
79
|
JAT_RATE_INTERVAL_IN_SECONDS:
|
77
|
-
|
80
|
+
JAT_RATE_LIMIT_IMPLEMENTATION:
|
81
|
+
JAT_RATE_LIMIT_PER_INTERVAL:
|
78
82
|
JAT_TICKETS_FOR_TEAM_SPRINT_TICKET_DISPATCHER_JQL: "project = <%= project_key %> AND <%= sprint_field_name %> IS EMPTY"
|
79
83
|
JIRA_API_TOKEN: "current API TOKEN"
|
80
84
|
JIRA_BOARD_NAME: "Team Board"
|
@@ -97,7 +101,8 @@ Feature: Environment Configuration Management
|
|
97
101
|
| EXPECTED_START_DATE_FIELD_NAME | Expected Start |
|
98
102
|
| IMPLEMENTATION_TEAM_FIELD_NAME | Implementation Team |
|
99
103
|
| JAT_RATE_INTERVAL_IN_SECONDS | |
|
100
|
-
|
|
104
|
+
| JAT_RATE_LIMIT_IMPLEMENTATION | |
|
105
|
+
| JAT_RATE_LIMIT_PER_INTERVAL | |
|
101
106
|
| JAT_TICKETS_FOR_TEAM_SPRINT_TICKET_DISPATCHER_JQL | project = PROJ AND Sprint IS EMPTY |
|
102
107
|
| JIRA_API_TOKEN | current API TOKEN |
|
103
108
|
| JIRA_BOARD_NAME | Team Board |
|
@@ -5,13 +5,25 @@ Feature: Control the HTTP request rate limit
|
|
5
5
|
|
6
6
|
Scenario Outline: Limiting the request rate
|
7
7
|
Given the following environment variables are set:
|
8
|
-
| name
|
9
|
-
|
|
10
|
-
|
|
8
|
+
| name | value |
|
9
|
+
| JAT_RATE_INTERVAL_IN_SECONDS | <rate_interval_in_seconds> |
|
10
|
+
| JAT_RATE_LIMIT_IMPLEMENTATION | <jat_rate_limit_implementation> |
|
11
|
+
| JAT_RATE_LIMIT_PER_INTERVAL | <rate_limit_per_interval> |
|
11
12
|
Then successfully running `jira-auto-tool --board-list --sprint-prefix` takes between <minimal_time> and <maximal_time> seconds
|
12
13
|
|
13
14
|
Examples:
|
14
|
-
|
|
15
|
-
| 0
|
16
|
-
| 1
|
17
|
-
| 1
|
15
|
+
| jat_rate_limit_implementation | rate_limit_per_interval | rate_interval_in_seconds | minimal_time | maximal_time |
|
16
|
+
| | 0 | 0 | 0 | 5 |
|
17
|
+
| in_process | 1 | 2 | 1 | 20 |
|
18
|
+
| redis | 1 | 2 | 1 | 20 |
|
19
|
+
| redis | 1 | 10 | 18 | 120 |
|
20
|
+
|
21
|
+
Scenario: Unexpected rate limiting implementation generates an error
|
22
|
+
Given the following environment variables are set:
|
23
|
+
| name | value |
|
24
|
+
| JAT_RATE_LIMIT_IMPLEMENTATION | UNKNOWN IMPLEMENTATION |
|
25
|
+
When I run `jira-auto-tool --board-list`
|
26
|
+
Then it should fail with:
|
27
|
+
"""
|
28
|
+
RuntimeError: "UNKNOWN IMPLEMENTATION": unexpected rate limiting implementation specified!
|
29
|
+
"""
|
@@ -34,7 +34,7 @@ Feature: Add sprints using existing ones as reference
|
|
34
34
|
| Food_Restaurant_25.3.4 | 2025-06-13 11:00:00 UTC | future |
|
35
35
|
|
36
36
|
Scenario: Add several planning interval sprints
|
37
|
-
When I successfully run `jira-auto-tool --
|
37
|
+
When I successfully run `jira-auto-tool --sa=25.2.2,3 --sa=25.3.1,4 --sa=25.4.1,5`
|
38
38
|
Then afterwards the board only has the following sprints:
|
39
39
|
| name | start_date | state |
|
40
40
|
| Food_Supply_25.1.3 | 2025-02-01 11:00:00 UTC | closed |
|
@@ -0,0 +1,26 @@
|
|
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
|
@@ -0,0 +1,40 @@
|
|
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,48 +1,48 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "ratelimit"
|
4
|
-
require "redis"
|
5
|
-
|
6
|
-
require "jira/auto/tool"
|
7
|
-
|
8
3
|
module Jira
|
9
4
|
module Auto
|
10
5
|
class Tool
|
11
6
|
class RateLimitedJiraClient < JIRA::Client
|
12
|
-
|
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
|
13
24
|
NO_RATE_INTERVAL_IN_SECONDS = 0
|
14
25
|
|
15
|
-
attr_reader :
|
26
|
+
attr_reader :rate_interval_in_seconds, :rate_limit_per_interval
|
16
27
|
|
17
|
-
def initialize(options,
|
28
|
+
def initialize(options, rate_interval_in_seconds: 1, rate_limit_per_interval: 1)
|
18
29
|
super(options)
|
19
|
-
@
|
20
|
-
@
|
30
|
+
@rate_interval_in_seconds = rate_interval_in_seconds
|
31
|
+
@rate_limit_per_interval = rate_limit_per_interval
|
21
32
|
end
|
22
33
|
|
23
34
|
alias original_request request
|
24
|
-
def request(*args)
|
25
|
-
return original_request(*args) if rate_limit == NO_RATE_LIMIT_IN_SECONDS
|
26
|
-
|
27
|
-
rate_limiter.exec_within_threshold(rate_limiter_key, interval: rate_interval, threshold: rate_limit) do
|
28
|
-
response = original_request(*args)
|
29
35
|
|
30
|
-
|
31
|
-
|
32
|
-
|
36
|
+
def request(*args)
|
37
|
+
if rate_limit_per_interval == NO_RATE_LIMIT_PER_INTERVAL
|
38
|
+
original_request(*args)
|
39
|
+
else
|
40
|
+
rate_limit { original_request(*args) }
|
33
41
|
end
|
34
42
|
end
|
35
43
|
|
36
|
-
def
|
37
|
-
"
|
38
|
-
end
|
39
|
-
|
40
|
-
def rate_limiter
|
41
|
-
self.class.rate_limiter(rate_limiter_key, rate_interval)
|
42
|
-
end
|
43
|
-
|
44
|
-
def self.rate_limiter(rate_limiter_key, rate_interval)
|
45
|
-
@rate_limiter ||= Ratelimit.new(rate_limiter_key, bucket_interval: rate_interval)
|
44
|
+
def rate_limit(&)
|
45
|
+
raise "rate_limit must be implemented by a subclass"
|
46
46
|
end
|
47
47
|
end
|
48
48
|
end
|
data/lib/jira/auto/tool.rb
CHANGED
@@ -83,15 +83,17 @@ module Jira
|
|
83
83
|
end
|
84
84
|
|
85
85
|
def jira_client
|
86
|
-
RateLimitedJiraClient
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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)
|
95
97
|
end
|
96
98
|
|
97
99
|
def jira_client_options
|
@@ -105,6 +107,7 @@ module Jira
|
|
105
107
|
}
|
106
108
|
end
|
107
109
|
|
110
|
+
# TODO: fix this overly complex logic
|
108
111
|
def jira_http_debug?
|
109
112
|
value = if config.key?(:jira_http_debug)
|
110
113
|
config[:jira_http_debug]
|
@@ -136,12 +139,13 @@ module Jira
|
|
136
139
|
jira_base_url + url
|
137
140
|
end
|
138
141
|
|
139
|
-
%i[
|
142
|
+
ENVIRONMENT_BASED_VALUE_SYMBOLS = %i[
|
140
143
|
art_sprint_regex
|
141
144
|
expected_start_date_field_name
|
142
145
|
implementation_team_field_name
|
143
|
-
jat_rate_limit_in_seconds
|
144
146
|
jat_rate_interval_in_seconds
|
147
|
+
jat_rate_limit_implementation
|
148
|
+
jat_rate_limit_per_interval
|
145
149
|
jat_tickets_for_team_sprint_ticket_dispatcher_jql
|
146
150
|
jira_api_token
|
147
151
|
jira_board_name
|
@@ -150,9 +154,11 @@ module Jira
|
|
150
154
|
jira_http_debug
|
151
155
|
jira_project_key
|
152
156
|
jira_site_url
|
153
|
-
jira_username
|
154
157
|
jira_sprint_field_name
|
155
|
-
|
158
|
+
jira_username
|
159
|
+
].freeze
|
160
|
+
|
161
|
+
ENVIRONMENT_BASED_VALUE_SYMBOLS.each do |method_name|
|
156
162
|
define_overridable_environment_based_value(method_name)
|
157
163
|
end
|
158
164
|
|
@@ -0,0 +1,65 @@
|
|
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
|
@@ -0,0 +1,56 @@
|
|
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,78 +1,99 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "rspec"
|
4
4
|
|
5
5
|
module Jira
|
6
6
|
module Auto
|
7
7
|
class Tool
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
let(:rate_interval) { 2 }
|
13
|
-
let(:rate_limit) { 1 }
|
14
|
-
let(:oauth_client) { instance_double(JIRA::OauthClient, request: nil, consumer: nil) }
|
15
|
-
let(:rate_limiter) { instance_double(Ratelimit) }
|
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) }
|
16
12
|
|
13
|
+
context "when the rate limiting implementation is unspecified" do
|
17
14
|
before do
|
18
|
-
allow(
|
19
|
-
|
20
|
-
allow(JIRA::OauthClient).to receive_messages(new: oauth_client)
|
15
|
+
allow(tool).to receive_messages(jat_rate_limit_implementation_when_defined_else: nil)
|
16
|
+
end
|
21
17
|
|
22
|
-
|
18
|
+
it { expect(result).to eq(RateLimitedJiraClient::InProcessBased) }
|
19
|
+
end
|
23
20
|
|
24
|
-
|
25
|
-
|
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")
|
26
24
|
end
|
27
25
|
|
28
|
-
it
|
29
|
-
|
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")
|
30
32
|
end
|
31
33
|
|
32
|
-
it
|
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
|
34
42
|
|
35
|
-
|
43
|
+
it do
|
44
|
+
expect { result }
|
45
|
+
.to raise_error(RuntimeError,
|
46
|
+
%("unexpected_implementation": unexpected rate limiting implementation specified!"))
|
36
47
|
end
|
48
|
+
end
|
49
|
+
end
|
37
50
|
|
38
|
-
|
39
|
-
|
40
|
-
|
51
|
+
RSpec.shared_examples "a rate limited client" do
|
52
|
+
before do
|
53
|
+
allow(client).to receive_messages(original_request: :response)
|
54
|
+
end
|
41
55
|
|
42
|
-
|
56
|
+
it "returns the response" do
|
57
|
+
expect(client.request(:get, "/path/to/resource")).to eq(:response)
|
58
|
+
end
|
43
59
|
|
44
|
-
|
45
|
-
|
46
|
-
.with("jira_auto_tool_api_requests", { interval: rate_interval, threshold: rate_limit })
|
47
|
-
.exactly(4).times
|
48
|
-
end
|
60
|
+
it "calls the original request method" do
|
61
|
+
client.request(:get, "/path/to/resource")
|
49
62
|
|
50
|
-
|
51
|
-
|
63
|
+
expect(client).to have_received(:original_request).with(:get, "/path/to/resource")
|
64
|
+
end
|
65
|
+
end
|
52
66
|
|
53
|
-
|
67
|
+
describe "#request" do
|
68
|
+
let(:client) { described_class.new({}, rate_interval_in_seconds:, rate_limit_per_interval:) }
|
54
69
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
.exactly(4).times
|
59
|
-
end
|
60
|
-
end
|
70
|
+
context "when the rate limiter is not needed" do
|
71
|
+
let(:rate_interval_in_seconds) { 0 }
|
72
|
+
let(:rate_limit_per_interval) { 0 }
|
61
73
|
|
62
|
-
|
63
|
-
let(:rate_limit) { 0 }
|
74
|
+
it_behaves_like "a rate limited client"
|
64
75
|
|
65
|
-
|
66
|
-
|
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)
|
67
79
|
|
68
|
-
|
69
|
-
|
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 }
|
70
87
|
|
71
|
-
|
72
|
-
|
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
|
73
95
|
|
74
|
-
|
75
|
-
end
|
96
|
+
client.request(:get, "/path/to/resource")
|
76
97
|
end
|
77
98
|
end
|
78
99
|
end
|
data/spec/jira/auto/tool_spec.rb
CHANGED
@@ -16,7 +16,10 @@ module Jira
|
|
16
16
|
end
|
17
17
|
|
18
18
|
describe "#board_controller" do
|
19
|
-
before
|
19
|
+
before do
|
20
|
+
allow(tool)
|
21
|
+
.to receive_messages(jira_client: instance_double(RateLimitedJiraClient))
|
22
|
+
end
|
20
23
|
|
21
24
|
it { expect(tool.board_controller).to be_a(BoardController) }
|
22
25
|
end
|
@@ -118,7 +121,9 @@ module Jira
|
|
118
121
|
end
|
119
122
|
|
120
123
|
describe "#project" do
|
121
|
-
let(:jira_client)
|
124
|
+
let(:jira_client) do
|
125
|
+
instance_double(RateLimitedJiraClient, Project: project_query)
|
126
|
+
end
|
122
127
|
let(:jira_project) { instance_double(JIRA::Resource::Project) }
|
123
128
|
let(:jira_project_key) { "JIRA_PROJECT_KEY" }
|
124
129
|
let(:project_query) { double("project_query", find: jira_project) } # rubocop:disable RSpec/VerifiedDoubles
|
@@ -217,21 +222,7 @@ module Jira
|
|
217
222
|
end
|
218
223
|
end
|
219
224
|
|
220
|
-
|
221
|
-
expected_start_date_field_name
|
222
|
-
implementation_team_field_name
|
223
|
-
jat_tickets_for_team_sprint_ticket_dispatcher_jql
|
224
|
-
jat_rate_limit_in_seconds
|
225
|
-
jat_rate_interval_in_seconds
|
226
|
-
jira_api_token
|
227
|
-
jira_board_name
|
228
|
-
jira_board_name_regex
|
229
|
-
jira_context_path
|
230
|
-
jira_http_debug
|
231
|
-
jira_project_key
|
232
|
-
jira_site_url jira_username
|
233
|
-
jira_sprint_field_name
|
234
|
-
].each do |method_name|
|
225
|
+
described_class::ENVIRONMENT_BASED_VALUE_SYMBOLS.each do |method_name|
|
235
226
|
describe "environment based values" do
|
236
227
|
let(:object_with_overridable_value) { tool }
|
237
228
|
|
@@ -277,21 +268,24 @@ module Jira
|
|
277
268
|
}
|
278
269
|
end
|
279
270
|
|
271
|
+
let(:expected_jira_client) { instance_double(RateLimitedJiraClient) }
|
272
|
+
|
280
273
|
before do
|
281
274
|
allow(tool)
|
282
275
|
.to receive_messages(jira_username: "jira_username_value", jira_site_url: "https://jira_site_url_value",
|
283
276
|
jira_api_token: "jira_api_token_value",
|
284
277
|
jira_context_path_when_defined_else: "/context_path_value",
|
285
278
|
jira_http_debug?: false,
|
286
|
-
|
279
|
+
jat_rate_limit_per_interval_when_defined_else: "10",
|
287
280
|
jat_rate_interval_in_seconds_when_defined_else: "60")
|
288
281
|
end
|
289
282
|
|
290
283
|
it "has a jira client" do
|
291
|
-
expected_jira_client = instance_double(RateLimitedJiraClient)
|
292
|
-
|
293
284
|
allow(RateLimitedJiraClient)
|
294
|
-
.to receive(:
|
285
|
+
.to receive(:implementation_class_for).with(tool).and_return(RateLimitedJiraClient::InProcessBased)
|
286
|
+
|
287
|
+
allow(RateLimitedJiraClient::InProcessBased)
|
288
|
+
.to receive(:new).with(client_options, rate_limit_per_interval: 10, rate_interval_in_seconds: 60)
|
295
289
|
.and_return(expected_jira_client)
|
296
290
|
|
297
291
|
expect(tool.jira_client).to equal(expected_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.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christophe Broult
|
@@ -163,6 +163,20 @@ dependencies:
|
|
163
163
|
- - ">="
|
164
164
|
- !ruby/object:Gem::Version
|
165
165
|
version: '0'
|
166
|
+
- !ruby/object:Gem::Dependency
|
167
|
+
name: ruby-limiter
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
173
|
+
type: :runtime
|
174
|
+
prerelease: false
|
175
|
+
version_requirements: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
166
180
|
- !ruby/object:Gem::Dependency
|
167
181
|
name: syslog
|
168
182
|
requirement: !ruby/object:Gem::Requirement
|
@@ -276,6 +290,8 @@ files:
|
|
276
290
|
- lib/jira/auto/tool/project/options.rb
|
277
291
|
- lib/jira/auto/tool/project/ticket_fields.rb
|
278
292
|
- lib/jira/auto/tool/rate_limited_jira_client.rb
|
293
|
+
- lib/jira/auto/tool/rate_limited_jira_client/in_process_based.rb
|
294
|
+
- lib/jira/auto/tool/rate_limited_jira_client/redis_based.rb
|
279
295
|
- lib/jira/auto/tool/request_builder.rb
|
280
296
|
- lib/jira/auto/tool/request_builder/field_context_fetcher.rb
|
281
297
|
- lib/jira/auto/tool/request_builder/field_option_fetcher.rb
|
@@ -327,6 +343,8 @@ files:
|
|
327
343
|
- spec/jira/auto/tool/performer/sprint_time_in_dates_aligner_spec.rb
|
328
344
|
- spec/jira/auto/tool/project/ticket_fields_spec.rb
|
329
345
|
- spec/jira/auto/tool/project_spec.rb
|
346
|
+
- spec/jira/auto/tool/rate_limited_jira_client/in_process_based_spec.rb
|
347
|
+
- spec/jira/auto/tool/rate_limited_jira_client/redis_based_spec.rb
|
330
348
|
- spec/jira/auto/tool/rate_limited_jira_client_spec.rb
|
331
349
|
- spec/jira/auto/tool/request_builder/field_context_fetcher_spec.rb
|
332
350
|
- spec/jira/auto/tool/request_builder/field_option_fetcher_spec.rb
|