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 +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +16 -2
- data/SIDEKIQ_USAGE.md +341 -0
- data/lib/oxtenderabstract/archive_processor.rb +37 -25
- data/lib/oxtenderabstract/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 29f0092ce09cc7fd85cfdea5489dbe645e1d28f99a12a2f98e57a48761bf2f13
|
4
|
+
data.tar.gz: abb88a96187a2a0e11e16efdf067368e5e4cd20504e080384789504a8a530c21
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7351bf939d3e42d07ae65f313e5cb0621732cfa5c9c4cc3d0bfa77581fd9c4b51d284844901dcd50eed4e93d1dddff6a79a8055b4f79c45c1fbbeca2efc24129
|
7
|
+
data.tar.gz: 8df04c15cd6381544847fce45624f148b1ea824812aa7717ff52a1fcf304e030cc8beb910b02bbfd294d84411397d10cd1e03fac7a90d061b3c600f41c76276c
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.4.
|
1
|
+
3.4.5
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,20 @@
|
|
1
|
-
## [0.9.
|
1
|
+
## [0.9.5] - 2025-08-06
|
2
2
|
|
3
|
-
|
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 =
|
92
|
-
|
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
|
-
|
133
|
-
|
134
|
-
|
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
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
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
|
+
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.
|
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
|