bug_bunny 3.1.0 → 3.1.2
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/CHANGELOG.md +25 -0
- data/README.md +197 -345
- data/lib/bug_bunny/client.rb +26 -11
- data/lib/bug_bunny/configuration.rb +28 -2
- data/lib/bug_bunny/consumer.rb +27 -16
- data/lib/bug_bunny/controller.rb +142 -70
- data/lib/bug_bunny/producer.rb +51 -32
- data/lib/bug_bunny/request.rb +14 -2
- data/lib/bug_bunny/resource.rb +152 -18
- data/lib/bug_bunny/session.rb +47 -18
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +1 -1
- data/test/integration/infrastructure_test.rb +61 -0
- data/test/integration/manual_client_test.rb +203 -0
- data/test/test_helper.rb +96 -11
- metadata +18 -16
- data/bin_client.rb +0 -51
- data/bin_suite.rb +0 -106
- data/bin_worker.rb +0 -26
- data/test/integration/fire_and_forget_test.rb +0 -76
- data/test/integration/rpc_flow_test.rb +0 -78
- data/test/unit/configuration_test.rb +0 -40
- data/test/unit/consumer_test.rb +0 -44
- data/test/unit/controller_headers_test.rb +0 -38
- data/test/unit/hybrid_resource_test.rb +0 -60
- data/test/unit/middleware_test.rb +0 -61
- data/test/unit/resource_test.rb +0 -49
- data/test_controller.rb +0 -49
- data/test_helper.rb +0 -20
- data/test_resource.rb +0 -19
data/README.md
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
# 🐰 BugBunny
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/bug_bunny)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Active Record over AMQP.**
|
|
6
|
+
|
|
7
|
+
BugBunny transforma la complejidad de la mensajería asíncrona (RabbitMQ) en una arquitectura **RESTful familiar** para desarrolladores Rails. Envía mensajes como si estuvieras usando Active Record y procésalos como si fueran Controladores de Rails.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 📖 Tabla de Contenidos
|
|
12
|
+
- [Introducción: La Filosofía](#-introducción-la-filosofía)
|
|
13
|
+
- [Instalación](#-instalación)
|
|
14
|
+
- [Configuración Inicial](#-configuración-inicial)
|
|
15
|
+
- [Configuración de Infraestructura en Cascada](#-configuración-de-infraestructura-en-cascada-nuevo-v31)
|
|
16
|
+
- [Modo Cliente: Recursos (ORM)](#-modo-cliente-recursos-orm)
|
|
17
|
+
- [Definición y Atributos](#1-definición-y-atributos-híbridos)
|
|
18
|
+
- [CRUD y Consultas](#2-crud-y-consultas-restful)
|
|
19
|
+
- [Contexto Dinámico (.with)](#3-contexto-dinámico-with)
|
|
20
|
+
- [Client Middleware](#4-client-middleware-interceptores)
|
|
21
|
+
- [Modo Servidor: Controladores](#-modo-servidor-controladores)
|
|
22
|
+
- [Ruteo Inteligente](#1-ruteo-inteligente)
|
|
23
|
+
- [El Controlador](#2-el-controlador)
|
|
24
|
+
- [Manejo de Errores](#3-manejo-de-errores-declarativo)
|
|
25
|
+
- [Observabilidad y Tracing](#-observabilidad-y-tracing)
|
|
26
|
+
- [Guía de Producción](#-guía-de-producción)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 💡 Introducción: La Filosofía
|
|
31
|
+
|
|
32
|
+
En lugar de pensar en "Exchanges" y "Queues", BugBunny inyecta verbos HTTP (`GET`, `POST`, `PUT`, `DELETE`) y rutas (`users/1`) en los headers de AMQP.
|
|
33
|
+
|
|
34
|
+
* **Tu código (Cliente):** `User.create(name: 'Gabi')`
|
|
35
|
+
* **Protocolo (BugBunny):** Envía `POST /users` (Header `type: users`) vía RabbitMQ.
|
|
36
|
+
* **Worker (Servidor):** Recibe el mensaje y ejecuta `UsersController#create`.
|
|
6
37
|
|
|
7
38
|
---
|
|
8
39
|
|
|
@@ -11,495 +42,316 @@ Su filosofía es **"Active Record over AMQP"**. Transforma la complejidad de la
|
|
|
11
42
|
Agrega la gema a tu `Gemfile`:
|
|
12
43
|
|
|
13
44
|
```ruby
|
|
14
|
-
gem 'bug_bunny'
|
|
45
|
+
gem 'bug_bunny', '~> 3.1'
|
|
15
46
|
```
|
|
16
47
|
|
|
17
|
-
Ejecuta el bundle:
|
|
48
|
+
Ejecuta el bundle e instala los archivos base:
|
|
18
49
|
|
|
19
50
|
```bash
|
|
20
51
|
bundle install
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Genera los archivos de configuración iniciales:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
52
|
rails g bug_bunny:install
|
|
27
53
|
```
|
|
28
54
|
|
|
29
|
-
Esto
|
|
55
|
+
Esto genera:
|
|
30
56
|
1. `config/initializers/bug_bunny.rb`
|
|
31
57
|
2. `app/rabbit/controllers/`
|
|
32
58
|
|
|
33
59
|
---
|
|
34
60
|
|
|
35
|
-
## ⚙️ Configuración
|
|
36
|
-
|
|
37
|
-
### 1. Inicializador y Logging
|
|
61
|
+
## ⚙️ Configuración Inicial
|
|
38
62
|
|
|
39
|
-
|
|
63
|
+
Para entornos productivos (Puma/Sidekiq), es **obligatorio** configurar un Pool de conexiones.
|
|
40
64
|
|
|
41
65
|
```ruby
|
|
42
66
|
# config/initializers/bug_bunny.rb
|
|
43
67
|
|
|
44
68
|
BugBunny.configure do |config|
|
|
45
|
-
#
|
|
69
|
+
# 1. Credenciales
|
|
46
70
|
config.host = ENV.fetch('RABBITMQ_HOST', 'localhost')
|
|
47
71
|
config.username = ENV.fetch('RABBITMQ_USER', 'guest')
|
|
48
72
|
config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
|
|
49
73
|
config.vhost = ENV.fetch('RABBITMQ_VHOST', '/')
|
|
50
74
|
|
|
51
|
-
#
|
|
52
|
-
config.rpc_timeout = 10 #
|
|
53
|
-
config.network_recovery_interval = 5 #
|
|
54
|
-
|
|
55
|
-
# --- Logging (Niveles recomendados) ---
|
|
56
|
-
# Logger de BugBunny: Muestra tus requests (INFO)
|
|
57
|
-
rails_logger = Rails.logger
|
|
58
|
-
|
|
59
|
-
if defined?(ActiveSupport::TaggedLogging) && !rails_logger.respond_to?(:tagged)
|
|
60
|
-
config.logger = ActiveSupport::TaggedLogging.new(rails_logger)
|
|
61
|
-
else
|
|
62
|
-
config.logger = rails_logger
|
|
63
|
-
end
|
|
75
|
+
# 2. Timeouts y Recuperación
|
|
76
|
+
config.rpc_timeout = 10 # Segundos máx para esperar respuesta (Síncrono)
|
|
77
|
+
config.network_recovery_interval = 5 # Reintento de conexión
|
|
64
78
|
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
else
|
|
69
|
-
config.bunny_logger = rails_logger
|
|
70
|
-
end
|
|
71
|
-
config.bunny_logger.level = Logger::WARN
|
|
79
|
+
# 3. Logging
|
|
80
|
+
config.logger = Rails.logger
|
|
81
|
+
end
|
|
72
82
|
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
# 4. Connection Pool (CRÍTICO para concurrencia)
|
|
84
|
+
# Define un pool global para compartir conexiones entre hilos
|
|
85
|
+
BUG_BUNNY_POOL = ConnectionPool.new(size: ENV.fetch('RAILS_MAX_THREADS', 5).to_i, timeout: 5) do
|
|
86
|
+
BugBunny.create_connection
|
|
75
87
|
end
|
|
88
|
+
|
|
89
|
+
# Inyecta el pool en los recursos
|
|
90
|
+
BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
|
|
76
91
|
```
|
|
77
92
|
|
|
78
|
-
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 🏗️ Configuración de Infraestructura en Cascada (Nuevo v3.1)
|
|
96
|
+
|
|
97
|
+
BugBunny v3.1 introduce un sistema de configuración jerárquico para los parámetros de RabbitMQ (como la durabilidad de Exchanges y Colas). Las opciones se resuelven en el siguiente orden de prioridad:
|
|
79
98
|
|
|
80
|
-
|
|
99
|
+
1. **Defaults de la Gema:** Rápidos y efímeros (`durable: false`).
|
|
100
|
+
2. **Configuración Global:** Definida en el inicializador para todo el entorno.
|
|
101
|
+
3. **Configuración de Recurso:** Atributos de clase en modelos específicos.
|
|
102
|
+
4. **Configuración al Vuelo:** Parámetros pasados en la llamada `.with` o en el Cliente manual.
|
|
103
|
+
|
|
104
|
+
**Ejemplo de Configuración Global (Nivel 2):**
|
|
105
|
+
Útil para hacer que todos los recursos en el entorno de pruebas sean auto-borrables.
|
|
81
106
|
|
|
82
107
|
```ruby
|
|
83
108
|
# config/initializers/bug_bunny.rb
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
109
|
+
BugBunny.configure do |config|
|
|
110
|
+
if Rails.env.test?
|
|
111
|
+
config.exchange_options = { auto_delete: true }
|
|
112
|
+
config.queue_options = { auto_delete: true }
|
|
113
|
+
end
|
|
88
114
|
end
|
|
89
|
-
|
|
90
|
-
# Inyecta el pool a los recursos para que lo usen automáticamente
|
|
91
|
-
BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
|
|
92
115
|
```
|
|
93
116
|
|
|
94
117
|
---
|
|
95
118
|
|
|
96
|
-
## 🚀 Modo
|
|
119
|
+
## 🚀 Modo Cliente: Recursos (ORM)
|
|
97
120
|
|
|
98
|
-
|
|
121
|
+
Los recursos son proxies de servicios remotos. Heredan de `BugBunny::Resource`.
|
|
99
122
|
|
|
100
|
-
### Definición
|
|
123
|
+
### 1. Definición y Atributos Híbridos
|
|
124
|
+
BugBunny v3 es **Schema-less**. Soporta atributos tipados (ActiveModel) y dinámicos simultáneamente, además de definir su propia infraestructura.
|
|
101
125
|
|
|
102
126
|
```ruby
|
|
103
127
|
# app/models/manager/service.rb
|
|
104
128
|
class Manager::Service < BugBunny::Resource
|
|
105
|
-
#
|
|
106
|
-
self.exchange = '
|
|
107
|
-
self.exchange_type = '
|
|
129
|
+
# Configuración de Transporte
|
|
130
|
+
self.exchange = 'cluster_events'
|
|
131
|
+
self.exchange_type = 'topic'
|
|
108
132
|
|
|
109
|
-
#
|
|
110
|
-
#
|
|
133
|
+
# Configuración de Infraestructura Específica (Nivel 3)
|
|
134
|
+
# Este recurso crítico sobrevivirá a reinicios del servidor RabbitMQ
|
|
135
|
+
self.exchange_options = { durable: true, auto_delete: false }
|
|
136
|
+
|
|
137
|
+
# Configuración de Ruteo (La "URL" base)
|
|
111
138
|
self.resource_name = 'services'
|
|
112
139
|
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
140
|
+
# A. Atributos Tipados (Opcional, para casting)
|
|
141
|
+
attribute :created_at, :datetime
|
|
142
|
+
attribute :replicas, :integer, default: 1
|
|
143
|
+
|
|
144
|
+
# B. Validaciones (Funcionan en ambos tipos)
|
|
145
|
+
validates :name, presence: true
|
|
117
146
|
end
|
|
118
147
|
```
|
|
119
148
|
|
|
120
|
-
### CRUD RESTful
|
|
121
|
-
|
|
122
|
-
Las operaciones de Ruby se traducen a verbos HTTP sobre AMQP.
|
|
149
|
+
### 2. CRUD y Consultas RESTful
|
|
123
150
|
|
|
124
151
|
```ruby
|
|
125
152
|
# --- LEER (GET) ---
|
|
126
|
-
#
|
|
127
|
-
# Routing Key: "services"
|
|
128
|
-
services = Manager::Service.all
|
|
129
|
-
|
|
153
|
+
# RPC: Espera respuesta del worker.
|
|
130
154
|
# Envia: GET services/123
|
|
131
155
|
service = Manager::Service.find('123')
|
|
132
156
|
|
|
157
|
+
# --- BÚSQUEDAS AVANZADAS ---
|
|
158
|
+
# Soporta Hashes anidados para filtros complejos.
|
|
159
|
+
# Envia: GET services?q[status]=active&q[tags][]=web
|
|
160
|
+
Manager::Service.where(q: { status: 'active', tags: ['web'] })
|
|
161
|
+
|
|
133
162
|
# --- CREAR (POST) ---
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
# Nota: Envuelve los params automáticamente en la clave 'service'.
|
|
163
|
+
# RPC: Envía payload y espera el objeto persistido.
|
|
164
|
+
# Payload: { "service": { "name": "nginx", "replicas": 3 } }
|
|
137
165
|
svc = Manager::Service.create(name: 'nginx', replicas: 3)
|
|
138
166
|
|
|
139
167
|
# --- ACTUALIZAR (PUT) ---
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
svc.
|
|
168
|
+
# Dirty Tracking: Solo envía los campos que cambiaron.
|
|
169
|
+
svc.name = 'nginx-pro'
|
|
170
|
+
svc.save
|
|
143
171
|
|
|
144
172
|
# --- ELIMINAR (DELETE) ---
|
|
145
|
-
# Envia: DELETE services/123
|
|
146
173
|
svc.destroy
|
|
147
174
|
```
|
|
148
175
|
|
|
149
|
-
### Contexto Dinámico (`.with`)
|
|
150
|
-
|
|
151
|
-
Puedes cambiar la configuración (Routing Key, Exchange) para una operación específica sin afectar al modelo global. El contexto se mantiene durante el ciclo de vida del objeto.
|
|
176
|
+
### 3. Contexto Dinámico (`.with`)
|
|
177
|
+
Puedes sobrescribir la configuración de enrutamiento o infraestructura para una ejecución específica sin afectar al modelo global (Thread-Safe).
|
|
152
178
|
|
|
153
179
|
```ruby
|
|
154
|
-
#
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
# Al guardar, BugBunny recuerda el contexto y envía a 'urgent'
|
|
160
|
-
svc.save
|
|
161
|
-
# Log: [BugBunny] [POST] '/services' | Routing Key: 'urgent'
|
|
180
|
+
# Nivel 4: Configuración al vuelo. Inyectamos opciones solo para esta llamada.
|
|
181
|
+
Manager::Service.with(
|
|
182
|
+
routing_key: 'high_priority',
|
|
183
|
+
exchange_options: { durable: false } # Ignora el durable: true de la clase
|
|
184
|
+
).create(name: 'redis_temp')
|
|
162
185
|
```
|
|
163
186
|
|
|
164
|
-
###
|
|
165
|
-
|
|
187
|
+
### 4. Client Middleware (Interceptores)
|
|
188
|
+
Intercepta peticiones de ida y respuestas de vuelta en la arquitectura del cliente.
|
|
166
189
|
|
|
167
|
-
**
|
|
190
|
+
**Middlewares Incluidos (Built-ins)**
|
|
191
|
+
Si usas `BugBunny::Resource` el manejo de JSON y errores ya está integrado. Pero si utilizas el cliente manual (`BugBunny::Client`), puedes inyectar los middlewares incluidos para no tener que parsear respuestas manualmente:
|
|
168
192
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
# Soporta hashes anidados (gracias a Rack::Utils)
|
|
172
|
-
# Envia: GET services?q[status]=active&q[tags][]=web
|
|
173
|
-
Manager::Service.where(q: { status: 'active', tags: ['web'] })
|
|
174
|
-
```
|
|
193
|
+
* `BugBunny::Middleware::JsonResponse`: Parsea automáticamente el cuerpo de la respuesta de JSON a un Hash de Ruby.
|
|
194
|
+
* `BugBunny::Middleware::RaiseError`: Evalúa el código de estado (`status`) de la respuesta y lanza excepciones nativas (`BugBunny::NotFound`, `BugBunny::UnprocessableEntity`, `BugBunny::InternalServerError`, etc.).
|
|
175
195
|
|
|
176
|
-
|
|
196
|
+
```ruby
|
|
197
|
+
# Uso con el cliente manual
|
|
198
|
+
client = BugBunny::Client.new(pool: BUG_BUNNY_POOL) do |stack|
|
|
199
|
+
stack.use BugBunny::Middleware::RaiseError
|
|
200
|
+
stack.use BugBunny::Middleware::JsonResponse
|
|
201
|
+
end
|
|
177
202
|
|
|
178
|
-
|
|
203
|
+
# Ahora el cliente devolverá Hashes y lanzará errores si el worker falla
|
|
204
|
+
response = client.request('users/1', method: :get)
|
|
205
|
+
```
|
|
179
206
|
|
|
180
|
-
|
|
207
|
+
**Middlewares Personalizados**
|
|
208
|
+
Ideales para inyectar Auth o Headers de trazabilidad en todos los requests de un Recurso.
|
|
181
209
|
|
|
182
|
-
#### 1. Definición Inline (Rápida)
|
|
183
|
-
Ideal para inyectar headers estáticos específicos de un recurso.
|
|
184
210
|
```ruby
|
|
185
|
-
class
|
|
211
|
+
class Manager::Service < BugBunny::Resource
|
|
186
212
|
client_middleware do |stack|
|
|
187
213
|
stack.use(Class.new(BugBunny::Middleware::Base) do
|
|
188
214
|
def on_request(env)
|
|
189
|
-
env.headers['
|
|
190
|
-
env.headers['
|
|
215
|
+
env.headers['Authorization'] = "Bearer #{ENV['API_TOKEN']}"
|
|
216
|
+
env.headers['X-App-Version'] = '1.0.0'
|
|
191
217
|
end
|
|
192
218
|
end)
|
|
193
219
|
end
|
|
194
220
|
end
|
|
195
221
|
```
|
|
196
222
|
|
|
197
|
-
#### 2. Clase Reutilizable (Recomendada)
|
|
198
|
-
Si tienes lógica compartida (ej: Autenticación), define una clase y úsala en múltiples recursos.
|
|
199
|
-
|
|
200
|
-
```ruby
|
|
201
|
-
# app/middleware/auth_middleware.rb
|
|
202
|
-
class AuthMiddleware < BugBunny::Middleware::Base
|
|
203
|
-
def on_request(env)
|
|
204
|
-
env.headers['Authorization'] = "Bearer #{ENV['API_KEY']}"
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# app/models/user.rb
|
|
209
|
-
class User < BugBunny::Resource
|
|
210
|
-
client_middleware do |stack|
|
|
211
|
-
stack.use AuthMiddleware
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
#### 3. Contexto Dinámico (Pro)
|
|
217
|
-
Permite inyectar valores que cambian en cada petición (como el Usuario actual o Tenant), leyendo de variables globales thread-safe (como CurrentAttributes en Rails).
|
|
218
|
-
|
|
219
|
-
```ruby
|
|
220
|
-
# Middleware que lee el Tenant actual
|
|
221
|
-
# app/middleware/tenant_middleware.rb
|
|
222
|
-
class TenantMiddleware < BugBunny::Middleware::Base
|
|
223
|
-
def on_request(env)
|
|
224
|
-
# Ejemplo usando Rails CurrentAttributes
|
|
225
|
-
if Current.tenant_id
|
|
226
|
-
env.headers['X-Tenant-ID'] = Current.tenant_id
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
class Order < BugBunny::Resource
|
|
232
|
-
client_middleware do |stack|
|
|
233
|
-
stack.use TenantMiddleware
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
```
|
|
237
|
-
|
|
238
223
|
---
|
|
239
224
|
|
|
240
|
-
## 📡 Modo Servidor
|
|
225
|
+
## 📡 Modo Servidor: Controladores
|
|
241
226
|
|
|
242
|
-
BugBunny
|
|
227
|
+
BugBunny implementa un **Router** que despacha mensajes a controladores basándose en el header `type` (URL) y `x-http-method`.
|
|
243
228
|
|
|
244
|
-
### 1.
|
|
229
|
+
### 1. Ruteo Inteligente
|
|
230
|
+
El consumidor infiere automáticamente la acción:
|
|
245
231
|
|
|
246
|
-
|
|
232
|
+
| Verbo AMQP | Path (Header `type`) | Controlador | Acción |
|
|
233
|
+
| :--- | :--- | :--- | :--- |
|
|
234
|
+
| `GET` | `services` | `ServicesController` | `index` |
|
|
235
|
+
| `GET` | `services/123` | `ServicesController` | `show` |
|
|
236
|
+
| `POST` | `services` | `ServicesController` | `create` |
|
|
237
|
+
| `PUT` | `services/123` | `ServicesController` | `update` |
|
|
238
|
+
| `DELETE` | `services/123` | `ServicesController` | `destroy` |
|
|
239
|
+
| `POST` | `services/123/restart` | `ServicesController` | `restart` (Custom) |
|
|
240
|
+
|
|
241
|
+
### 2. El Controlador
|
|
242
|
+
Ubicación: `app/rabbit/controllers/`.
|
|
247
243
|
|
|
248
244
|
```ruby
|
|
249
|
-
# app/rabbit/controllers/services_controller.rb
|
|
250
245
|
class ServicesController < BugBunny::Controller
|
|
251
|
-
# Callbacks
|
|
252
|
-
before_action :set_service, only:
|
|
246
|
+
# Callbacks estándar
|
|
247
|
+
before_action :set_service, only: [:show, :update]
|
|
253
248
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
render status: 200, json:
|
|
249
|
+
def show
|
|
250
|
+
# Renderiza JSON que viajará de vuelta por la cola reply-to
|
|
251
|
+
render status: 200, json: { id: @service.id, state: 'running' }
|
|
257
252
|
end
|
|
258
253
|
|
|
259
|
-
# POST services
|
|
260
254
|
def create
|
|
261
|
-
# BugBunny
|
|
262
|
-
#
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
255
|
+
# BugBunny envuelve los params automáticamente (param_key)
|
|
256
|
+
# params[:service] => { name: '...', replicas: ... }
|
|
257
|
+
if Service.create(params[:service])
|
|
258
|
+
render status: 201, json: { status: 'created' }
|
|
259
|
+
else
|
|
260
|
+
render status: 422, json: { errors: 'Invalid' }
|
|
261
|
+
end
|
|
267
262
|
end
|
|
268
263
|
|
|
269
264
|
private
|
|
270
265
|
|
|
271
266
|
def set_service
|
|
272
|
-
# params[:id] se extrae
|
|
273
|
-
@service =
|
|
274
|
-
|
|
275
|
-
unless @service
|
|
276
|
-
render status: 404, json: { error: "Service not found" }
|
|
277
|
-
end
|
|
267
|
+
# params[:id] se extrae del Path
|
|
268
|
+
@service = Service.find(params[:id])
|
|
278
269
|
end
|
|
279
270
|
end
|
|
280
271
|
```
|
|
281
272
|
|
|
282
|
-
###
|
|
283
|
-
|
|
284
|
-
Puedes definir un `ApplicationController` base para manejar errores de forma centralizada y declarativa.
|
|
273
|
+
### 3. Manejo de Errores Declarativo
|
|
274
|
+
Captura excepciones y devuélvelas como códigos de estado AMQP/HTTP.
|
|
285
275
|
|
|
286
276
|
```ruby
|
|
287
|
-
# app/rabbit/controllers/application.rb
|
|
288
277
|
class ApplicationController < BugBunny::Controller
|
|
289
|
-
|
|
290
|
-
rescue_from ActiveRecord::RecordNotFound do
|
|
278
|
+
rescue_from ActiveRecord::RecordNotFound do |e|
|
|
291
279
|
render status: :not_found, json: { error: "Resource missing" }
|
|
292
280
|
end
|
|
293
281
|
|
|
294
|
-
rescue_from ActiveModel::ValidationError do |e|
|
|
295
|
-
render status: :unprocessable_entity, json: e.model.errors
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
# Catch-all (Red de seguridad)
|
|
299
282
|
rescue_from StandardError do |e|
|
|
300
283
|
BugBunny.configuration.logger.error(e)
|
|
301
|
-
render status: :internal_server_error, json: { error: "
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### 3. Namespace de Controladores (Opcional)
|
|
307
|
-
|
|
308
|
-
Por defecto, BugBunny busca los controladores dentro del módulo `Rabbit::Controllers`. Esto implica que tus archivos deben estar en `app/rabbit/controllers/`.
|
|
309
|
-
|
|
310
|
-
Si prefieres organizar tus consumidores en otro lugar (ej: dentro de un dominio específico o carpeta existente), puedes cambiar el namespace.
|
|
311
|
-
|
|
312
|
-
**Configuración:**
|
|
313
|
-
```ruby
|
|
314
|
-
# config/initializers/bug_bunny.rb
|
|
315
|
-
BugBunny.configure do |config|
|
|
316
|
-
config.controller_namespace = 'Billing::Events'
|
|
317
|
-
end
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### 4. Tabla de Ruteo (Convención)
|
|
321
|
-
|
|
322
|
-
El Router infiere la acción automáticamente:
|
|
323
|
-
|
|
324
|
-
| Verbo | URL Pattern | Controlador | Acción |
|
|
325
|
-
| :--- | :--- | :--- | :--- |
|
|
326
|
-
| `GET` | `services` | `ServicesController` | `index` |
|
|
327
|
-
| `GET` | `services/12` | `ServicesController` | `show` |
|
|
328
|
-
| `POST` | `services` | `ServicesController` | `create` |
|
|
329
|
-
| `PUT` | `services/12` | `ServicesController` | `update` |
|
|
330
|
-
| `DELETE` | `services/12` | `ServicesController` | `destroy` |
|
|
331
|
-
| `POST` | `services/12/restart` | `ServicesController` | `restart` (Custom) |
|
|
332
|
-
|
|
333
|
-
### 🔎 Observabilidad y Logging
|
|
334
|
-
|
|
335
|
-
BugBunny implementa un sistema de **Tracing Distribuido** nativo. Esto permite rastrear una petición desde que se origina en tu aplicación (Producer) hasta que es procesada por el worker (Consumer), manteniendo el mismo ID de traza (`correlation_id`) en todos los logs.
|
|
336
|
-
|
|
337
|
-
#### 1. Productor: Inyectar el Trace ID
|
|
338
|
-
|
|
339
|
-
Para asegurar que los mensajes salgan de tu aplicación con el ID de traza correcto (por ejemplo, el `X-Request-Id` de Rails, Sidekiq o tu propio `Current.request_id`), debes inyectarlo antes de publicar el mensaje.
|
|
340
|
-
|
|
341
|
-
La forma recomendada es crear un Middleware y registrarlo globalmente.
|
|
342
|
-
|
|
343
|
-
**A. Crear el Middleware**
|
|
344
|
-
|
|
345
|
-
```ruby
|
|
346
|
-
# app/middleware/correlation_injector.rb
|
|
347
|
-
class CorrelationInjector < BugBunny::Middleware::Base
|
|
348
|
-
def on_request(env)
|
|
349
|
-
# Ejemplo: Si usas Rails CurrentAttributes o similar
|
|
350
|
-
if defined?(Current) && Current.request_id
|
|
351
|
-
env.correlation_id = Current.request_id
|
|
352
|
-
end
|
|
353
|
-
end
|
|
354
|
-
end
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
**B. Registrar el Middleware (Initializer)**
|
|
358
|
-
|
|
359
|
-
```ruby
|
|
360
|
-
# config/initializers/bug_bunny.rb
|
|
361
|
-
require 'bug_bunny'
|
|
362
|
-
require_relative '../../app/middleware/correlation_injector'
|
|
363
|
-
|
|
364
|
-
# Módulo para interceptar la inicialización de cualquier cliente
|
|
365
|
-
module BugBunnyGlobalMiddleware
|
|
366
|
-
def initialize(pool:)
|
|
367
|
-
super
|
|
368
|
-
@stack.use CorrelationInjector
|
|
284
|
+
render status: :internal_server_error, json: { error: "Crash" }
|
|
369
285
|
end
|
|
370
286
|
end
|
|
371
|
-
|
|
372
|
-
# Aplicamos el parche para que afecte a Resources y Clientes manuales
|
|
373
|
-
BugBunny::Client.prepend(BugBunnyGlobalMiddleware)
|
|
374
287
|
```
|
|
375
288
|
|
|
376
289
|
---
|
|
377
290
|
|
|
378
|
-
|
|
291
|
+
## 🔎 Observabilidad y Tracing
|
|
379
292
|
|
|
380
|
-
|
|
293
|
+
> **Novedad v3.1:** BugBunny implementa Distributed Tracing nativo.
|
|
381
294
|
|
|
382
|
-
|
|
383
|
-
Al recibir un mensaje, el Consumidor realiza automáticamente los siguientes pasos:
|
|
384
|
-
1. Extrae el `correlation_id` de las propiedades AMQP (o genera un UUID si no existe).
|
|
385
|
-
2. Envuelve todo el procesamiento en un bloque de log etiquetado (`tagged logging`).
|
|
386
|
-
3. Pasa el ID al Controlador.
|
|
295
|
+
El `correlation_id` se mantiene intacto a través de toda la cadena: `Producer -> RabbitMQ -> Consumer -> Controller`.
|
|
387
296
|
|
|
388
|
-
|
|
297
|
+
### 1. Logs Automáticos (Consumer)
|
|
298
|
+
No requiere configuración. El worker envuelve la ejecución en bloques de log etiquetados con el UUID.
|
|
389
299
|
|
|
390
300
|
```text
|
|
391
|
-
[d41d8cd9
|
|
392
|
-
[d41d8cd9
|
|
301
|
+
[d41d8cd9...] [Consumer] Listening on queue...
|
|
302
|
+
[d41d8cd9...] [API] Processing ServicesController#create...
|
|
393
303
|
```
|
|
394
304
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
> **Nota:** No agregues `:uuid` aquí, ya que el Consumidor lo agrega automáticamente.
|
|
305
|
+
### 2. Logs de Negocio (Controller)
|
|
306
|
+
Inyecta contexto rico (Tenant, Usuario, IP) en los logs usando `log_tags`.
|
|
399
307
|
|
|
400
308
|
```ruby
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
'WORKER',
|
|
407
|
-
->(_) { ENV['APP_VERSION'] }
|
|
309
|
+
# app/rabbit/controllers/application_controller.rb
|
|
310
|
+
class ApplicationController < BugBunny::Controller
|
|
311
|
+
self.log_tags = [
|
|
312
|
+
->(c) { c.params[:tenant_id] }, # Agrega [Tenant-55]
|
|
313
|
+
->(c) { c.headers['X-Source'] } # Agrega [Console]
|
|
408
314
|
]
|
|
409
315
|
end
|
|
410
316
|
```
|
|
411
317
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
[d41d8cd9...] [WORKER] [v1.0.2] [API] Procesando mensaje...
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
##### C. Configuración por Controlador (Contexto Rico)
|
|
418
|
-
Para agregar información específica del mensaje o lógica de negocio (como IDs de inquilinos, usuario actual, o headers específicos), utiliza `self.log_tags` en tus controladores.
|
|
419
|
-
|
|
420
|
-
Esto aprovecha el `around_action` nativo de la gema para inyectar contexto.
|
|
318
|
+
### 3. Inyección en el Productor
|
|
319
|
+
Para que tus logs de Rails y Rabbit coincidan, usa un middleware global:
|
|
421
320
|
|
|
422
321
|
```ruby
|
|
423
|
-
#
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
self.log_tags = [
|
|
429
|
-
->(c) { c.params[:tenant_id] }, # Tag del Tenant (si viene en el body)
|
|
430
|
-
->(c) { c.headers['X-Source'] } # Tag del origen
|
|
431
|
-
]
|
|
432
|
-
end
|
|
322
|
+
# config/initializers/bug_bunny.rb
|
|
323
|
+
# Middleware para inyectar Current.request_id de Rails al mensaje Rabbit
|
|
324
|
+
class CorrelationInjector < BugBunny::Middleware::Base
|
|
325
|
+
def on_request(env)
|
|
326
|
+
env.correlation_id = Current.request_id if defined?(Current)
|
|
433
327
|
end
|
|
434
328
|
end
|
|
435
|
-
```
|
|
436
329
|
|
|
437
|
-
|
|
438
|
-
(
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
---
|
|
444
|
-
|
|
445
|
-
## 🔌 Modo Publisher (Cliente Manual)
|
|
446
|
-
|
|
447
|
-
Si necesitas enviar mensajes crudos fuera de la lógica Resource, usa `BugBunny::Client`.
|
|
448
|
-
|
|
449
|
-
```ruby
|
|
450
|
-
client = BugBunny::Client.new(pool: BUG_BUNNY_POOL)
|
|
451
|
-
|
|
452
|
-
# --- REQUEST (Síncrono / RPC) ---
|
|
453
|
-
# Espera la respuesta. Lanza BugBunny::RequestTimeout si falla.
|
|
454
|
-
response = client.request('services/123/logs',
|
|
455
|
-
method: :get,
|
|
456
|
-
exchange: 'logs_exchange',
|
|
457
|
-
timeout: 5
|
|
458
|
-
)
|
|
459
|
-
puts response['body']
|
|
460
|
-
|
|
461
|
-
# --- PUBLISH (Asíncrono / Fire-and-Forget) ---
|
|
462
|
-
# No espera respuesta.
|
|
463
|
-
client.publish('audit/events',
|
|
464
|
-
method: :post,
|
|
465
|
-
body: { event: 'login', user_id: 1 }
|
|
466
|
-
)
|
|
330
|
+
BugBunny::Client.prepend(Module.new {
|
|
331
|
+
def initialize(pool:)
|
|
332
|
+
super
|
|
333
|
+
@stack.use CorrelationInjector
|
|
334
|
+
end
|
|
335
|
+
})
|
|
467
336
|
```
|
|
468
337
|
|
|
469
|
-
### ⚠️ Consideraciones sobre RPC (Direct Reply-To)
|
|
470
|
-
|
|
471
|
-
BugBunny utiliza el mecanismo nativo `amq.rabbitmq.reply-to` para las peticiones RPC. Esto maximiza el rendimiento eliminando la necesidad de crear colas temporales por cada petición.
|
|
472
|
-
|
|
473
|
-
**Trade-off:**
|
|
474
|
-
Al usar este mecanismo, las respuestas son efímeras. Si el proceso Cliente (tu aplicación Rails/Sidekiq) se reinicia abruptamente justo después de enviar la petición pero milisegundos antes de procesar la respuesta, **esa respuesta se perderá**.
|
|
475
|
-
|
|
476
|
-
**Recomendación:**
|
|
477
|
-
Diseña tus acciones de Controlador RPC (`POST`, `PUT`) para que sean **idempotentes**.
|
|
478
|
-
* *Mal diseño:* "Crear pago" (si se reintenta, cobra doble).
|
|
479
|
-
* *Buen diseño:* "Crear pago con ID X" (si se reintenta y ya existe, devuelve el recibo existente).
|
|
480
|
-
|
|
481
|
-
Esto permite que, ante un `BugBunny::RequestTimeout` por caída del cliente, puedas reintentar la operación de forma segura.
|
|
482
|
-
|
|
483
338
|
---
|
|
484
339
|
|
|
485
|
-
##
|
|
486
|
-
|
|
487
|
-
BugBunny desacopla el transporte de la lógica usando headers estándar.
|
|
340
|
+
## 🧵 Guía de Producción
|
|
488
341
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
3. **Parametros:** `params` unifica:
|
|
492
|
-
* **Route Params:** `services/123` -> `params[:id] = 123`
|
|
493
|
-
* **Query Params:** `services?force=true` -> `params[:force] = true`
|
|
494
|
-
* **Body:** Payload JSON fusionado en el hash.
|
|
342
|
+
### Connection Pooling
|
|
343
|
+
Es vital usar `ConnectionPool` si usas servidores web multi-hilo (Puma) o workers (Sidekiq). BugBunny no gestiona hilos internamente; se apoya en el pool.
|
|
495
344
|
|
|
496
|
-
###
|
|
345
|
+
### Fork Safety
|
|
346
|
+
BugBunny incluye un `Railtie` que detecta automáticamente cuando Rails hace un "Fork" (ej: Puma en modo Cluster o Spring). Desconecta automáticamente las conexiones heredadas para evitar corrupción de datos en los sockets TCP.
|
|
497
347
|
|
|
498
|
-
|
|
348
|
+
### RPC y "Direct Reply-To"
|
|
349
|
+
Para máxima velocidad, BugBunny usa `amq.rabbitmq.reply-to`.
|
|
350
|
+
* **Trade-off:** Si el cliente (Rails) se reinicia justo después de enviar un mensaje RPC pero antes de recibir la respuesta, esa respuesta se pierde.
|
|
351
|
+
* **Recomendación:** Diseña tus acciones RPC (`POST`, `PUT`) para que sean **idempotentes** (seguras de reintentar ante un timeout).
|
|
499
352
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
```
|
|
353
|
+
### Seguridad
|
|
354
|
+
El Router incluye protecciones contra **Remote Code Execution (RCE)**. Verifica estrictamente que la clase instanciada herede de `BugBunny::Controller` antes de ejecutarla, impidiendo la inyección de clases arbitrarias de Ruby vía el header `type`.
|
|
503
355
|
|
|
504
356
|
---
|
|
505
357
|
|