embulk-input-jira 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a7ada5966316509edccb0d858aee0e74fc6907dc
4
- data.tar.gz: 5e80e6f96afe4aef6994579abb49cd466e097a4b
3
+ metadata.gz: f18cc941dfa5acf3d4042b5bf94be8f56f839874
4
+ data.tar.gz: fe29da077a7423972da281afd02d9818ddbb1214
5
5
  SHA512:
6
- metadata.gz: cb335f39601ae83421e5a5b3ce632d3be06c35592d21b290e68f74fe5f14c01c9a92947b0b28ed33bea4e8b6128d7e4746f17fdbbabc6d6513b4c27572f03640
7
- data.tar.gz: 1508035dad8ad5c76c138bda382686f5bf90cc470e4359f01f637df93ea3cae4dfa21645e40a7ec98c1e755723bc3be058b7f76ae3a903925610c798d8b8cf75
6
+ metadata.gz: c65a216e19e5117bafc0b72a460c8705e19249aecfb5869cf92d2ee5f120d438f1dedcc34a947b92388b1c236542da39fbfcdc66d44a1c335662c4837dbac0a4
7
+ data.tar.gz: 1791e18b0d566961f64dfa20c854dfba3aa712732cf28974577e3ac903c6fcd9be287eab04279eefc8e3bc452e0ad73dae7a3da4de36de123a9c90422c9bbee1
data/.gitignore CHANGED
@@ -5,3 +5,4 @@
5
5
  /Gemfile.lock
6
6
  /.ruby-version
7
7
  /coverage/
