embulk-input-jira 0.2.4 → 0.2.5

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
  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