shoryuken-template 0.3.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/.rubocop.yml +60 -0
- data/CHANGELOG.md +74 -0
- data/CONTRIBUTING.md +98 -0
- data/INSTALLATION.md +361 -0
- data/LICENSE.txt +21 -0
- data/PUBLISHING.md +111 -0
- data/Procfile.example +13 -0
- data/QUICKSTART.md +156 -0
- data/README.md +393 -0
- data/RUBY_3.4_FEATURES.md +341 -0
- data/Rakefile +31 -0
- data/UPGRADE_GUIDE.md +293 -0
- data/examples/configuration_example.rb +47 -0
- data/examples/fifo_worker_example.rb +60 -0
- data/examples/heroku_setup.rb +59 -0
- data/examples/parallel_worker_example.rb +49 -0
- data/examples/standard_worker_example.rb +38 -0
- data/examples/worker_specific_config_example.rb +134 -0
- data/lib/shoryuken/template/configuration.rb +133 -0
- data/lib/shoryuken/template/error_handler.rb +118 -0
- data/lib/shoryuken/template/logger.rb +92 -0
- data/lib/shoryuken/template/version.rb +7 -0
- data/lib/shoryuken/template/workers/base_worker.rb +72 -0
- data/lib/shoryuken/template/workers/fifo_worker.rb +84 -0
- data/lib/shoryuken/template/workers/parallel_worker.rb +120 -0
- data/lib/shoryuken/template/workers/standard_worker.rb +44 -0
- data/lib/shoryuken/template.rb +16 -0
- data/shoryuken.yml.example +43 -0
- data/sig/shoryuken/template.rbs +6 -0
- metadata +192 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shoryuken/template'
|
|
4
|
+
|
|
5
|
+
# Exemplo de Worker FIFO
|
|
6
|
+
# Processa mensagens sequencialmente de uma fila FIFO
|
|
7
|
+
class AccountTransactionWorker
|
|
8
|
+
include Shoryuken::Template::Workers::FifoWorker
|
|
9
|
+
|
|
10
|
+
# Configuração do worker para fila FIFO
|
|
11
|
+
worker_options queue: 'transactions-queue.fifo'
|
|
12
|
+
|
|
13
|
+
# Processa mensagens sequencialmente
|
|
14
|
+
# message_group_id garante a ordem dentro do grupo
|
|
15
|
+
def process_message(body, message_group_id)
|
|
16
|
+
transaction_id = body['transaction_id']
|
|
17
|
+
account_id = body['account_id']
|
|
18
|
+
amount = body['amount']
|
|
19
|
+
transaction_type = body['type']
|
|
20
|
+
|
|
21
|
+
log_info(
|
|
22
|
+
"Processando transação sequencialmente",
|
|
23
|
+
transaction_id:,
|
|
24
|
+
account_id:,
|
|
25
|
+
message_group_id:
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Lógica que requer processamento sequencial
|
|
29
|
+
# Por exemplo: débitos e créditos em uma conta
|
|
30
|
+
account = load_account(account_id)
|
|
31
|
+
|
|
32
|
+
case transaction_type
|
|
33
|
+
when 'credit'
|
|
34
|
+
account.add_credit(amount)
|
|
35
|
+
when 'debit'
|
|
36
|
+
account.add_debit(amount)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
account.save!
|
|
40
|
+
|
|
41
|
+
log_info(
|
|
42
|
+
"Transação processada com sucesso",
|
|
43
|
+
transaction_id:,
|
|
44
|
+
new_balance: account.balance
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def load_account(account_id)
|
|
51
|
+
# Lógica de carregamento da conta
|
|
52
|
+
OpenStruct.new(
|
|
53
|
+
id: account_id,
|
|
54
|
+
balance: 1000,
|
|
55
|
+
add_credit: ->(amount) { @balance = balance + amount },
|
|
56
|
+
add_debit: ->(amount) { @balance = balance - amount },
|
|
57
|
+
save!: -> { true }
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Configuração para Heroku
|
|
4
|
+
# Este arquivo deve ser carregado antes de iniciar os workers
|
|
5
|
+
# Ruby 3.4.2+ com YJIT habilitado para melhor performance
|
|
6
|
+
|
|
7
|
+
require 'shoryuken/template'
|
|
8
|
+
|
|
9
|
+
# Configuração otimizada para Heroku
|
|
10
|
+
Shoryuken::Template.configure do |config|
|
|
11
|
+
# Dyno padrão tem 512MB de RAM
|
|
12
|
+
# Ajuste baseado no tipo de dyno
|
|
13
|
+
dyno_type = ENV['DYNO_SIZE'] || 'standard-1x'
|
|
14
|
+
|
|
15
|
+
# Pool de threads global baseado no tipo de dyno
|
|
16
|
+
config.thread_pool_size = case dyno_type
|
|
17
|
+
when 'standard-1x'
|
|
18
|
+
10
|
|
19
|
+
when 'standard-2x'
|
|
20
|
+
20
|
|
21
|
+
when 'performance-m'
|
|
22
|
+
40
|
|
23
|
+
when 'performance-l'
|
|
24
|
+
80
|
|
25
|
+
else
|
|
26
|
+
10
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Timeout menor para evitar timeout do Heroku (30s) em workers web-facing
|
|
30
|
+
# Para workers assíncronos, pode usar timeout maior
|
|
31
|
+
config.thread_timeout = 25
|
|
32
|
+
|
|
33
|
+
# Log level baseado no ambiente
|
|
34
|
+
config.logger_level = ENV['RACK_ENV'] == 'production' ? :info : :debug
|
|
35
|
+
|
|
36
|
+
# Retry configurado
|
|
37
|
+
config.max_retries = 3
|
|
38
|
+
config.retry_delay = 5
|
|
39
|
+
|
|
40
|
+
# Configurações específicas por worker se necessário
|
|
41
|
+
# Workers que processam tarefas longas podem ter timeout maior
|
|
42
|
+
if ENV['RACK_ENV'] == 'production'
|
|
43
|
+
config.configure_worker('HeavyProcessingWorker',
|
|
44
|
+
thread_pool_size: 5,
|
|
45
|
+
thread_timeout: 600 # 10 minutos para tarefas pesadas
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Graceful shutdown para Heroku
|
|
51
|
+
Signal.trap('TERM') do
|
|
52
|
+
puts 'SIGTERM recebido, iniciando shutdown graceful...'
|
|
53
|
+
|
|
54
|
+
# Aguarda finalização de todas as threads
|
|
55
|
+
Shoryuken::Template.configuration.reset_thread_pools!
|
|
56
|
+
|
|
57
|
+
puts 'Shutdown concluído'
|
|
58
|
+
exit(0)
|
|
59
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shoryuken/template'
|
|
4
|
+
|
|
5
|
+
# Exemplo de Worker Paralelo
|
|
6
|
+
# Processa múltiplas mensagens em paralelo usando threads
|
|
7
|
+
class ImageProcessorWorker
|
|
8
|
+
include Shoryuken::Template::Workers::ParallelWorker
|
|
9
|
+
|
|
10
|
+
# Configuração do worker com batch habilitado
|
|
11
|
+
worker_options queue: 'images-queue', batch: true
|
|
12
|
+
|
|
13
|
+
# Cada mensagem do batch será processada em uma thread separada
|
|
14
|
+
def process_message(body)
|
|
15
|
+
image_url = body['image_url']
|
|
16
|
+
image_id = body['image_id']
|
|
17
|
+
|
|
18
|
+
log_info(
|
|
19
|
+
"Processando imagem em thread",
|
|
20
|
+
image_id:,
|
|
21
|
+
thread_id: Thread.current.object_id
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Sua lógica de processamento aqui
|
|
25
|
+
# Cada imagem é processada em paralelo
|
|
26
|
+
download_image(image_url)
|
|
27
|
+
resize_image(image_id)
|
|
28
|
+
upload_to_cdn(image_id)
|
|
29
|
+
|
|
30
|
+
log_info("Imagem processada com sucesso", image_id:)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def download_image(url)
|
|
36
|
+
# Lógica de download
|
|
37
|
+
sleep 2 # Simula download
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def resize_image(image_id)
|
|
41
|
+
# Lógica de redimensionamento
|
|
42
|
+
sleep 1 # Simula processamento
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def upload_to_cdn(image_id)
|
|
46
|
+
# Lógica de upload
|
|
47
|
+
sleep 1 # Simula upload
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shoryuken/template'
|
|
4
|
+
|
|
5
|
+
# Exemplo de Worker Padrão
|
|
6
|
+
# Processa uma mensagem por vez de uma fila SQS padrão
|
|
7
|
+
class OrderProcessorWorker
|
|
8
|
+
include Shoryuken::Template::Workers::StandardWorker
|
|
9
|
+
|
|
10
|
+
# Configuração do worker
|
|
11
|
+
worker_options queue: 'orders-queue'
|
|
12
|
+
|
|
13
|
+
# Implementa o método de processamento
|
|
14
|
+
def process_message(body)
|
|
15
|
+
order_id = body['order_id']
|
|
16
|
+
customer_id = body['customer_id']
|
|
17
|
+
|
|
18
|
+
log_info("Processando pedido", order_id:, customer_id:)
|
|
19
|
+
|
|
20
|
+
# Sua lógica de negócio aqui
|
|
21
|
+
process_order(order_id)
|
|
22
|
+
notify_customer(customer_id)
|
|
23
|
+
|
|
24
|
+
log_info("Pedido processado com sucesso", order_id:)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def process_order(order_id)
|
|
30
|
+
# Lógica de processamento do pedido
|
|
31
|
+
sleep 1 # Simula processamento
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def notify_customer(customer_id)
|
|
35
|
+
# Lógica de notificação
|
|
36
|
+
sleep 0.5 # Simula envio de notificação
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shoryuken/template'
|
|
4
|
+
|
|
5
|
+
# Exemplo de configuração específica por worker
|
|
6
|
+
# Demonstra como configurar diferentes pools e timeouts para diferentes tipos de workers
|
|
7
|
+
|
|
8
|
+
Shoryuken::Template.configure do |config|
|
|
9
|
+
# ===== CONFIGURAÇÃO GLOBAL (FALLBACK) =====
|
|
10
|
+
# Usada por workers que não têm configuração específica
|
|
11
|
+
config.thread_pool_size = 10
|
|
12
|
+
config.thread_timeout = 300 # 5 minutos
|
|
13
|
+
|
|
14
|
+
# ===== WORKERS I/O BOUND =====
|
|
15
|
+
# Processamento de emails, chamadas HTTP, etc
|
|
16
|
+
# Podem ter muitas threads pois ficam esperando I/O
|
|
17
|
+
config.configure_worker('EmailWorker',
|
|
18
|
+
thread_pool_size: 50, # Muitas threads
|
|
19
|
+
thread_timeout: 30 # Timeout curto
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
config.configure_worker('NotificationWorker',
|
|
23
|
+
thread_pool_size: 40,
|
|
24
|
+
thread_timeout: 60
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# ===== WORKERS CPU BOUND =====
|
|
28
|
+
# Processamento de vídeos, imagens pesadas, cálculos complexos
|
|
29
|
+
# Devem ter menos threads para não sobrecarregar a CPU
|
|
30
|
+
config.configure_worker('VideoProcessorWorker',
|
|
31
|
+
thread_pool_size: 5, # Poucas threads
|
|
32
|
+
thread_timeout: 1800 # Timeout longo (30 minutos)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
config.configure_worker('ImageProcessorWorker',
|
|
36
|
+
thread_pool_size: 10,
|
|
37
|
+
thread_timeout: 600 # 10 minutos
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# ===== WORKERS CRÍTICOS =====
|
|
41
|
+
# Workers importantes que precisam de recursos dedicados
|
|
42
|
+
config.configure_worker('PaymentProcessorWorker',
|
|
43
|
+
thread_pool_size: 20, # Pool dedicado
|
|
44
|
+
thread_timeout: 120 # 2 minutos
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# ===== WORKERS EXPERIMENTAIS =====
|
|
48
|
+
# Workers novos ou em teste com recursos limitados
|
|
49
|
+
config.configure_worker('ExperimentalWorker',
|
|
50
|
+
thread_pool_size: 2, # Pool pequeno
|
|
51
|
+
thread_timeout: 60 # Timeout curto
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Outros workers não listados usarão a configuração global
|
|
55
|
+
# Ex: 'OrderProcessorWorker' usará thread_pool_size: 10, timeout: 300
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# ===== IMPLEMENTAÇÃO DOS WORKERS =====
|
|
59
|
+
|
|
60
|
+
# Worker I/O Bound - Email
|
|
61
|
+
class EmailWorker
|
|
62
|
+
include Shoryuken::Template::Workers::ParallelWorker
|
|
63
|
+
|
|
64
|
+
worker_options queue: 'emails-queue', batch: true
|
|
65
|
+
|
|
66
|
+
def process_message(body)
|
|
67
|
+
# Usa pool de 50 threads, timeout de 30s
|
|
68
|
+
send_email(body['to'], body['subject'], body['body'])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def send_email(to, subject, body)
|
|
74
|
+
log_info("Enviando email", to:)
|
|
75
|
+
# Simula envio de email (I/O bound)
|
|
76
|
+
sleep 2
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Worker CPU Bound - Vídeo
|
|
81
|
+
class VideoProcessorWorker
|
|
82
|
+
include Shoryuken::Template::Workers::ParallelWorker
|
|
83
|
+
|
|
84
|
+
worker_options queue: 'videos-queue', batch: true
|
|
85
|
+
|
|
86
|
+
def process_message(body)
|
|
87
|
+
# Usa pool de 5 threads, timeout de 30 minutos
|
|
88
|
+
process_video(body['video_id'])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def process_video(video_id)
|
|
94
|
+
log_info("Processando vídeo", video_id:)
|
|
95
|
+
# Processamento pesado de vídeo (CPU bound)
|
|
96
|
+
sleep 10
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Worker sem configuração específica - usa global
|
|
101
|
+
class OrderProcessorWorker
|
|
102
|
+
include Shoryuken::Template::Workers::StandardWorker
|
|
103
|
+
|
|
104
|
+
worker_options queue: 'orders-queue'
|
|
105
|
+
|
|
106
|
+
def process_message(body)
|
|
107
|
+
# Usa configuração global: pool de 10 threads, timeout de 5 minutos
|
|
108
|
+
process_order(body['order_id'])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def process_order(order_id)
|
|
114
|
+
log_info("Processando pedido", order_id:)
|
|
115
|
+
sleep 1
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# ===== DICAS =====
|
|
120
|
+
#
|
|
121
|
+
# 1. Configure apenas workers com necessidades muito diferentes
|
|
122
|
+
# - Não configure todos os workers individualmente
|
|
123
|
+
#
|
|
124
|
+
# 2. Workers I/O bound: mais threads (30-50)
|
|
125
|
+
# - Email, HTTP, API calls, Database queries
|
|
126
|
+
#
|
|
127
|
+
# 3. Workers CPU bound: menos threads (5-10)
|
|
128
|
+
# - Processamento de vídeo, imagens, cálculos complexos
|
|
129
|
+
#
|
|
130
|
+
# 4. Monitore o uso de memória
|
|
131
|
+
# - Muitos pools grandes = muita memória
|
|
132
|
+
#
|
|
133
|
+
# 5. Use configuração global como fallback
|
|
134
|
+
# - Deixe valores sensatos na configuração global
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent'
|
|
4
|
+
|
|
5
|
+
module Shoryuken
|
|
6
|
+
module Template
|
|
7
|
+
# Configuração global para o Shoryuken Template
|
|
8
|
+
class Configuration
|
|
9
|
+
attr_accessor :thread_pool_size, :thread_timeout, :error_handler,
|
|
10
|
+
:logger_level, :max_retries, :retry_delay
|
|
11
|
+
attr_reader :worker_configs
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@thread_pool_size = ENV.fetch('SHORYUKEN_THREAD_POOL_SIZE', '10').to_i
|
|
15
|
+
@thread_timeout = ENV.fetch('SHORYUKEN_THREAD_TIMEOUT', '300').to_i
|
|
16
|
+
@logger_level = ENV.fetch('SHORYUKEN_LOG_LEVEL', 'info').to_sym
|
|
17
|
+
@max_retries = ENV.fetch('SHORYUKEN_MAX_RETRIES', '3').to_i
|
|
18
|
+
@retry_delay = ENV.fetch('SHORYUKEN_RETRY_DELAY', '5').to_i
|
|
19
|
+
@error_handler = nil
|
|
20
|
+
@worker_configs = {}
|
|
21
|
+
@thread_pools = {}
|
|
22
|
+
@mutex = Mutex.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Configura um worker específico
|
|
26
|
+
def configure_worker(worker_class_or_name, thread_pool_size: nil, thread_timeout: nil)
|
|
27
|
+
worker_name = worker_class_or_name.is_a?(String) ? worker_class_or_name : worker_class_or_name.name
|
|
28
|
+
|
|
29
|
+
@mutex.synchronize do
|
|
30
|
+
@worker_configs[worker_name] = {
|
|
31
|
+
thread_pool_size:,
|
|
32
|
+
thread_timeout:
|
|
33
|
+
}.compact
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Retorna o pool de threads para um worker específico ou o pool global
|
|
38
|
+
def thread_pool_for(worker_name = nil)
|
|
39
|
+
return default_thread_pool unless worker_name
|
|
40
|
+
|
|
41
|
+
config = @worker_configs[worker_name]
|
|
42
|
+
pool_size = config&.dig(:thread_pool_size)
|
|
43
|
+
|
|
44
|
+
return default_thread_pool unless pool_size
|
|
45
|
+
|
|
46
|
+
@mutex.synchronize do
|
|
47
|
+
@thread_pools[worker_name] ||= create_thread_pool(pool_size)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Retorna o timeout para um worker específico ou o timeout global
|
|
52
|
+
def thread_timeout_for(worker_name = nil)
|
|
53
|
+
return thread_timeout unless worker_name
|
|
54
|
+
|
|
55
|
+
@worker_configs.dig(worker_name, :thread_timeout) || thread_timeout
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Pool de threads padrão
|
|
59
|
+
def default_thread_pool
|
|
60
|
+
@default_thread_pool ||= create_thread_pool(thread_pool_size)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Reset de todos os pools de threads
|
|
64
|
+
def reset_thread_pools!
|
|
65
|
+
@mutex.synchronize do
|
|
66
|
+
# Shutdown do pool padrão
|
|
67
|
+
shutdown_pool(@default_thread_pool)
|
|
68
|
+
@default_thread_pool = nil
|
|
69
|
+
|
|
70
|
+
# Shutdown dos pools específicos
|
|
71
|
+
@thread_pools.each_value { shutdown_pool(_1) }
|
|
72
|
+
@thread_pools.clear
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Validação da configuração
|
|
77
|
+
def validate!
|
|
78
|
+
raise ConfigurationError, "thread_pool_size deve ser maior que 0" if thread_pool_size <= 0
|
|
79
|
+
raise ConfigurationError, "thread_timeout deve ser maior que 0" if thread_timeout <= 0
|
|
80
|
+
raise ConfigurationError, "max_retries deve ser maior ou igual a 0" if max_retries < 0
|
|
81
|
+
|
|
82
|
+
# Valida configurações específicas de workers
|
|
83
|
+
@worker_configs.each do |worker_name, config|
|
|
84
|
+
if config[:thread_pool_size] && config[:thread_pool_size] <= 0
|
|
85
|
+
raise ConfigurationError, "thread_pool_size para #{worker_name} deve ser maior que 0"
|
|
86
|
+
end
|
|
87
|
+
if config[:thread_timeout] && config[:thread_timeout] <= 0
|
|
88
|
+
raise ConfigurationError, "thread_timeout para #{worker_name} deve ser maior que 0"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def create_thread_pool(size)
|
|
96
|
+
Concurrent::FixedThreadPool.new(
|
|
97
|
+
size,
|
|
98
|
+
max_queue: size * 2,
|
|
99
|
+
fallback_policy: :caller_runs,
|
|
100
|
+
idletime: 60
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def shutdown_pool(pool)
|
|
105
|
+
return unless pool
|
|
106
|
+
|
|
107
|
+
pool.shutdown
|
|
108
|
+
pool.wait_for_termination(30)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class ConfigurationError < Error; end
|
|
113
|
+
|
|
114
|
+
# Configuração singleton
|
|
115
|
+
class << self
|
|
116
|
+
attr_writer :configuration
|
|
117
|
+
|
|
118
|
+
def configuration
|
|
119
|
+
@configuration ||= Configuration.new
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def configure
|
|
123
|
+
yield(configuration)
|
|
124
|
+
configuration.validate!
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def reset_configuration!
|
|
128
|
+
configuration.reset_thread_pools!
|
|
129
|
+
@configuration = Configuration.new
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoryuken
|
|
4
|
+
module Template
|
|
5
|
+
# Tratamento de erros robusto com retry logic
|
|
6
|
+
class ErrorHandler
|
|
7
|
+
class << self
|
|
8
|
+
# Executa bloco com retry logic
|
|
9
|
+
def with_retry(max_retries: nil, delay: nil, on: StandardError)
|
|
10
|
+
config = Shoryuken::Template.configuration
|
|
11
|
+
max_retries ||= config.max_retries
|
|
12
|
+
delay ||= config.retry_delay
|
|
13
|
+
attempts = 0
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
attempts += 1
|
|
17
|
+
yield
|
|
18
|
+
rescue on => e
|
|
19
|
+
if attempts <= max_retries
|
|
20
|
+
Shoryuken::Template.logger.warn(
|
|
21
|
+
"Tentativa #{attempts}/#{max_retries} falhou, tentando novamente em #{delay}s",
|
|
22
|
+
error: e.message,
|
|
23
|
+
error_class: e.class.name,
|
|
24
|
+
backtrace: e.backtrace.first(5)
|
|
25
|
+
)
|
|
26
|
+
sleep(delay * attempts) # Backoff exponencial
|
|
27
|
+
retry
|
|
28
|
+
else
|
|
29
|
+
Shoryuken::Template.logger.error(
|
|
30
|
+
"Todas as tentativas falharam",
|
|
31
|
+
error: e.message,
|
|
32
|
+
error_class: e.class.name,
|
|
33
|
+
attempts:,
|
|
34
|
+
backtrace: e.backtrace.first(10)
|
|
35
|
+
)
|
|
36
|
+
raise
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Executa bloco com tratamento de erro
|
|
42
|
+
def handle_error(worker_name, message = nil, &block)
|
|
43
|
+
start_time = Time.now
|
|
44
|
+
|
|
45
|
+
begin
|
|
46
|
+
result = block.call
|
|
47
|
+
duration = Time.now - start_time
|
|
48
|
+
|
|
49
|
+
Shoryuken::Template.logger.info(
|
|
50
|
+
"Mensagem processada com sucesso",
|
|
51
|
+
worker: worker_name,
|
|
52
|
+
duration: duration.round(3),
|
|
53
|
+
message_id: message&.message_id
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
result
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
duration = Time.now - start_time
|
|
59
|
+
|
|
60
|
+
Shoryuken::Template.logger.error(
|
|
61
|
+
"Erro ao processar mensagem",
|
|
62
|
+
worker: worker_name,
|
|
63
|
+
error: e.message,
|
|
64
|
+
error_class: e.class.name,
|
|
65
|
+
duration: duration.round(3),
|
|
66
|
+
message_id: message&.message_id,
|
|
67
|
+
backtrace: e.backtrace.first(10)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Chama error handler customizado se configurado
|
|
71
|
+
if Shoryuken::Template.configuration.error_handler
|
|
72
|
+
call_custom_error_handler(e, worker_name, message)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
raise
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Executa bloco com timeout
|
|
80
|
+
def with_timeout(seconds, &)
|
|
81
|
+
result = nil
|
|
82
|
+
timeout_thread = Thread.new { sleep(seconds) }
|
|
83
|
+
result_thread = Thread.new { result = yield }
|
|
84
|
+
|
|
85
|
+
if result_thread.join(seconds)
|
|
86
|
+
timeout_thread.kill
|
|
87
|
+
raise result_thread[:exception] if result_thread[:exception]
|
|
88
|
+
|
|
89
|
+
result
|
|
90
|
+
else
|
|
91
|
+
result_thread.kill
|
|
92
|
+
raise TimeoutError, "Operação excedeu o timeout de #{seconds} segundos"
|
|
93
|
+
end
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
result_thread&.kill
|
|
96
|
+
timeout_thread&.kill
|
|
97
|
+
raise e
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def call_custom_error_handler(error, worker_name, message)
|
|
103
|
+
handler = Shoryuken::Template.configuration.error_handler
|
|
104
|
+
handler.call(error, worker_name, message)
|
|
105
|
+
rescue => handler_error
|
|
106
|
+
Shoryuken::Template.logger.error(
|
|
107
|
+
"Erro no error handler customizado",
|
|
108
|
+
error: handler_error.message,
|
|
109
|
+
error_class: handler_error.class.name,
|
|
110
|
+
backtrace: handler_error.backtrace.first(5)
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class TimeoutError < Error; end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Shoryuken
|
|
7
|
+
module Template
|
|
8
|
+
# Logger estruturado com suporte a JSON
|
|
9
|
+
class StructuredLogger
|
|
10
|
+
LEVELS = {
|
|
11
|
+
debug: Logger::DEBUG,
|
|
12
|
+
info: Logger::INFO,
|
|
13
|
+
warn: Logger::WARN,
|
|
14
|
+
error: Logger::ERROR,
|
|
15
|
+
fatal: Logger::FATAL
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :logger
|
|
19
|
+
|
|
20
|
+
def initialize(output = $stdout)
|
|
21
|
+
@logger = Logger.new(output)
|
|
22
|
+
@logger.level = LEVELS[Shoryuken::Template.configuration.logger_level] || Logger::INFO
|
|
23
|
+
@logger.formatter = method(:formatter)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def debug(message = nil, **context, &block)
|
|
27
|
+
log(:debug, message, context, &block)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def info(message = nil, **context, &block)
|
|
31
|
+
log(:info, message, context, &block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def warn(message = nil, **context, &block)
|
|
35
|
+
log(:warn, message, context, &block)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def error(message = nil, **context, &block)
|
|
39
|
+
log(:error, message, context, &block)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def fatal(message = nil, **context, &block)
|
|
43
|
+
log(:fatal, message, context, &block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def log(level, message, context, &block)
|
|
49
|
+
return unless @logger.send("#{level}?")
|
|
50
|
+
|
|
51
|
+
message = block.call if block_given?
|
|
52
|
+
@logger.send(level, build_log_data(message, context))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_log_data(message, context)
|
|
56
|
+
{
|
|
57
|
+
timestamp: Time.now.utc.iso8601(3),
|
|
58
|
+
level: @logger.level,
|
|
59
|
+
message:,
|
|
60
|
+
pid: Process.pid,
|
|
61
|
+
thread_id: Thread.current.object_id,
|
|
62
|
+
environment: ENV.fetch('RACK_ENV') { ENV.fetch('RAILS_ENV', 'development') }
|
|
63
|
+
}.merge(context)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def formatter(_severity, _time, _progname, msg)
|
|
67
|
+
log_hash = case msg
|
|
68
|
+
when Hash
|
|
69
|
+
msg
|
|
70
|
+
else
|
|
71
|
+
{
|
|
72
|
+
timestamp: Time.now.utc.iso8601(3),
|
|
73
|
+
message: msg.to_s,
|
|
74
|
+
pid: Process.pid,
|
|
75
|
+
thread_id: Thread.current.object_id
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
"#{JSON.generate(log_hash)}\n"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Logger singleton
|
|
84
|
+
class << self
|
|
85
|
+
attr_writer :logger
|
|
86
|
+
|
|
87
|
+
def logger
|
|
88
|
+
@logger ||= StructuredLogger.new
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|