ox-tender-abstract 0.9.4 → 0.9.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
  SHA256:
3
- metadata.gz: cf4b74ab15f633cbfb3ad65d37a5fa900016e45e01f1bb300284b28609d805d7
4
- data.tar.gz: 53564ab6b3af3c30998b241ee69a899875394833bdce28f2e2ad765edd81dfca
3
+ metadata.gz: 29f0092ce09cc7fd85cfdea5489dbe645e1d28f99a12a2f98e57a48761bf2f13
4
+ data.tar.gz: abb88a96187a2a0e11e16efdf067368e5e4cd20504e080384789504a8a530c21
5
5
  SHA512:
6
- metadata.gz: d122d953f825672d6b61d551f49c08a669eaeb4ef60b55d4c718c4d9dd9ee68e99d83a026745ff77451963ed30957a0ef684bcb67300574dab9f7e18e64e22c7
7
- data.tar.gz: 7396189e5e44a5c3fa5b9e988b6ed0394072b1b7fadacfed60df6ebe36afd741685efa3f46978912028c561f4c0aa5820a85560bf5222134fab840815a88c25a
6
+ metadata.gz: 7351bf939d3e42d07ae65f313e5cb0621732cfa5c9c4cc3d0bfa77581fd9c4b51d284844901dcd50eed4e93d1dddff6a79a8055b4f79c45c1fbbeca2efc24129
7
+ data.tar.gz: 8df04c15cd6381544847fce45624f148b1ea824812aa7717ff52a1fcf304e030cc8beb910b02bbfd294d84411397d10cd1e03fac7a90d061b3c600f41c76276c
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.4
1
+ 3.4.5
data/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
- ## [0.9.4] - 2025-07-28
1
+ ## [0.9.5] - 2025-08-06
2
2
 
3
- - include_attachments
3
+ ### Fixed
4
+
5
+ - Fixed encoding compatibility error when processing API blocking messages
6
+ - Fixed missing `include_attachments` parameter in `search_tenders` and `search_tenders_with_auto_wait` methods
7
+ - Improved error handling for binary response data from API
8
+
9
+ ### Changed
10
+
11
+ - Enhanced encoding handling in `ArchiveProcessor` to safely handle UTF-8 and BINARY responses
12
+ - Restored `include_attachments` parameter to all search methods with default value `true`
13
+
14
+ ### Added
15
+
16
+ - Better error logging for encoding issues
17
+ - Safe encoding conversion using `force_encoding('UTF-8').scrub`
4
18
 
5
19
  ## [0.9.3] - 2025-07-27
6
20
 
