embulk-input-zendesk 0.2.6 → 0.2.7

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