io-complyance-unify-sdk 3.0.0
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 +7 -0
- data/CHANGELOG.md +26 -0
- data/README.md +595 -0
- data/lib/complyance/circuit_breaker.rb +99 -0
- data/lib/complyance/persistent_queue_manager.rb +474 -0
- data/lib/complyance/retry_strategy.rb +198 -0
- data/lib/complyance_sdk/config/retry_config.rb +127 -0
- data/lib/complyance_sdk/config/sdk_config.rb +212 -0
- data/lib/complyance_sdk/exceptions/circuit_breaker_open_error.rb +14 -0
- data/lib/complyance_sdk/exceptions/sdk_exception.rb +93 -0
- data/lib/complyance_sdk/generators/config_generator.rb +67 -0
- data/lib/complyance_sdk/generators/install_generator.rb +22 -0
- data/lib/complyance_sdk/generators/templates/complyance_initializer.rb +36 -0
- data/lib/complyance_sdk/http/authentication_middleware.rb +43 -0
- data/lib/complyance_sdk/http/client.rb +223 -0
- data/lib/complyance_sdk/http/logging_middleware.rb +153 -0
- data/lib/complyance_sdk/jobs/base_job.rb +63 -0
- data/lib/complyance_sdk/jobs/process_document_job.rb +92 -0
- data/lib/complyance_sdk/jobs/sidekiq_job.rb +165 -0
- data/lib/complyance_sdk/middleware/rack_middleware.rb +39 -0
- data/lib/complyance_sdk/models/country.rb +205 -0
- data/lib/complyance_sdk/models/country_policy_registry.rb +159 -0
- data/lib/complyance_sdk/models/document_type.rb +52 -0
- data/lib/complyance_sdk/models/environment.rb +144 -0
- data/lib/complyance_sdk/models/logical_doc_type.rb +228 -0
- data/lib/complyance_sdk/models/mode.rb +47 -0
- data/lib/complyance_sdk/models/operation.rb +47 -0
- data/lib/complyance_sdk/models/policy_result.rb +145 -0
- data/lib/complyance_sdk/models/purpose.rb +52 -0
- data/lib/complyance_sdk/models/source.rb +104 -0
- data/lib/complyance_sdk/models/source_ref.rb +130 -0
- data/lib/complyance_sdk/models/unify_request.rb +208 -0
- data/lib/complyance_sdk/models/unify_response.rb +198 -0
- data/lib/complyance_sdk/queue/persistent_queue_manager.rb +609 -0
- data/lib/complyance_sdk/railtie.rb +29 -0
- data/lib/complyance_sdk/retry/circuit_breaker.rb +159 -0
- data/lib/complyance_sdk/retry/retry_manager.rb +108 -0
- data/lib/complyance_sdk/retry/retry_strategy.rb +225 -0
- data/lib/complyance_sdk/version.rb +5 -0
- data/lib/complyance_sdk.rb +935 -0
- metadata +322 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
3
|
+
module Complyance
|
|
4
|
+
# Circuit Breaker implementation for handling service failures
|
|
5
|
+
class CircuitBreaker
|
|
6
|
+
STATE_CLOSED = 'CLOSED'
|
|
7
|
+
STATE_OPEN = 'OPEN'
|
|
8
|
+
STATE_HALF_OPEN = 'HALF_OPEN'
|
|
9
|
+
|
|
10
|
+
attr_reader :state, :failure_count, :last_failure_time
|
|
11
|
+
|
|
12
|
+
# Initialize circuit breaker
|
|
13
|
+
# @param config [Hash] Circuit breaker configuration
|
|
14
|
+
# @option config [Integer] :failure_threshold Number of failures before opening circuit
|
|
15
|
+
# @option config [Integer] :reset_timeout Timeout in seconds before attempting reset
|
|
16
|
+
def initialize(config = {})
|
|
17
|
+
@state = STATE_CLOSED
|
|
18
|
+
@failure_count = 0
|
|
19
|
+
@last_failure_time = 0
|
|
20
|
+
@failure_threshold = config[:failure_threshold] || 3
|
|
21
|
+
@reset_timeout = config[:reset_timeout] || 60
|
|
22
|
+
@logger = Logger.new(STDOUT)
|
|
23
|
+
@logger.level = Logger::INFO
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Execute a function with circuit breaker protection
|
|
27
|
+
# @param operation [Proc] Function to execute
|
|
28
|
+
# @return [Object] Result of operation
|
|
29
|
+
# @raise [RuntimeError] If circuit is open
|
|
30
|
+
def execute(&operation)
|
|
31
|
+
check_state
|
|
32
|
+
|
|
33
|
+
begin
|
|
34
|
+
result = operation.call
|
|
35
|
+
record_success
|
|
36
|
+
result
|
|
37
|
+
rescue => e
|
|
38
|
+
record_failure
|
|
39
|
+
raise e
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if circuit breaker is currently open
|
|
44
|
+
# @return [Boolean] True if open
|
|
45
|
+
def open?
|
|
46
|
+
@state == STATE_OPEN
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Check if circuit breaker should allow operation
|
|
52
|
+
# @raise [RuntimeError] If circuit is open
|
|
53
|
+
def check_state
|
|
54
|
+
if @state == STATE_OPEN
|
|
55
|
+
remaining_time = get_remaining_timeout
|
|
56
|
+
if remaining_time > 0
|
|
57
|
+
@logger.debug "Circuit breaker is OPEN - #{remaining_time} seconds remaining"
|
|
58
|
+
raise "Circuit breaker is open - #{remaining_time} seconds remaining"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Reset timeout expired, move to half-open
|
|
62
|
+
@state = STATE_HALF_OPEN
|
|
63
|
+
@logger.info "Circuit breaker state changed to HALF-OPEN"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Record a successful operation
|
|
68
|
+
def record_success
|
|
69
|
+
if @state == STATE_HALF_OPEN
|
|
70
|
+
@state = STATE_CLOSED
|
|
71
|
+
@failure_count = 0
|
|
72
|
+
@last_failure_time = 0
|
|
73
|
+
@logger.info "Circuit breaker reset - state changed to CLOSED"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Record a failed operation
|
|
78
|
+
def record_failure
|
|
79
|
+
@failure_count += 1
|
|
80
|
+
@last_failure_time = Time.now.to_i
|
|
81
|
+
|
|
82
|
+
if @state == STATE_HALF_OPEN ||
|
|
83
|
+
(@state == STATE_CLOSED && @failure_count >= @failure_threshold)
|
|
84
|
+
@state = STATE_OPEN
|
|
85
|
+
@logger.warn "Circuit breaker opened after #{@failure_count} failures"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Get remaining time before circuit breaker resets
|
|
90
|
+
# @return [Integer] Seconds remaining, 0 if timeout expired
|
|
91
|
+
def get_remaining_timeout
|
|
92
|
+
return 0 if @last_failure_time == 0
|
|
93
|
+
|
|
94
|
+
elapsed = Time.now.to_i - @last_failure_time
|
|
95
|
+
remaining = @reset_timeout - elapsed
|
|
96
|
+
[0, remaining].max
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'monitor'
|
|
5
|
+
require_relative 'circuit_breaker'
|
|
6
|
+
require_relative 'gets_unify_sdk'
|
|
7
|
+
|
|
8
|
+
module Complyance
|
|
9
|
+
# Manages persistent queue for document submissions
|
|
10
|
+
class PersistentQueueManager
|
|
11
|
+
QUEUE_DIR = 'complyance-queue'
|
|
12
|
+
PENDING_DIR = 'pending'
|
|
13
|
+
PROCESSING_DIR = 'processing'
|
|
14
|
+
FAILED_DIR = 'failed'
|
|
15
|
+
SUCCESS_DIR = 'success'
|
|
16
|
+
|
|
17
|
+
attr_reader :is_running
|
|
18
|
+
|
|
19
|
+
# Initialize queue manager
|
|
20
|
+
# @param api_key [String] API key for authentication
|
|
21
|
+
# @param local [Boolean] Whether to use local environment
|
|
22
|
+
# @param circuit_breaker [CircuitBreaker, nil] Optional shared circuit breaker
|
|
23
|
+
def initialize(api_key, local, circuit_breaker = nil)
|
|
24
|
+
@api_key = api_key
|
|
25
|
+
@local = local
|
|
26
|
+
@queue_base_path = queue_base_path
|
|
27
|
+
@logger = Logger.new(STDOUT)
|
|
28
|
+
@logger.level = Logger::INFO
|
|
29
|
+
@processing_lock = Monitor.new
|
|
30
|
+
@is_running = false
|
|
31
|
+
|
|
32
|
+
# Initialize circuit breaker with 3 failure threshold and 1 minute timeout
|
|
33
|
+
@circuit_breaker = circuit_breaker || CircuitBreaker.new(
|
|
34
|
+
failure_threshold: 3,
|
|
35
|
+
reset_timeout: 60
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
initialize_queue_directories
|
|
39
|
+
@logger.info "PersistentQueueManager initialized with queue directory: #{@queue_base_path}"
|
|
40
|
+
|
|
41
|
+
# Automatically start processing and retry any existing failed submissions
|
|
42
|
+
start_processing
|
|
43
|
+
retry_failed_submissions
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get base path for queue directories
|
|
47
|
+
# @return [String] Base path
|
|
48
|
+
def queue_base_path
|
|
49
|
+
File.join(ENV['HOME'] || ENV['USERPROFILE'], QUEUE_DIR)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Initialize queue directory structure
|
|
53
|
+
def initialize_queue_directories
|
|
54
|
+
[PENDING_DIR, PROCESSING_DIR, FAILED_DIR, SUCCESS_DIR].each do |dir|
|
|
55
|
+
path = File.join(@queue_base_path, dir)
|
|
56
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
|
57
|
+
end
|
|
58
|
+
@logger.debug "Queue directories initialized"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Enqueue a submission for processing
|
|
62
|
+
# @param submission [PayloadSubmission] Submission to enqueue
|
|
63
|
+
def enqueue(submission)
|
|
64
|
+
begin
|
|
65
|
+
file_name = generate_file_name(submission)
|
|
66
|
+
file_path = File.join(@queue_base_path, PENDING_DIR, file_name)
|
|
67
|
+
|
|
68
|
+
# Check if file already exists (same document ID)
|
|
69
|
+
if File.exist?(file_path)
|
|
70
|
+
@logger.info "Document already exists in queue: #{file_name}. Skipping duplicate submission."
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Parse the UnifyRequest JSON string
|
|
75
|
+
json_payload = submission.payload
|
|
76
|
+
@logger.debug "Queue: Received payload with length: #{json_payload.bytesize} characters"
|
|
77
|
+
|
|
78
|
+
# Verify the payload is not empty
|
|
79
|
+
if json_payload.strip.empty? || json_payload == '{}'
|
|
80
|
+
@logger.error "🔥 QUEUE: ERROR - Received empty or invalid payload: '#{json_payload}'"
|
|
81
|
+
raise "Cannot enqueue empty payload"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Parse the UnifyRequest JSON string to a proper JSON object
|
|
85
|
+
unify_request_map = JSON.parse(json_payload)
|
|
86
|
+
|
|
87
|
+
# Create submission record
|
|
88
|
+
record = {
|
|
89
|
+
payload: unify_request_map,
|
|
90
|
+
source_id: "#{submission.source.name}:#{submission.source.version}",
|
|
91
|
+
country: submission.country.to_s,
|
|
92
|
+
document_type: submission.document_type.to_s,
|
|
93
|
+
enqueued_at: Time.now.iso8601,
|
|
94
|
+
timestamp: (Time.now.to_f * 1000).to_i # Convert to milliseconds for consistency
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Write to file
|
|
98
|
+
File.write(file_path, JSON.pretty_generate(record))
|
|
99
|
+
|
|
100
|
+
@logger.info "Enqueued submission: #{file_name} for source: #{record[:source_id]}, country: #{record[:country]}"
|
|
101
|
+
|
|
102
|
+
# Start processing if not already running
|
|
103
|
+
start_processing
|
|
104
|
+
|
|
105
|
+
rescue => e
|
|
106
|
+
@logger.error "Failed to enqueue submission to persistent storage: #{e.message}"
|
|
107
|
+
raise "Failed to persist submission: #{e.message}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Generate unique filename for submission
|
|
112
|
+
# @param submission [PayloadSubmission] Submission to generate filename for
|
|
113
|
+
# @return [String] Generated filename
|
|
114
|
+
def generate_file_name(submission)
|
|
115
|
+
# Extract document ID from payload
|
|
116
|
+
document_id = extract_document_id(submission.payload)
|
|
117
|
+
|
|
118
|
+
# Generate filename using source and document ID for unique reference
|
|
119
|
+
source_id = "#{submission.source.name}:#{submission.source.version}".gsub(/[^\w-]/, '_')
|
|
120
|
+
country = submission.country.to_s
|
|
121
|
+
|
|
122
|
+
format("%s_%s_%s_%s.json",
|
|
123
|
+
source_id,
|
|
124
|
+
document_id,
|
|
125
|
+
country,
|
|
126
|
+
submission.document_type.to_s
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Extract document ID from payload JSON
|
|
131
|
+
# @param payload [String] JSON payload
|
|
132
|
+
# @return [String] Extracted document ID
|
|
133
|
+
def extract_document_id(payload)
|
|
134
|
+
data = JSON.parse(payload)
|
|
135
|
+
if data.dig('payload', 'invoice_data', 'invoice_number')
|
|
136
|
+
data['payload']['invoice_data']['invoice_number']
|
|
137
|
+
else
|
|
138
|
+
"doc_#{Time.now.to_i}"
|
|
139
|
+
end
|
|
140
|
+
rescue => e
|
|
141
|
+
@logger.warn "Failed to extract document ID from payload: #{e.message}"
|
|
142
|
+
"doc_#{Time.now.to_i}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Start processing queue items
|
|
146
|
+
def start_processing
|
|
147
|
+
unless @is_running
|
|
148
|
+
@is_running = true
|
|
149
|
+
@logger.info "Started persistent queue processing"
|
|
150
|
+
|
|
151
|
+
# Start background processing in a new thread
|
|
152
|
+
@processing_thread = Thread.new do
|
|
153
|
+
while @is_running
|
|
154
|
+
process_pending_submissions
|
|
155
|
+
sleep(0.5) # 500ms delay
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Process pending submissions immediately
|
|
162
|
+
def process_pending_submissions_now
|
|
163
|
+
@logger.info "Manually triggering processing of pending submissions"
|
|
164
|
+
|
|
165
|
+
# Check circuit breaker state before manual processing
|
|
166
|
+
if @circuit_breaker.open?
|
|
167
|
+
current_time = Time.now.to_i
|
|
168
|
+
time_since_last_failure = current_time - @circuit_breaker.last_failure_time
|
|
169
|
+
|
|
170
|
+
if time_since_last_failure < 60 # 1 minute
|
|
171
|
+
remaining_time = 60 - time_since_last_failure
|
|
172
|
+
@logger.info "🚫 Circuit breaker is OPEN - remaining time: #{remaining_time}s. Manual processing skipped."
|
|
173
|
+
return
|
|
174
|
+
else
|
|
175
|
+
@logger.info "✅ Circuit breaker timeout expired (#{time_since_last_failure}s) - proceeding with manual processing"
|
|
176
|
+
end
|
|
177
|
+
else
|
|
178
|
+
@logger.info "✅ Circuit breaker is CLOSED - proceeding with manual processing"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
process_pending_submissions
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Stop processing queue items
|
|
185
|
+
def stop_processing
|
|
186
|
+
@is_running = false
|
|
187
|
+
@processing_thread&.join
|
|
188
|
+
@logger.info "Stopped persistent queue processing"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Process pending submissions in queue
|
|
192
|
+
def process_pending_submissions
|
|
193
|
+
return unless @is_running
|
|
194
|
+
|
|
195
|
+
@processing_lock.synchronize do
|
|
196
|
+
begin
|
|
197
|
+
pending_dir = File.join(@queue_base_path, PENDING_DIR)
|
|
198
|
+
files = Dir[File.join(pending_dir, '*.json')]
|
|
199
|
+
|
|
200
|
+
if files.empty?
|
|
201
|
+
@logger.debug "No pending submissions to process"
|
|
202
|
+
return
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
@logger.debug "Found #{files.size} pending submissions in queue"
|
|
206
|
+
|
|
207
|
+
# Check circuit breaker state before attempting to process
|
|
208
|
+
if @circuit_breaker.open?
|
|
209
|
+
current_time = Time.now.to_i
|
|
210
|
+
time_since_last_failure = current_time - @circuit_breaker.last_failure_time
|
|
211
|
+
|
|
212
|
+
if time_since_last_failure < 60 # 1 minute
|
|
213
|
+
remaining_time = 60 - time_since_last_failure
|
|
214
|
+
@logger.debug "Circuit breaker is OPEN - #{remaining_time} seconds remaining. Queue has #{files.size} items waiting."
|
|
215
|
+
return
|
|
216
|
+
else
|
|
217
|
+
@logger.debug "Circuit breaker timeout expired - attempting to process #{files.size} queued items"
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
files.each do |file_path|
|
|
222
|
+
if File.exist?(file_path)
|
|
223
|
+
begin
|
|
224
|
+
process_submission_file(file_path)
|
|
225
|
+
rescue => e
|
|
226
|
+
@logger.error "Failed to process queued submission #{file_path}: #{e.message}"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
rescue => e
|
|
232
|
+
@logger.error "Error processing pending submissions: #{e.message}"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Process a single submission file
|
|
238
|
+
# @param file_path [String] Path to submission file
|
|
239
|
+
def process_submission_file(file_path)
|
|
240
|
+
begin
|
|
241
|
+
# Read submission record
|
|
242
|
+
record = JSON.parse(File.read(file_path))
|
|
243
|
+
file_name = File.basename(file_path)
|
|
244
|
+
processing_path = File.join(@queue_base_path, PROCESSING_DIR, file_name)
|
|
245
|
+
|
|
246
|
+
# Move to processing directory
|
|
247
|
+
FileUtils.mv(file_path, processing_path)
|
|
248
|
+
|
|
249
|
+
@logger.debug "Processing submission: #{file_name} for source: #{record['source_id']}"
|
|
250
|
+
|
|
251
|
+
begin
|
|
252
|
+
# Convert stored payload back to UnifyRequest
|
|
253
|
+
unify_request = UnifyRequest.new(record['payload'])
|
|
254
|
+
|
|
255
|
+
# Use the SDK's pushToUnify method with circuit breaker
|
|
256
|
+
response = @circuit_breaker.execute do
|
|
257
|
+
GETSUnifySDK.push_to_unify(unify_request)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Check for success
|
|
261
|
+
is_success = false
|
|
262
|
+
if response
|
|
263
|
+
is_success = response.success?
|
|
264
|
+
|
|
265
|
+
# Check submission status
|
|
266
|
+
if response.data&.submission
|
|
267
|
+
submission = response.data.submission
|
|
268
|
+
is_success ||= submission.accepted? ||
|
|
269
|
+
submission.status.to_s.downcase == 'accepted'
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Check document status
|
|
273
|
+
if response.data&.document
|
|
274
|
+
document = response.data.document
|
|
275
|
+
is_success ||= document.status.to_s.downcase == 'success'
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
if is_success
|
|
280
|
+
@logger.info "Queue: SUCCESS - Removing file from queue: #{file_name}"
|
|
281
|
+
File.delete(processing_path)
|
|
282
|
+
return
|
|
283
|
+
else
|
|
284
|
+
status = response ? response.status : 'null'
|
|
285
|
+
@logger.warn "Queue: NON-SUCCESS - Moving to failed directory. Status: '#{status}'"
|
|
286
|
+
raise "API returned non-success status: #{status}"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
rescue => e
|
|
290
|
+
@logger.error "Failed to send queued submission via pushToUnify: #{e.message}"
|
|
291
|
+
|
|
292
|
+
failed_path = File.join(@queue_base_path, FAILED_DIR, file_name)
|
|
293
|
+
|
|
294
|
+
if File.exist?(failed_path)
|
|
295
|
+
File.delete(processing_path)
|
|
296
|
+
else
|
|
297
|
+
FileUtils.mv(processing_path, failed_path)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
raise e
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
rescue => e
|
|
304
|
+
@logger.warn "Failed to process submission: #{File.basename(file_path)} - Error: #{e.message}"
|
|
305
|
+
|
|
306
|
+
file_name = File.basename(file_path)
|
|
307
|
+
processing_path = File.join(@queue_base_path, PROCESSING_DIR, file_name)
|
|
308
|
+
failed_path = File.join(@queue_base_path, FAILED_DIR, file_name)
|
|
309
|
+
|
|
310
|
+
# Move to failed directory
|
|
311
|
+
if File.exist?(processing_path)
|
|
312
|
+
if File.exist?(failed_path)
|
|
313
|
+
File.delete(processing_path)
|
|
314
|
+
else
|
|
315
|
+
FileUtils.mv(processing_path, failed_path)
|
|
316
|
+
end
|
|
317
|
+
elsif File.exist?(file_path)
|
|
318
|
+
if File.exist?(failed_path)
|
|
319
|
+
File.delete(file_path)
|
|
320
|
+
else
|
|
321
|
+
FileUtils.mv(file_path, failed_path)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Get current queue status
|
|
328
|
+
# @return [Hash] Queue status information
|
|
329
|
+
def queue_status
|
|
330
|
+
begin
|
|
331
|
+
pending_count = Dir[File.join(@queue_base_path, PENDING_DIR, '*.json')].size
|
|
332
|
+
processing_count = Dir[File.join(@queue_base_path, PROCESSING_DIR, '*.json')].size
|
|
333
|
+
failed_count = Dir[File.join(@queue_base_path, FAILED_DIR, '*.json')].size
|
|
334
|
+
success_count = Dir[File.join(@queue_base_path, SUCCESS_DIR, '*.json')].size
|
|
335
|
+
|
|
336
|
+
{
|
|
337
|
+
pending_count: pending_count,
|
|
338
|
+
processing_count: processing_count,
|
|
339
|
+
failed_count: failed_count,
|
|
340
|
+
success_count: success_count,
|
|
341
|
+
is_running: @is_running
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
rescue => e
|
|
345
|
+
@logger.error "Failed to get queue status: #{e.message}"
|
|
346
|
+
{
|
|
347
|
+
pending_count: 0,
|
|
348
|
+
processing_count: 0,
|
|
349
|
+
failed_count: 0,
|
|
350
|
+
success_count: 0,
|
|
351
|
+
is_running: false
|
|
352
|
+
}
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Retry failed submissions
|
|
357
|
+
def retry_failed_submissions
|
|
358
|
+
begin
|
|
359
|
+
failed_dir = File.join(@queue_base_path, FAILED_DIR)
|
|
360
|
+
files = Dir[File.join(failed_dir, '*.json')]
|
|
361
|
+
|
|
362
|
+
if files.empty?
|
|
363
|
+
@logger.info "No failed submissions to retry"
|
|
364
|
+
return
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
@logger.info "Retrying #{files.size} failed submissions"
|
|
368
|
+
|
|
369
|
+
files.each do |file_path|
|
|
370
|
+
file_name = File.basename(file_path)
|
|
371
|
+
pending_path = File.join(@queue_base_path, PENDING_DIR, file_name)
|
|
372
|
+
FileUtils.mv(file_path, pending_path)
|
|
373
|
+
@logger.debug "Moved failed submission back to pending: #{file_name}"
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
rescue => e
|
|
377
|
+
@logger.error "Failed to retry failed submissions: #{e.message}"
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Clean up old success files
|
|
382
|
+
# @param days_to_keep [Integer] Number of days to keep success files
|
|
383
|
+
def cleanup_old_success_files(days_to_keep)
|
|
384
|
+
begin
|
|
385
|
+
success_dir = File.join(@queue_base_path, SUCCESS_DIR)
|
|
386
|
+
cutoff_time = Time.now - (days_to_keep * 24 * 60 * 60)
|
|
387
|
+
|
|
388
|
+
files = Dir[File.join(success_dir, '*.json')].select do |file|
|
|
389
|
+
File.mtime(file) < cutoff_time
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
files.each do |file|
|
|
393
|
+
File.delete(file)
|
|
394
|
+
@logger.debug "Cleaned up old success file: #{File.basename(file)}"
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
@logger.info "Cleaned up #{files.size} old success files" unless files.empty?
|
|
398
|
+
|
|
399
|
+
rescue => e
|
|
400
|
+
@logger.error "Failed to cleanup old success files: #{e.message}"
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Clear all files from the queue (emergency cleanup)
|
|
405
|
+
def clear_all_queues
|
|
406
|
+
begin
|
|
407
|
+
@logger.info "Clearing all queue directories..."
|
|
408
|
+
|
|
409
|
+
clear_directory(PENDING_DIR)
|
|
410
|
+
clear_directory(PROCESSING_DIR)
|
|
411
|
+
clear_directory(FAILED_DIR)
|
|
412
|
+
clear_directory(SUCCESS_DIR)
|
|
413
|
+
|
|
414
|
+
@logger.info "All queue directories cleared successfully"
|
|
415
|
+
|
|
416
|
+
rescue => e
|
|
417
|
+
@logger.error "Error clearing queue directories: #{e.message}"
|
|
418
|
+
raise "Failed to clear queues: #{e.message}"
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Clear all files from a specific queue directory
|
|
423
|
+
# @param dir_name [String] Directory name to clear
|
|
424
|
+
private def clear_directory(dir_name)
|
|
425
|
+
dir = File.join(@queue_base_path, dir_name)
|
|
426
|
+
if File.directory?(dir)
|
|
427
|
+
files = Dir[File.join(dir, '*.json')]
|
|
428
|
+
files.each do |file|
|
|
429
|
+
File.delete(file)
|
|
430
|
+
@logger.debug "Deleted file: #{File.basename(file)}"
|
|
431
|
+
end
|
|
432
|
+
@logger.info "Cleared #{files.size} files from #{dir_name}"
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Clean up duplicate files across queue directories
|
|
437
|
+
def cleanup_duplicate_files
|
|
438
|
+
begin
|
|
439
|
+
@logger.info "Cleaning up duplicate files across queue directories..."
|
|
440
|
+
|
|
441
|
+
file_map = {}
|
|
442
|
+
[PENDING_DIR, PROCESSING_DIR, FAILED_DIR, SUCCESS_DIR].each do |dir_name|
|
|
443
|
+
dir = File.join(@queue_base_path, dir_name)
|
|
444
|
+
if File.directory?(dir)
|
|
445
|
+
Dir[File.join(dir, '*.json')].each do |file|
|
|
446
|
+
file_name = File.basename(file)
|
|
447
|
+
if file_map[file_name]
|
|
448
|
+
# File exists in multiple directories, keep the one with latest modification time
|
|
449
|
+
existing_time = File.mtime(file_map[file_name])
|
|
450
|
+
current_time = File.mtime(file)
|
|
451
|
+
|
|
452
|
+
if current_time > existing_time
|
|
453
|
+
File.delete(file_map[file_name])
|
|
454
|
+
file_map[file_name] = file
|
|
455
|
+
@logger.debug "Removed duplicate file (older): #{file_map[file_name]}"
|
|
456
|
+
else
|
|
457
|
+
File.delete(file)
|
|
458
|
+
@logger.debug "Removed duplicate file (older): #{file}"
|
|
459
|
+
end
|
|
460
|
+
else
|
|
461
|
+
file_map[file_name] = file
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
@logger.info "Duplicate file cleanup completed"
|
|
468
|
+
|
|
469
|
+
rescue => e
|
|
470
|
+
@logger.error "Error during duplicate file cleanup: #{e.message}"
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|