data/SIDEKIQ_USAGE.md ADDED
@@ -0,0 +1,341 @@
1
+ # Использование OxTenderAbstract с Sidekiq
2
+
3
+ ## Обработка блокировок API в отложенных задачах
4
+
5
+ При частых запросах к API zakupki.gov.ru сервер может заблокировать загрузку архивов на 10 минут. Библиотека теперь правильно обрабатывает такие блокировки и возвращает специальные результаты.
6
+
7
+ ## Рекомендуемый Sidekiq Worker с автоматическим ожиданием
8
+
9
+ ```ruby
10
+ class TenderImportWorker
11
+ include Sidekiq::Worker
12
+
13
+ # Простая настройка - библиотека сама управляет блокировками
14
+ sidekiq_options retry: 3
15
+
16
+ def perform(region, date, subsystem_type = 'PRIZ', document_type = 'epNotificationEF2020', resume_state = nil)
17
+ # Используем новый метод с автоматическим ожиданием
18
+ result = OxTenderAbstract.search_tenders_with_auto_wait(
19
+ org_region: region,
20
+ exact_date: date,
21
+ subsystem_type: subsystem_type,
22
+ document_type: document_type,
23
+ resume_state: resume_state
24
+ )
25
+
26
+ if result.failure?
27
+ # Обрабатываем только критические ошибки
28
+ handle_failure(result, region, date, subsystem_type, document_type)
29
+ else
30
+ process_tenders(result.data[:tenders])
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def handle_failure(result, region, date, subsystem_type, document_type)
37
+ # С автоматическим ожиданием блокировки обрабатываются автоматически
38
+ # Нужно обрабатывать только реальные ошибки
39
+ logger.error "Tender import failed: #{result.error}"
40
+ raise StandardError, result.error
41
+ end
42
+
43
+ def process_tenders(tenders)
44
+ tenders.each do |tender|
45
+ save_tender_to_database(tender)
46
+ end
47
+
48
+ logger.info "Processed #{tenders.size} tenders"
49
+ end
50
+ end
51
+ ```
52
+
53
+ ## Альтернативный Worker с ручным управлением
54
+
55
+ ```ruby
56
+ class TenderImportWorkerManual
57
+ include Sidekiq::Worker
58
+
59
+ # Настраиваем повторные попытки с увеличенной задержкой для блокировок
60
+ sidekiq_options retry: 5
61
+
62
+ def perform(region, date, subsystem_type = 'PRIZ', document_type = 'epNotificationEF2020', resume_state = nil)
63
+ # Отключаем автоматическое ожидание для ручного управления
64
+ OxTenderAbstract.configure do |config|
65
+ config.auto_wait_on_block = false
66
+ end
67
+
68
+ result = OxTenderAbstract.search_tenders_with_auto_wait(
69
+ org_region: region,
70
+ exact_date: date,
71
+ subsystem_type: subsystem_type,
72
+ document_type: document_type,
73
+ resume_state: resume_state
74
+ )
75
+
76
+ if result.failure?
77
+ handle_failure(result, region, date, subsystem_type, document_type)
78
+ else
79
+ process_tenders(result.data[:tenders])
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def handle_failure(result, region, date, subsystem_type, document_type)
86
+ # Проверяем тип ошибки
87
+ if result.metadata[:error_type] == :blocked
88
+ # API заблокировал доступ на 10 минут
89
+ retry_after = result.metadata[:retry_after] || 600
90
+
91
+ logger.warn "Archive download blocked, retrying in #{retry_after} seconds"
92
+
93
+ # Перепланируем задачу через указанное время
94
+ TenderImportWorker.perform_in(
95
+ retry_after.seconds + 30, # +30 секунд для гарантии
96
+ region, date, subsystem_type, document_type
97
+ )
98
+ else
99
+ # Обычная ошибка - логируем и возможно повторяем стандартно
100
+ logger.error "Tender import failed: #{result.error}"
101
+ raise StandardError, result.error
102
+ end
103
+ end
104
+
105
+ def process_tenders(tenders)
106
+ tenders.each do |tender|
107
+ # Обработка каждого тендера
108
+ save_tender_to_database(tender)
109
+ end
110
+
111
+ logger.info "Processed #{tenders.size} tenders"
112
+ end
113
+ end
114
+ ```
115
+
116
+ ## Конфигурация автоматического ожидания
117
+
118
+ Библиотека теперь поддерживает встроенное автоматическое ожидание при блокировках:
119
+
120
+ ```ruby
121
+ # config/initializers/ox_tender_abstract.rb
122
+ OxTenderAbstract.configure do |config|
123
+ config.token = ENV['ZAKUPKI_API_TOKEN']
124
+
125
+ # Настройки автоматического ожидания
126
+ config.auto_wait_on_block = true # Автоматически ждать при блокировке (по умолчанию true)
127
+ config.block_wait_time = 610 # Время ожидания в секундах (10 мин + 10 сек)
128
+ config.max_wait_time = 900 # Максимальное время ожидания (15 мин)
129
+ end
130
+ ```
131
+
132
+ ### Режимы работы
133
+
134
+ 1. **Автоматическое ожидание** (`auto_wait_on_block = true`) - библиотека сама ждет и продолжает
135
+ 2. **Ручное управление** (`auto_wait_on_block = false`) - возвращает состояние для продолжения в Sidekiq
136
+
137
+ ## Настройка Sidekiq для обработки блокировок
138
+
139
+ ### 1. Кастомная стратегия повторов
140
+
141
+ ```ruby
142
+ # config/initializers/sidekiq.rb
143
+
144
+ # Кастомная стратегия повторов для API блокировок
145
+ class TenderRetryStrategy
146
+ def call(worker, job, queue)
147
+ # Извлекаем информацию об ошибке
148
+ exception = job['error_message']
149
+
150
+ if exception&.include?('blocked')
151
+ # Для блокировок используем фиксированную задержку
152
+ return 600 # 10 минут
153
+ else
154
+ # Стандартная экспоненциальная задержка
155
+ return (job['retry_count'] ** 4) + 15
156
+ end
157
+ end
158
+ end
159
+
160
+ Sidekiq.configure_server do |config|
161
+ config.death_handlers << lambda do |job, ex|
162
+ # Логируем окончательно проваленные задачи
163
+ Rails.logger.error "Sidekiq job #{job['class']} failed permanently: #{ex.message}"
164
+ end
165
+ end
166
+ ```
167
+
168
+ ### 2. Настройка очередей с приоритетами
169
+
170
+ ```ruby
171
+ # config/sidekiq.yml
172
+ :queues:
173
+ - [critical, 2]
174
+ - [tenders_import, 1]
175
+ - [tenders_retry, 1]
176
+ - [default, 1]
177
+ ```
178
+
179
+ ### 3. Worker с интеллектуальными повторами
180
+
181
+ ```ruby
182
+ class SmartTenderImportWorker
183
+ include Sidekiq::Worker
184
+
185
+ sidekiq_options queue: 'tenders_import', retry: 3
186
+
187
+ # Кастомная логика повторов
188
+ sidekiq_retry_in do |count, exception|
189
+ case exception.message
190
+ when /blocked/
191
+ # Для блокировок ждем 10 минут
192
+ 600
193
+ when /network error/i
194
+ # Для сетевых ошибок короткая задержка
195
+ 30 * (count + 1)
196
+ else
197
+ # Стандартная задержка
198
+ 60 * (count + 1)
199
+ end
200
+ end
201
+
202
+ def perform(params)
203
+ with_error_handling do
204
+ import_tenders(params)
205
+ end
206
+ end
207
+
208
+ private
209
+
210
+ def with_error_handling
211
+ yield
212
+ rescue => e
213
+ if e.message.include?('blocked')
214
+ # Перемещаем в специальную очередь для повторов
215
+ SmartTenderImportWorker.set(queue: 'tenders_retry')
216
+ .perform_in(610.seconds, { retry: true }.merge(params))
217
+ else
218
+ raise e
219
+ end
220
+ end
221
+ end
222
+ ```
223
+
224
+ ## Мониторинг и отладка
225
+
226
+ ### 1. Логирование блокировок
227
+
228
+ ```ruby
229
+ class TenderImportLogger
230
+ def self.log_blocked_request(region, date, retry_after)
231
+ Rails.logger.warn {
232
+ "[TENDER_BLOCKED] Region: #{region}, Date: #{date}, Retry after: #{retry_after}s"
233
+ }
234
+
235
+ # Отправка в системы мониторинга
236
+ StatsD.increment('tender_import.blocked')
237
+ StatsD.histogram('tender_import.retry_delay', retry_after)
238
+ end
239
+ end
240
+ ```
241
+
242
+ ### 2. Метрики для мониторинга
243
+
244
+ ```ruby
245
+ # В worker'е
246
+ def perform(params)
247
+ start_time = Time.current
248
+
249
+ begin
250
+ result = import_tenders(params)
251
+ StatsD.increment('tender_import.success')
252
+ StatsD.histogram('tender_import.duration', Time.current - start_time)
253
+ rescue => e
254
+ StatsD.increment('tender_import.error')
255
+ StatsD.increment("tender_import.error.#{error_type(e)}")
256
+ raise
257
+ end
258
+ end
259
+
260
+ def error_type(exception)
261
+ case exception.message
262
+ when /blocked/ then 'blocked'
263
+ when /network/ then 'network'
264
+ when /parse/ then 'parse'
265
+ else 'unknown'
266
+ end
267
+ end
268
+ ```
269
+
270
+ ## Рекомендации
271
+
272
+ 1. **Используйте разные очереди** для обычных и повторных задач
273
+ 2. **Мониторьте частоту блокировок** - если они частые, уменьшите нагрузку
274
+ 3. **Настройте алерты** на высокий процент блокировок
275
+ 4. **Кэшируйте результаты** где возможно, чтобы уменьшить количество запросов
276
+ 5. **Используйте rate limiting** на уровне приложения
277
+
278
+ ## Пример полной настройки
279
+
280
+ ```ruby
281
+ # app/workers/tender_import_worker.rb
282
+ class TenderImportWorker
283
+ include Sidekiq::Worker
284
+ include Sidekiq::Throttled::Worker
285
+
286
+ # Ограничиваем количество одновременных запросов
287
+ sidekiq_throttle(
288
+ threshold: { limit: 5, period: 1.minute },
289
+ key: ->(region, date) { "tender_import:#{region}" }
290
+ )
291
+
292
+ sidekiq_options queue: 'tenders', retry: 5
293
+
294
+ def perform(region, date, options = {})
295
+ TenderImportService.new(region, date, options).call
296
+ end
297
+ end
298
+
299
+ # app/services/tender_import_service.rb
300
+ class TenderImportService
301
+ def initialize(region, date, options = {})
302
+ @region = region
303
+ @date = date
304
+ @options = options
305
+ end
306
+
307
+ def call
308
+ result = OxTenderAbstract.search_tenders(
309
+ org_region: @region,
310
+ exact_date: @date,
311
+ subsystem_type: @options[:subsystem_type] || 'PRIZ'
312
+ )
313
+
314
+ if result.failure?
315
+ handle_error(result)
316
+ else
317
+ process_success(result)
318
+ end
319
+ end
320
+
321
+ private
322
+
323
+ def handle_error(result)
324
+ case result.metadata[:error_type]
325
+ when :blocked
326
+ schedule_retry(result.metadata[:retry_after])
327
+ when :network
328
+ raise NetworkError, result.error
329
+ else
330
+ raise StandardError, result.error
331
+ end
332
+ end
333
+
334
+ def schedule_retry(retry_after)
335
+ TenderImportWorker.perform_in(
336
+ (retry_after + 30).seconds,
337
+ @region, @date, @options.merge(retry: true)
338
+ )
339
+ end
340
+ end
341
+ ```
@@ -88,8 +88,12 @@ module OxTenderAbstract
88
88
  log_warn "Download attempt #{attempt} failed: #{last_error}"
