embulk-input-zendesk 0.2.6 → 0.2.7

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: 8965b15e24973482e7f636c44633ace2821f9c0d
4
- data.tar.gz: 1bbcaf319cabe8f53ddfde5b2cdeb16f0c54c612
3
+ metadata.gz: 3d1102bce0d8a2464284659c4349277604022de6
4
+ data.tar.gz: 44688236a95097808b51dfaa2528c87ba3230f6f
5
5
  SHA512:
6
- metadata.gz: 79ea61da70c871ea6d1bde7789dbc0af0fce75101aa29ac8d7821b0bf73b42df87c925c40dc796a4333cda32d17546ef86c233a463b5d3eed86211cedd2d6e0b
7
- data.tar.gz: 5a767959bf7955ed241d67dc5993731046c1abfa328af9738bff8dda40bf16543aa5ec568cb5ec3fcb26912a4790baab88da09ec6f5a5782eddd8bf8b4833aa5
6
+ metadata.gz: 9ee108f8f523bbc57ef69d4f943d51fa2ad1c18db75e91d8433b5a01178bc1f037a45f43db4334f7e0ca3cb8ed350e9d89180f769340eef53e0668a4b86bd81b
7
+ data.tar.gz: f3ac93d20e468e462a622ece7c2287935c9011739064935aaeccd899b3629aebc91258ee6363d8b1ca111b918a7202484cd21c78b71f4aa412159accd56f62cc
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.2.7 - 2017-07-19
2
+ * [fixed] Ensure thread pool is shutdown [#38](https://github.com/treasure-data/embulk-input-zendesk/pull/38)
3
+ * [enhancement] Add retry for temporary error: missing required key from JSON response
4
+
1
5
  ## 0.2.6 - 2017-05-23
2
6
  * [enhancement] Enable incremental loading for ticket_metrics
3
7
 
@@ -1,7 +1,7 @@
1
1
 
2
2
  Gem::Specification.new do |spec|
3
3
  spec.name = "embulk-input-zendesk"
4
- spec.version = "0.2.6"
4
+ spec.version = "0.2.7"
5
5
  spec.authors = ["uu59", "muga", "sakama"]
6
6
  spec.summary = "Zendesk input plugin for Embulk"
7
7
  spec.description = "Loads records from Zendesk."
@@ -31,7 +31,7 @@ module Embulk
31
31
  end
32
32
  end
33
33
 
34
- def get_pool
34
+ def create_pool
35
35
  Concurrent::ThreadPoolExecutor.new(
36
36
  min_threads: 10,
37
37
  max_threads: 100,
@@ -69,36 +69,25 @@ module Embulk
69
69
  end
70
70
 
71
71
  # they have both Incremental API and non-incremental API
72
- %w(tickets users organizations).each do |target|
72
+ # 170717: `ticket_events` can use standard endpoint format now, ie. `<target>.json`
73
+ %w(tickets ticket_events users organizations).each do |target|
73
74
  define_method(target) do |partial = true, start_time = 0, &block|
74
75
  # Always use incremental_export. There is some difference between incremental_export and export.
75
76
  incremental_export("/api/v2/incremental/#{target}.json", target, start_time, [], partial, &block)
76
77
  end
77
78
  end
78
79
 
79
- # they have incremental API only
80
- %w(ticket_events).each do |target|
81
- define_method(target) do |partial = true, start_time = 0, &block|
82
- path = "/api/v2/incremental/#{target}"
83
- incremental_export(path, target, start_time, [], partial, &block)
84
- end
85
- end
86
-
87
80
  # Ticket metrics will need to be export using both the non incremental and incremental on ticket
88
81
  # We provide support by filter out ticket_metrics with created at smaller than start time
89
82
  # while passing the incremental start time to the incremental ticket/ticket_metrics export
90
- %w(ticket_metrics).each do |target|
91
- define_method(target) do |partial = true, start_time = 0, &block|
92
- path = "/api/v2/incremental/tickets.json"
93
- if partial
94
- path = "/api/v2/#{target}.json"
95
- # If partial export then we need to use the old end point. Since new end point return both ticket and
96
- # ticket metric with ticket come first so the current approach that cut off the response packet won't work
97
- # Since partial is only use for preview and guess so this should be fine
98
- export(path, target, &block)
99
- else
100
- incremental_export(path, "metric_sets", start_time, [], partial,{include: "metric_sets"}, &block)
101
- end
83
+ define_method('ticket_metrics') do |partial = true, start_time = 0, &block|
84
+ if partial
85
+ # If partial export then we need to use the old end point. Since new end point return both ticket and
86
+ # ticket metric with ticket come first so the current approach that cut off the response packet won't work
87
+ # Since partial is only use for preview and guess so this should be fine
88
+ export('/api/v2/ticket_metrics.json', 'ticket_metrics', &block)
89
+ else
90
+ incremental_export('/api/v2/incremental/tickets.json', 'metric_sets', start_time, [], partial, { include: 'metric_sets' }, &block)
102
91
  end
103
92
  end
104
93
 
@@ -139,24 +128,21 @@ module Embulk
139
128
 
140
129
  first_fetched[key].uniq { |r| r['id'] }.each do |record|
141
130
  block.call record
142
- # known_ticket_ids: collect fetched ticket IDs, to exclude in next step
143
131
  end
144
132
 
145
- pool = get_pool
146
- (2..last_page_num).each do |page|
147
- pool.post do
148
- response = request(path, per_page: per_page, page: page)
149
- fetched_records = extract_records_from_response(response, key)
150
- Embulk.logger.info "Fetched #{key} on page=#{page} >>> size: #{fetched_records.length}"
151
- fetched_records.uniq { |r| r['id'] }.each do |record|
152
- block.call record
133
+ execute_thread_pool do |pool|
134
+ (2..last_page_num).each do |page|
135
+ pool.post do
136
+ response = request(path, per_page: per_page, page: page)
137
+ fetched_records = extract_records_from_response(response, key)
138
+ Embulk.logger.info "Fetched #{key} on page=#{page} >>> size: #{fetched_records.length}"
139
+ fetched_records.uniq { |r| r['id'] }.each do |record|
140
+ block.call record
141
+ end
153
142
  end
154
143
  end
155
144
  end
156
145
 
157
- pool.shutdown
158
- pool.wait_for_termination
159
-
160
146
  nil # this is necessary different with incremental_export
161
147
  end
162
148
 
@@ -177,60 +163,55 @@ module Embulk
177
163
  end
178
164
  end
179
165
 
180
- def incremental_export(path, key, start_time = 0, known_ids = [], partial = true,query = {}, &block)
166
+ def incremental_export(path, key, start_time = 0, known_ids = [], partial = true, query = {}, &block)
167
+ query.merge!(start_time: start_time)
181
168
  if partial
182
- records = request_partial(path, query.merge({start_time: start_time})).first(5)
169
+ records = request_partial(path, query).first(5)
183
170
  records.uniq{|r| r["id"]}.each do |record|
184
171
  block.call record
185
172
  end
186
173
  return
187
174
  end
188
175
 
189
- pool = get_pool
190
- last_data = loop do
191
- start_fetching = Time.now
192
- response = request(path, query.merge({start_time: start_time}))
193
- begin
176
+ execute_thread_pool do |pool|
177
+ loop do
178
+ start_fetching = Time.now
179
+ response = request(path, query)
180
+ actual_fetched = 0
194
181
  data = JSON.parse(response.body)
195
- rescue => e
196
- raise Embulk::DataError.new(e)
197
- end
198
- actual_fetched = 0
199
- records = data[key]
200
- records.each do |record|
201
- # https://developer.zendesk.com/rest_api/docs/core/incremental_export#excluding-system-updates
202
- # "generated_timestamp" will be updated when Zendesk internal changing
203
- # "updated_at" will be updated when ticket data was changed
204
- # start_time for query parameter will be processed on Zendesk with generated_timestamp,
205
- # but it was calculated by record' updated_at time.
206
- # So the doesn't changed record from previous import would be appear by Zendesk internal changes.
207
- # We ignore record that has updated_at <= start_time
208
- if start_time && record["generated_timestamp"] && record["updated_at"]
209
- updated_at = Time.parse(record["updated_at"])
210
- next if updated_at <= Time.at(start_time)
211
- end
182
+ # no key found in response occasionally => retry
183
+ raise TempError, "No '#{key}' found in JSON response" unless data.key? key
184
+ data[key].each do |record|
185
+ # https://developer.zendesk.com/rest_api/docs/core/incremental_export#excluding-system-updates
186
+ # "generated_timestamp" will be updated when Zendesk internal changing
187
+ # "updated_at" will be updated when ticket data was changed
188
+ # start_time for query parameter will be processed on Zendesk with generated_timestamp,
189
+ # but it was calculated by record' updated_at time.
190
+ # So the doesn't changed record from previous import would be appear by Zendesk internal changes.
191
+ # We ignore record that has updated_at <= start_time
192
+ if start_time && record["generated_timestamp"] && record["updated_at"]
193
+ updated_at = Time.parse(record["updated_at"])
194
+ next if updated_at <= Time.at(start_time)
195
+ end
212
196
 
213
- # de-duplicated records.
214
- # https://developer.zendesk.com/rest_api/docs/core/incremental_export#usage-notes
215
- # https://github.com/zendesk/zendesk_api_client_rb/issues/251
216
- next if known_ids.include?(record["id"])
197
+ # de-duplicated records.
198
+ # https://developer.zendesk.com/rest_api/docs/core/incremental_export#usage-notes
199
+ # https://github.com/zendesk/zendesk_api_client_rb/issues/251
200
+ next if known_ids.include?(record["id"])
217
201
 
218
- known_ids << record["id"]
219
- pool.post { yield(record) }
220
- actual_fetched += 1
221
- end
222
- Embulk.logger.info "Fetched #{actual_fetched} records from start_time:#{start_time} (#{Time.at(start_time)}) within #{Time.now.to_i - start_fetching.to_i} seconds"
223
- start_time = data["end_time"]
202
+ known_ids << record["id"]
203
+ pool.post { block.call record }
204
+ actual_fetched += 1
205
+ end
206
+ Embulk.logger.info "Fetched #{actual_fetched} records from start_time:#{start_time} (#{Time.at(start_time)}) within #{Time.now.to_i - start_fetching.to_i} seconds"
207
+ start_time = data["end_time"]
224
208
 
225
- # NOTE: If count is less than 1000, then stop paginating.
226
- # Otherwise, use the next_page URL to get the next page of results.
227
- # https://developer.zendesk.com/rest_api/docs/core/incremental_export#pagination
228
- break data if data["count"] < 1000
209
+ # NOTE: If count is less than 1000, then stop paginating.
210
+ # Otherwise, use the next_page URL to get the next page of results.
211
+ # https://developer.zendesk.com/rest_api/docs/core/incremental_export#pagination
212
+ break data if data["count"] < 1000
213
+ end
229
214
  end
230
-
231
- pool.shutdown
232
- pool.wait_for_termination
233
- last_data
234
215
  end
235
216
 
236
217
  def extract_records_from_response(response, key)
@@ -387,6 +368,27 @@ module Embulk
387
368
  end
388
369
  end
389
370
 
371
+ def execute_thread_pool(&block)
372
+ pool = create_pool
373
+ pr = PerfectRetry.new do |config|
374
+ config.limit = @config[:retry_limit]
375
+ config.logger = Embulk.logger
376
+ config.log_level = nil
377
+ config.rescues = [TempError]
378
+ config.sleep = lambda{|n| @config[:retry_initial_wait_sec]* (2 ** (n-1)) }
379
+ end
380
+ pr.with_retry { block.call(pool) }
381
+ rescue => e
382
+ raise Embulk::DataError.new(e)
383
+ ensure
384
+ Embulk.logger.info 'ThreadPool shutting down...'
385
+ pool.shutdown
386
+ pool.wait_for_termination
387
+ Embulk.logger.info "ThreadPool shutdown? #{pool.shutdown?}"
388
+ end
389
+ end
390
+
391
+ class TempError < StandardError
390
392
  end
391
393
  end
392
394
  end
@@ -572,6 +572,63 @@ module Embulk
572
572
  end
573
573
  end
574
574
 
575
+ sub_test_case "ensure thread pool is shutdown with/without errors, retry for TempError" do
576
+ def client
577
+ @client ||= Client.new(login_url: login_url, auth_method: "oauth", access_token: access_token, retry_limit: 1, retry_initial_wait_sec: 0)
578
+ end
579
+
580
+ setup do
581
+ stub(Embulk).logger { Logger.new(File::NULL) }
582
+ @httpclient = client.httpclient
583
+ stub(client).httpclient { @httpclient }
584
+ @pool = Concurrent::ThreadPoolExecutor.new
585
+ stub(client).create_pool { @pool }
586
+ end
587
+ test "should shutdown pool - without error" do
588
+ @httpclient.test_loopback_http_response << [
589
+ "HTTP/1.1 200",
590
+ "Content-Type: application/json",
591
+ "",
592
+ {
593
+ ticket_fields: [{ id: 1 }],
594
+ count: 1
595
+ }.to_json
596
+ ].join("\r\n")
597
+ handler = proc { }
598
+ client.ticket_fields(false, &handler)
599
+ assert_equal(true, @pool.shutdown?)
600
+ end
601
+
602
+ test "should shutdown pool - with TempError (retry)" do
603
+ response = [
604
+ "HTTP/1.1 200",
605
+ "Content-Type: application/json",
606
+ "",
607
+ { }.to_json # no required key: `tickets`, raise TempError
608
+ ].join("\r\n")
609
+ @httpclient.test_loopback_http_response << response
610
+ @httpclient.test_loopback_http_response << response # retry 1
611
+ assert_raise(TempError) do
612
+ client.tickets(false)
613
+ end
614
+ assert_equal(true, @pool.shutdown?)
615
+ end
616
+
617
+ test "should shutdown pool - with DataError (no retry)" do
618
+ response = [
619
+ "HTTP/1.1 400", # unhandled error, wrapped in DataError
620
+ "Content-Type: application/json",
621
+ "",
622
+ { }.to_json
623
+ ].join("\r\n")
624
+ @httpclient.test_loopback_http_response << response
625
+ assert_raise(DataError) do
626
+ client.tickets(false)
627
+ end
628
+ assert_equal(true, @pool.shutdown?)
629
+ end
630
+ end
631
+
575
632
  def login_url
576
633
  "http://example.com"
577
634
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: embulk-input-zendesk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - uu59
@@ -10,190 +10,190 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2017-05-23 00:00:00.000000000 Z
13
+ date: 2017-07-27 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
+ name: perfect_retry
17
+ version_requirements: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: '0.5'
16
22
  requirement: !ruby/object:Gem::Requirement
17
23
  requirements:
18
24
  - - "~>"
19
25
  - !ruby/object:Gem::Version
20
26
  version: '0.5'
21
- name: perfect_retry
22
27
  prerelease: false
23
28
  type: :runtime
29
+ - !ruby/object:Gem::Dependency
30
+ name: httpclient
24
31
  version_requirements: !ruby/object:Gem::Requirement
25
32
  requirements:
26
- - - "~>"
33
+ - - ">="
27
34
  - !ruby/object:Gem::Version
28
- version: '0.5'
29
- - !ruby/object:Gem::Dependency
35
+ version: '0'
30
36
  requirement: !ruby/object:Gem::Requirement
31
37
  requirements:
32
38
  - - ">="
33
39
  - !ruby/object:Gem::Version
34
40
  version: '0'
35
- name: httpclient
36
41
  prerelease: false
37
42
  type: :runtime
43
+ - !ruby/object:Gem::Dependency
44
+ name: concurrent-ruby
38
45
  version_requirements: !ruby/object:Gem::Requirement
39
46
  requirements:
40
47
  - - ">="
41
48
  - !ruby/object:Gem::Version
42
49
  version: '0'
43
- - !ruby/object:Gem::Dependency
44
50
  requirement: !ruby/object:Gem::Requirement
45
51
  requirements:
46
52
  - - ">="
47
53
  - !ruby/object:Gem::Version
48
54
  version: '0'
49
- name: concurrent-ruby
50
55
  prerelease: false
51
56
  type: :runtime
57
+ - !ruby/object:Gem::Dependency
58
+ name: embulk
52
59
  version_requirements: !ruby/object:Gem::Requirement
53
60
  requirements:
54
- - - ">="
61
+ - - "~>"
55
62
  - !ruby/object:Gem::Version
56
- version: '0'
57
- - !ruby/object:Gem::Dependency
63
+ version: 0.8.1
58
64
  requirement: !ruby/object:Gem::Requirement
59
65
  requirements:
60
66
  - - "~>"
61
67
  - !ruby/object:Gem::Version
62
68
  version: 0.8.1
63
- name: embulk
64
69
  prerelease: false
65
70
  type: :development
71
+ - !ruby/object:Gem::Dependency
72
+ name: bundler
66
73
  version_requirements: !ruby/object:Gem::Requirement
67
74
  requirements:
68
75
  - - "~>"
69
76
  - !ruby/object:Gem::Version
70
- version: 0.8.1
71
- - !ruby/object:Gem::Dependency
77
+ version: '1.0'
72
78
  requirement: !ruby/object:Gem::Requirement
73
79
  requirements:
74
80
  - - "~>"
75
81
  - !ruby/object:Gem::Version
76
82
  version: '1.0'
77
- name: bundler
78
83
  prerelease: false
79
84
  type: :development
85
+ - !ruby/object:Gem::Dependency
86
+ name: rake
80
87
  version_requirements: !ruby/object:Gem::Requirement
81
88
  requirements:
82
- - - "~>"
89
+ - - ">="
83
90
  - !ruby/object:Gem::Version
84
- version: '1.0'
85
- - !ruby/object:Gem::Dependency
91
+ version: '10.0'
86
92
  requirement: !ruby/object:Gem::Requirement
87
93
  requirements:
88
94
  - - ">="
89
95
  - !ruby/object:Gem::Version
90
96
  version: '10.0'
91
- name: rake
92
97
  prerelease: false
93
98
  type: :development
99
+ - !ruby/object:Gem::Dependency
100
+ name: pry
94
101
  version_requirements: !ruby/object:Gem::Requirement
95
102
  requirements:
96
103
  - - ">="
97
104
  - !ruby/object:Gem::Version
98
- version: '10.0'
99
- - !ruby/object:Gem::Dependency
105
+ version: '0'
100
106
  requirement: !ruby/object:Gem::Requirement
101
107
  requirements:
102
108
  - - ">="
103
109
  - !ruby/object:Gem::Version
104
110
  version: '0'
105
- name: pry
106
111
  prerelease: false
107
112
  type: :development
113
+ - !ruby/object:Gem::Dependency
114
+ name: test-unit
108
115
  version_requirements: !ruby/object:Gem::Requirement
109
116
  requirements:
110
- - - ">="
117
+ - - "~>"
111
118
  - !ruby/object:Gem::Version
112
- version: '0'
113
- - !ruby/object:Gem::Dependency
119
+ version: 3.1.5
114
120
  requirement: !ruby/object:Gem::Requirement
115
121
  requirements:
116
122
  - - "~>"
117
123
  - !ruby/object:Gem::Version
118
124
  version: 3.1.5
119
- name: test-unit
120
125
  prerelease: false
121
126
  type: :development
127
+ - !ruby/object:Gem::Dependency
128
+ name: test-unit-rr
122
129
  version_requirements: !ruby/object:Gem::Requirement
123
130
  requirements:
124
- - - "~>"
131
+ - - ">="
125
132
  - !ruby/object:Gem::Version
126
- version: 3.1.5
127
- - !ruby/object:Gem::Dependency
133
+ version: '0'
128
134
  requirement: !ruby/object:Gem::Requirement
129
135
  requirements:
130
136
  - - ">="
131
137
  - !ruby/object:Gem::Version
132
138
  version: '0'
133
- name: test-unit-rr
134
139
  prerelease: false
135
140
  type: :development
141
+ - !ruby/object:Gem::Dependency
142
+ name: rr
136
143
  version_requirements: !ruby/object:Gem::Requirement
137
144
  requirements:
138
- - - ">="
145
+ - - "~>"
139
146
  - !ruby/object:Gem::Version
140
- version: '0'
141
- - !ruby/object:Gem::Dependency
147
+ version: 1.1.2
142
148
  requirement: !ruby/object:Gem::Requirement
143
149
  requirements:
144
150
  - - "~>"
145
151
  - !ruby/object:Gem::Version
146
152
  version: 1.1.2
147
- name: rr
148
153
  prerelease: false
149
154
  type: :development
155
+ - !ruby/object:Gem::Dependency
156
+ name: simplecov
150
157
  version_requirements: !ruby/object:Gem::Requirement
151
158
  requirements:
152
- - - "~>"
159
+ - - ">="
153
160
  - !ruby/object:Gem::Version
154
- version: 1.1.2
155
- - !ruby/object:Gem::Dependency
161
+ version: '0'
156
162
  requirement: !ruby/object:Gem::Requirement
157
163
  requirements:
158
164
  - - ">="
159
165
  - !ruby/object:Gem::Version
160
166
  version: '0'
161
- name: simplecov
162
167
  prerelease: false
163
168
  type: :development
169
+ - !ruby/object:Gem::Dependency
170
+ name: gem_release_helper
164
171
  version_requirements: !ruby/object:Gem::Requirement
165
172
  requirements:
166
- - - ">="
173
+ - - "~>"
167
174
  - !ruby/object:Gem::Version
168
- version: '0'
169
- - !ruby/object:Gem::Dependency
175
+ version: '1.0'
170
176
  requirement: !ruby/object:Gem::Requirement
171
177
  requirements:
172
178
  - - "~>"
173
179
  - !ruby/object:Gem::Version
174
180
  version: '1.0'
175
- name: gem_release_helper
176
181
  prerelease: false
177
182
  type: :development
183
+ - !ruby/object:Gem::Dependency
184
+ name: codeclimate-test-reporter
178
185
  version_requirements: !ruby/object:Gem::Requirement
179
186
  requirements:
180
187
  - - "~>"
181
188
  - !ruby/object:Gem::Version
182
- version: '1.0'
183
- - !ruby/object:Gem::Dependency
189
+ version: '0.6'
184
190
  requirement: !ruby/object:Gem::Requirement
185
191
  requirements:
186
192
  - - "~>"
187
193
  - !ruby/object:Gem::Version
188
194
  version: '0.6'
189
- name: codeclimate-test-reporter
190
195
  prerelease: false
191
196
  type: :development
192
- version_requirements: !ruby/object:Gem::Requirement
193
- requirements:
194
- - - "~>"
195
- - !ruby/object:Gem::Version
196
- version: '0.6'
197
197
  description: Loads records from Zendesk.
198
198
  email:
199
199
  - k@uu59.org