8
+ /vendor/*
@@ -1,3 +1,7 @@
1
+ ## 0.2.5 - 2018-11-27
2
+
3
+ * [fixed] Fix infinitive 401 errors and dynamically adjust parallel threads [#52](https://github.com/treasure-data/embulk-input-jira/pull/52)
4
+
1
5
  ## 0.2.4 - 2017-11-17
2
6
 
3
7
  * [fixed] Fixed checking credentials by `Jiralicious User` [#51](https://github.com/treasure-data/embulk-input-jira/pull/51)
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "embulk-input-jira"
3
- spec.version = "0.2.4"
3
+ spec.version = "0.2.5"
4
4
  spec.authors = ["uu59", "yoshihara"]
5
5
  spec.summary = "Jira input plugin for Embulk"
6
6
  spec.description = "Loads records from Jira."
@@ -14,6 +14,7 @@ Gem::Specification.new do |spec|
14
14
 
15
15
  spec.add_dependency 'jiralicious', ['~> 0.5.0']
16
16
  spec.add_dependency 'parallel', ['~> 1.6.0']
17
+ spec.add_dependency 'ruby-limiter', ['~> 1.0']
17
18
  spec.add_dependency 'perfect_retry', ['~> 0.3']
18
19
  spec.add_development_dependency 'bundler', ['~> 1.0']
19
20
  spec.add_development_dependency 'rake', ['< 11.0']
@@ -103,7 +103,7 @@ module Embulk
103
103
  last_page = (total_count.to_f / PER_PAGE).ceil
104
104
 
105
105
  0.step(total_count, PER_PAGE).with_index(1) do |start_at, page|
106
- logger.debug "Fetching #{page} / #{last_page} page"
106
+ logger.info "Fetching #{page} / #{last_page} page"
107
107
  @retryer.with_retry do
108
108
  @jira.search_issues(@jql, options.merge(start_at: start_at)).each do |issue|
109
109
  values = @attributes.map do |(attribute_name, type)|
@@ -1,5 +1,6 @@
1
1
  require "jiralicious"
2
2
  require "parallel"
3
+ require "limiter"
3
4
  require "embulk/input/jira_api/issue"
4
5
  require "timeout"
5
6
 
@@ -7,31 +8,68 @@ module Embulk
7
8
  module Input
8
9
  module JiraApi
9
10
  class Client
10
- PARALLEL_THREAD_COUNT = 50
11
- SEARCH_TIMEOUT_SECONDS = 5
12
- SEARCH_ISSUES_TIMEOUT_SECONDS = 60
11
+ MAX_RATE_LIMIT = 50
12
+ MIN_RATE_LIMIT = 2
13
+ # Normal http request timeout is 300s
14
+ SEARCH_ISSUES_TIMEOUT_SECONDS = 300
13
15
  DEFAULT_SEARCH_RETRY_TIMES = 10
14
16
 
17
+ def initialize
18
+ @rate_limiter = Limiter::RateQueue.new(MAX_RATE_LIMIT, interval: 2)
19
+ end
20
+
15
21
  def self.setup(&block)
16
22
  Jiralicious.configure(&block)
17
23
  new
18
24
  end
19
25
 
20
26
  def search_issues(jql, options={})
21
- timeout_and_retry(SEARCH_ISSUES_TIMEOUT_SECONDS) do
22
- issues_raw = search(jql, options).issues_raw
23
-
24
- # TODO: below code has race-conditon.
25
- Parallel.map(issues_raw, in_threads: PARALLEL_THREAD_COUNT) do |issue_raw|
26
- # https://github.com/dorack/jiralicious/blob/v0.4.0/lib/jiralicious/search_result.rb#L32-34
27
- issue = Jiralicious::Issue.find(issue_raw["key"])
28
- JiraApi::Issue.new(issue)
27
+ issues_raw = search(jql, options).issues_raw
28
+ # Maximum number of issues to retrieve is 50
29
+ rate_limit = MAX_RATE_LIMIT
30
+ success_items = []
31
+ fail_items = []
32
+ error_object = nil
33
+ timeout_and_retry(SEARCH_ISSUES_TIMEOUT_SECONDS * MAX_RATE_LIMIT ) do
34
+ retry_count = 0
35
+ semaphore = Mutex.new
36
+ @rate_limiter = Limiter::RateQueue.new(rate_limit, interval: 2)
37
+ error_object = nil
38
+ while issues_raw.length > 0 && retry_count <= DEFAULT_SEARCH_RETRY_TIMES do
39
+ Parallel.each(issues_raw, in_threads: rate_limit) do |issue_raw|
40
+ # https://github.com/dorack/jiralicious/blob/v0.4.0/lib/jiralicious/search_result.rb#L32-34
41
+ begin
42
+ issue = find_issue(issue_raw["key"])
43
+ semaphore.synchronize {
44
+ success_items.push(JiraApi::Issue.new(issue))
45
+ }
46
+ rescue MultiJson::ParseError => e
47
+ html = e.message
48
+ title = html[%r|<title>(.*?)</title>|, 1]
49
+ # 401 due to high number of concurrent requests with current account
50
+ # The number of concurrent requests is not fixed by every account
51
+ # Hence catch the error item and retry later
52
+ raise title if title != "Unauthorized (401)"
53
+ semaphore.synchronize {
54
+ fail_items.push(issue_raw)
55
+ error_object = e
56
+ }
57
+ end
58
+ end
59
+ retry_count += 1
60
+ rate_limit = calculate_rate_limit(rate_limit, issues_raw.length, fail_items.length, retry_count)
61
+ issues_raw = fail_items
62
+ fail_items = []
63
+ raise error_object if retry_count > DEFAULT_SEARCH_RETRY_TIMES && !error_object.nil?
64
+ # Sleep after some seconds for JIRA API perhaps under the overload
65
+ sleep retry_count if fail_items.length > 0
29
66
  end
67
+ success_items
30
68
  end
31
69
  end
32
70
 
33
71
  def search(jql, options={})
34
- timeout_and_retry(SEARCH_TIMEOUT_SECONDS) do
72
+ timeout_and_retry(SEARCH_ISSUES_TIMEOUT_SECONDS) do
35
73
  Jiralicious.search(jql, options)
36
74
  end
37
75
  end
@@ -53,10 +91,19 @@ module Embulk
53
91
  raise ConfigError.new("Can not authorize with your credential.") if title == 'Unauthorized (401)'
54
92
  end
55
93
 
94
+ # Calculate rate limit based on previous run result
95
+ # Return 2 MIN_RATE_LIMIT in case turning from the 5th times or success_items is less than 2
96
+ # Otherwise return the min number between fail_items, success_items and current_limit
97
+ def calculate_rate_limit(current_limit, all_items, fail_items, times)
98
+ success_items = all_items - fail_items
99
+ return MIN_RATE_LIMIT if times >= DEFAULT_SEARCH_RETRY_TIMES/2 || success_items < MIN_RATE_LIMIT
100
+ return [fail_items, success_items, current_limit].min
101
+ end
102
+
56
103
  private
57
104
 
58
105
  def timeout_and_retry(wait, retry_times = DEFAULT_SEARCH_RETRY_TIMES, &block)
59
- count = 1
106
+ count = 0
60
107
  begin
61
108
  Timeout.timeout(wait) do
62
109
  yield
@@ -72,29 +119,25 @@ module Embulk
72
119
  # And (b) `search_issues` method has race-condition bug. If it occurred, MultiJson::ParseError raised too.
73
120
  html = e.message
74
121
  title = html[%r|<title>(.*?)</title>|, 1] #=> e.g. "Unauthorized (401)"
75
- if title
76
- # (a)
77
- case title
78
- when "Atlassian Cloud Notifications - Page Unavailable"
79
- # a.k.a. HTTP 503
80
- raise title
81
- when "Unauthorized (401)"
82
- Embulk.logger.warn "JIRA returns error: #{title}. Will go to retry"
83
- count += 1
84
- retry
85
- end
86
- else
87
- # (b)
88
- count += 1
89
- retry
90
- end
122
+ raise title if title == "Atlassian Cloud Notifications - Page Unavailable"
123
+ count += 1
124
+ raise title.nil? ? "Unknown Error" : title if count > retry_times
125
+ Embulk.logger.warn "JIRA returns error: #{title == 'Unauthorized (401)' ? title + " due to overloading API requests. Retrying on failed items only" : title}."
126
+ sleep count
127
+ retry
91
128
  rescue Timeout::Error => e
92
129
  count += 1
93
- sleep count # retry after some seconds for JIRA API perhaps under the overload
94
130
  raise e if count > retry_times
131
+ Embulk.logger.warn "Time out error."
132
+ sleep count # retry after some seconds for JIRA API perhaps under the overload
95
133
  retry
96
134
  end
97
135
  end
136
+
137
+ def find_issue(issue_key)
138
+ @rate_limiter.shift
139
+ Jiralicious::Issue.find(issue_key)
140
+ end
98
141
  end
99
142
  end
100
143
  end
@@ -30,7 +30,7 @@ describe Embulk::Input::JiraApi::Client do
30
30
  end
31
31
 
32
32
  it "retry DEFAULT_SEARCH_RETRY_TIMES times then raise error" do
33
- expect(Timeout).to receive(:timeout).exactly(Embulk::Input::JiraApi::Client::DEFAULT_SEARCH_RETRY_TIMES)
33
+ expect(Timeout).to receive(:timeout).exactly(Embulk::Input::JiraApi::Client::DEFAULT_SEARCH_RETRY_TIMES + 1)
34
34
  expect { subject }.to raise_error
35
35
  end
36
36
  end
@@ -38,6 +38,9 @@ describe Embulk::Input::JiraApi::Client do
38
38
 
39
39
  describe "#search_issues" do
40
40
  let(:jql) { "project=FOO" }
41
+ let(:jira_api) { Embulk::Input::JiraApi::Client.new }
42
+ let(:title_401) { "Unauthorized (401)"}
43
+ let(:multi_json_error) {MultiJson::ParseError.build(StandardError.new("<title>#{title_401}</title>"), {})}
41
44
  let(:results) do
42
45
  [
43
46
  {
@@ -67,15 +70,39 @@ describe Embulk::Input::JiraApi::Client do
67
70
  ]
68
71
  end
69
72
 
70
- subject { Embulk::Input::JiraApi::Client.new.search_issues(jql) }
73
+ subject { jira_api.search_issues(jql) }
71
74
 
72
- it do
75
+ it "Search issues successfully" do
76
+ allow(Jiralicious).to receive_message_chain(:search, :issues_raw).and_return(results)
77
+ allow(jira_api).to receive(:find_issue).and_return(results.first)
78
+
79
+ expect(subject).to be_kind_of Array
80
+ expect(subject.map(&:class)).to match_array [Embulk::Input::JiraApi::Issue, Embulk::Input::JiraApi::Issue]
81
+ end
82
+
83
+ it "Search issues successfully when first item success - 401 - second items success" do
73
84
  allow(Jiralicious).to receive_message_chain(:search, :issues_raw).and_return(results)
74
- allow(Jiralicious::Issue).to receive(:find).and_return(results.first)
85
+ allow(jira_api).to receive(:find_issue).and_return(results.first).and_raise(multi_json_error).and_return(results.first)
75
86
 
76
87
  expect(subject).to be_kind_of Array
77
88
  expect(subject.map(&:class)).to match_array [Embulk::Input::JiraApi::Issue, Embulk::Input::JiraApi::Issue]
78
89
  end
90
+
91
+ it "Search issues successfully when 401 - first and second items success" do
92
+ allow(Jiralicious).to receive_message_chain(:search, :issues_raw).and_return(results)
93
+ allow(jira_api).to receive(:find_issue).and_raise(multi_json_error).and_return(results.first)
94
+
95
+ expect(subject).to be_kind_of Array
96
+ expect(subject.map(&:class)).to match_array [Embulk::Input::JiraApi::Issue, Embulk::Input::JiraApi::Issue]
97
+ end
98
+
99
+ it "Search issues got 401 due to high concurrent load issues" do
100
+ allow(Jiralicious).to receive_message_chain(:search, :issues_raw).and_return(results)
101
+ allow(jira_api).to receive(:find_issue).and_raise(multi_json_error)
102
+ allow(jira_api).to receive(:sleep)
103
+
104
+ expect { subject }.to raise_error(StandardError, title_401)
105
+ end
79
106
  end
80
107
 
81
108
  describe "#total_count" do
@@ -120,7 +147,7 @@ describe Embulk::Input::JiraApi::Client do
120
147
  it "Always timeout, raise error after N times retry" do
121
148
  allow(Timeout).to receive(:timeout) { raise Timeout::Error }
122
149
 
123
- expect(Timeout).to receive(:timeout).with(wait).exactly(retry_times).times
150
+ expect(Timeout).to receive(:timeout).with(wait).exactly(retry_times + 1).times
124
151
  expect { subject }.to raise_error(Timeout::Error)
125
152
  end
126
153
 
@@ -146,4 +173,52 @@ describe Embulk::Input::JiraApi::Client do
146
173
  end
147
174
  end
148
175
  end
176
+
177
+ describe "#calculate_rate_limit" do
178
+ let(:jira_api) { Embulk::Input::JiraApi::Client.new }
179
+ it "current_limit = 50, all_items = 50, fail_items=50, times=1" do
180
+ current_limit = 50
181
+ all_items = 50
182
+ fail_items = 50
183
+ times = 1
184
+ expected_result = Embulk::Input::JiraApi::Client::MIN_RATE_LIMIT
185
+ expect(jira_api.calculate_rate_limit(current_limit, all_items, fail_items, times)).to eq expected_result
186
+ end
187
+
188
+ it "current_limit = 50, all_items = 50, fail_items=20, times=1" do
189
+ current_limit = 50
190
+ all_items = 50
191
+ fail_items = 20
192
+ times = 1
193
+ expected_result = 20
194
+ expect(jira_api.calculate_rate_limit(current_limit, all_items, fail_items, times)).to eq expected_result
195
+ end
196
+
197
+ it "current_limit = MIN_RATE_LIMIT, all_items = 50, fail_items=20, times=2" do
198
+ current_limit = Embulk::Input::JiraApi::Client::MIN_RATE_LIMIT
199
+ all_items = 50
200
+ fail_items = 20
201
+ times = 2
202
+ expected_result = Embulk::Input::JiraApi::Client::MIN_RATE_LIMIT
203
+ expect(jira_api.calculate_rate_limit(current_limit, all_items, fail_items, times)).to eq expected_result
204
+ end
205
+
206
+ it "current_limit = 10, all_items = 30, fail_items=25, times=2" do
207
+ current_limit = 10
208
+ all_items = 30
209
+ fail_items = 25
210
+ times = 2
211
+ expected_result = 5
212
+ expect(jira_api.calculate_rate_limit(current_limit, all_items, fail_items, times)).to eq expected_result
213
+ end
214
+
215
+ it "current_limit = 50, all_items = 50, fail_items=20, times=DEFAULT_SEARCH_RETRY_TIMES/2" do
216
+ current_limit = 50
217
+ all_items = 50
218
+ fail_items = 20
219
+ times = Embulk::Input::JiraApi::Client::DEFAULT_SEARCH_RETRY_TIMES/2
220
+ expected_result = Embulk::Input::JiraApi::Client::MIN_RATE_LIMIT
221
+ expect(jira_api.calculate_rate_limit(current_limit, all_items, fail_items, times)).to eq expected_result
222
+ end
223
+ end
149
224
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: embulk-input-jira
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - uu59
@@ -9,162 +9,176 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-11-17 00:00:00.000000000 Z
12
+ date: 2018-11-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: jiralicious
16
15
  requirement: !ruby/object:Gem::Requirement
17
16
  requirements:
18
17
  - - "~>"
19
18
  - !ruby/object:Gem::Version
20
19
  version: 0.5.0
20
+ name: jiralicious
21
+ prerelease: false
22
+ type: :runtime
21
23
  version_requirements: !ruby/object:Gem::Requirement
22
24
  requirements:
23
25
  - - "~>"
24
26
  - !ruby/object:Gem::Version
25
27
  version: 0.5.0
26
- prerelease: false
27
- type: :runtime
28
28
  - !ruby/object:Gem::Dependency
29
- name: parallel
30
29
  requirement: !ruby/object:Gem::Requirement
31
30
  requirements:
32
31
  - - "~>"
33
32
  - !ruby/object:Gem::Version
34
33
  version: 1.6.0
34
+ name: parallel
35
+ prerelease: false
36
+ type: :runtime
35
37
  version_requirements: !ruby/object:Gem::Requirement
36
38
  requirements:
37
39
  - - "~>"
38
40
  - !ruby/object:Gem::Version
39
41
  version: 1.6.0
42
+ - !ruby/object:Gem::Dependency
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ name: ruby-limiter
40
49
  prerelease: false
41
50
  type: :runtime
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '1.0'
42
56
  - !ruby/object:Gem::Dependency
43
- name: perfect_retry
44
57
  requirement: !ruby/object:Gem::Requirement
45
58
  requirements:
46
59
  - - "~>"
47
60
  - !ruby/object:Gem::Version
48
61
  version: '0.3'
62
+ name: perfect_retry
63
+ prerelease: false
64
+ type: :runtime
49
65
  version_requirements: !ruby/object:Gem::Requirement
50
66
  requirements:
51
67
  - - "~>"
52
68
  - !ruby/object:Gem::Version
53
69
  version: '0.3'
54
- prerelease: false
55
- type: :runtime
56
70
  - !ruby/object:Gem::Dependency
57
- name: bundler
58
71
  requirement: !ruby/object:Gem::Requirement
59
72
  requirements:
60
73
  - - "~>"
61
74
  - !ruby/object:Gem::Version
62
75
  version: '1.0'
76
+ name: bundler
77
+ prerelease: false
78
+ type: :development
63
79
  version_requirements: !ruby/object:Gem::Requirement
64
80
  requirements:
65
81
  - - "~>"
66
82
  - !ruby/object:Gem::Version
67
83
  version: '1.0'
68
- prerelease: false
69
- type: :development
70
84
  - !ruby/object:Gem::Dependency
71
- name: rake
72
85
  requirement: !ruby/object:Gem::Requirement
73
86
  requirements:
74
87
  - - "<"
75
88
  - !ruby/object:Gem::Version
76
89
  version: '11.0'
90
+ name: rake
91
+ prerelease: false
92
+ type: :development
77
93
  version_requirements: !ruby/object:Gem::Requirement
78
94
  requirements:
79
95
  - - "<"
80
96
  - !ruby/object:Gem::Version
81
97
  version: '11.0'
82
- prerelease: false
83
- type: :development
84
98
  - !ruby/object:Gem::Dependency
85
- name: rspec
86
99
  requirement: !ruby/object:Gem::Requirement
87
100
  requirements:
88
101
  - - "~>"
89
102
  - !ruby/object:Gem::Version
90
103
  version: 3.2.0
104
+ name: rspec
105
+ prerelease: false
106
+ type: :development
91
107
  version_requirements: !ruby/object:Gem::Requirement
92
108
  requirements:
93
109
  - - "~>"
94
110
  - !ruby/object:Gem::Version
95
111
  version: 3.2.0
96
- prerelease: false
97
- type: :development
98
112
  - !ruby/object:Gem::Dependency
99
- name: embulk
100
113
  requirement: !ruby/object:Gem::Requirement
101
114
  requirements:
102
115
  - - "~>"
103
116
  - !ruby/object:Gem::Version
104
117
  version: 0.8.7
118
+ name: embulk
119
+ prerelease: false
120
+ type: :development
105
121
  version_requirements: !ruby/object:Gem::Requirement
106
122
  requirements:
107
123
  - - "~>"
108
124
  - !ruby/object:Gem::Version
109
125
  version: 0.8.7
110
- prerelease: false
111
- type: :development
112
126
  - !ruby/object:Gem::Dependency
113
- name: simplecov
114
127
  requirement: !ruby/object:Gem::Requirement
115
128
  requirements:
116
129
  - - ">="
117
130
  - !ruby/object:Gem::Version
118
131
  version: '0'
132
+ name: simplecov
133
+ prerelease: false
134
+ type: :development
119
135
  version_requirements: !ruby/object:Gem::Requirement
120
136
  requirements:
121
137
  - - ">="
122
138
  - !ruby/object:Gem::Version
123
139
  version: '0'
124
- prerelease: false
125
- type: :development
126
140
  - !ruby/object:Gem::Dependency
127
- name: pry
128
141
  requirement: !ruby/object:Gem::Requirement
129
142
  requirements:
130
143
  - - ">="
131
144
  - !ruby/object:Gem::Version
132
145
  version: '0'
146
+ name: pry
147
+ prerelease: false
148
+ type: :development
133
149
  version_requirements: !ruby/object:Gem::Requirement
134
150
  requirements:
135
151
  - - ">="
136
152
  - !ruby/object:Gem::Version
137
153
  version: '0'
138
- prerelease: false
139
- type: :development
140
154
  - !ruby/object:Gem::Dependency
141
- name: codeclimate-test-reporter
142
155
  requirement: !ruby/object:Gem::Requirement
143
156
  requirements:
144
157
  - - ">="
145
158
  - !ruby/object:Gem::Version
146
159
  version: '0'
160
+ name: codeclimate-test-reporter
161
+ prerelease: false
162
+ type: :development
147
163
  version_requirements: !ruby/object:Gem::Requirement
148
164
  requirements:
149
165
  - - ">="
150
166
  - !ruby/object:Gem::Version
151
167
  version: '0'
152
- prerelease: false
153
- type: :development
154
168
  - !ruby/object:Gem::Dependency
155
- name: everyleaf-embulk_helper
156
169
  requirement: !ruby/object:Gem::Requirement
157
170
  requirements:
158
171
  - - ">="
159
172
  - !ruby/object:Gem::Version
160
173
  version: '0'
174
+ name: everyleaf-embulk_helper
175
+ prerelease: false
176
+ type: :development
161
177
  version_requirements: !ruby/object:Gem::Requirement
162
178
  requirements:
163
179
  - - ">="
164
180
  - !ruby/object:Gem::Version
165
181
  version: '0'
166
- prerelease: false
167
- type: :development
168
182
  description: Loads records from Jira.
169
183
  email:
170
184
  - k@uu59.org