89
89
  end
90
90
  rescue StandardError => e
91
- last_error = e.message
92
- log_error "Download attempt #{attempt} exception: #{last_error}"
91
+ last_error = begin
92
+ e.message.force_encoding('UTF-8').scrub
93
+ rescue StandardError
94
+ e.message.to_s
95
+ end
96
+ log_error "Download error details: #{e.class} - #{last_error}"
93
97
  end
94
98
 
95
99
  if attempt < MAX_RETRY_ATTEMPTS
@@ -128,34 +132,42 @@ module OxTenderAbstract
128
132
  unless response.is_a?(Net::HTTPSuccess)
129
133
  error_msg = "HTTP error: #{response.code} #{response.message}"
130
134
  if response.body && !response.body.empty?
131
- # Log first part of response body for debugging
132
- body_preview = response.body[0..500]
133
- log_error "Response body preview: #{body_preview}"
134
- error_msg += ". Response: #{body_preview[0..100]}"
135
+ # Log first part of response body for debugging - safely handle encoding
136
+ begin
137
+ body_preview = response.body.force_encoding('UTF-8').scrub[0..500]
138
+ log_error "Response body preview: #{body_preview}"
139
+ error_msg += ". Response: #{body_preview[0..100]}"
140
+ rescue StandardError => e
141
+ log_error "Response body encoding error: #{e.message}"
142
+ error_msg += '. Response body unreadable (encoding issue)'
143
+ end
135
144
  end
