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.
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Template
5
+ VERSION = "0.3.0"
6
+ end
7
+ end