136
145
  return Result.failure(error_msg)
137
146
  end
138
147
 
139
- # Check for download blocking message in successful response
140
- if response.body&.include?('Скачивание архива по данной ссылке заблокировано')
141
- if OxTenderAbstract.configuration.auto_wait_on_block
142
- wait_time = OxTenderAbstract.configuration.block_wait_time
143
- log_error "Archive download blocked. Auto-waiting for #{wait_time} seconds..."
144
-
145
- # Показываем прогресс ожидания
146
- show_wait_progress(wait_time)
147
-
148
- log_info 'Wait completed, retrying download...'
149
- # Рекурсивно повторяем попытку после ожидания
150
- return download_to_memory(url)
151
- else
152
- log_error 'Archive download blocked for 10 minutes'
153
- return Result.failure(
154
- 'Archive download blocked for 10 minutes',
155
- error_type: :blocked,
156
- retry_after: 600
157
- )
148
+ # Check for download blocking message in successful response - safely handle encoding
149
+ begin
150
+ response_text = response.body&.force_encoding('UTF-8')&.scrub
151
+ if response_text&.include?('Скачивание архива по данной ссылке заблокировано')
152
+ if OxTenderAbstract.configuration.auto_wait_on_block
153
+ wait_time = OxTenderAbstract.configuration.block_wait_time
154
+ log_error "Archive download blocked. Auto-waiting for #{wait_time} seconds..."
155
+
156
+ # Показываем прогресс ожидания
157
+ show_wait_progress(wait_time)
158
+
159
+ log_info 'Wait completed, retrying download...'
160
+ # Рекурсивно повторяем попытку после ожидания
161
+ return download_to_memory(url)
162
+ else
163
+ # Возвращаем специальную ошибку блокировки для ручной обработки
164
+ return Result.failure('Archive download blocked for 10 minutes',
165
+ ArchiveBlockedError.new('Archive download blocked', 600))
166
+ end
158
167
  end
168
+ rescue StandardError => e
169
+ log_error "Encoding error when checking for blocking message: #{e.message}"
170
+ # Продолжаем обработку, так как это может быть просто архив
159
171
  end
160
172
 
161
173
  content = response.body
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OxTenderAbstract
4
- VERSION = '0.9.4'
4
+ VERSION = '0.9.5'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ox-tender-abstract
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.4
4
+ version: 0.9.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - smolev
@@ -187,6 +187,7 @@ files:
187
187
  - LICENSE
188
188
  - README.md
189
189
  - Rakefile
190
+ - SIDEKIQ_USAGE.md
190
191
  - lib/ox-tender-abstract.rb
191
192
  - lib/oxtenderabstract/archive_processor.rb
192
193
  - lib/oxtenderabstract/client.rb
@@ -222,7 +223,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
222
223
  - !ruby/object:Gem::Version
223
224
  version: '0'
224
225
  requirements: []
225
- rubygems_version: 3.6.9
226
+ rubygems_version: 3.7.1
226
227
  specification_version: 4
227
228
  summary: Ruby library for working with Russian tender system (zakupki.gov.ru) SOAP
228
229
  API