bug_bunny 2.0.2 → 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 +4 -4
- data/CHANGELOG.md +23 -1
- data/README.md +222 -132
- data/bin_client.rb +51 -0
- data/bin_suite.rb +77 -0
- data/bin_worker.rb +20 -0
- data/initializer_example.rb +27 -0
- data/lib/bug_bunny/client.rb +119 -0
- data/lib/bug_bunny/config.rb +74 -3
- data/lib/bug_bunny/consumer.rb +151 -0
- data/lib/bug_bunny/controller.rb +74 -49
- data/lib/bug_bunny/exception.rb +81 -14
- data/lib/bug_bunny/middleware/json_response.rb +74 -0
- data/lib/bug_bunny/middleware/raise_error.rb +71 -0
- data/lib/bug_bunny/middleware/stack.rb +50 -0
- data/lib/bug_bunny/producer.rb +130 -0
- data/lib/bug_bunny/rabbit.rb +70 -300
- data/lib/bug_bunny/railtie.rb +54 -0
- data/lib/bug_bunny/request.rb +128 -0
- data/lib/bug_bunny/resource.rb +361 -156
- data/lib/bug_bunny/session.rb +82 -0
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +104 -16
- data/lib/generators/bug_bunny/install/install_generator.rb +48 -0
- data/lib/generators/bug_bunny/install/templates/initializer.rb +61 -0
- data/test_controller.rb +49 -0
- data/test_helper.rb +20 -0
- data/test_resource.rb +22 -0
- metadata +178 -4
- data/lib/bug_bunny/publisher.rb +0 -108
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e154bfecb70c1fc85408e87876976dfc8c64c01c2816c4f811a05e39e8432f9
|
|
4
|
+
data.tar.gz: 3b4e1508cd1a7f019fb638831c58117d00c46729b4f6bad4f8ecaf1b75914b47
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b43ec72552e6d17601665f7640aedd1e9c390463f6df5ca950b082e17316e387d0a10fa9e5ddfd3611d3f928a5d7d2af71367fa6c5409ba1f242402bd46843a1
|
|
7
|
+
data.tar.gz: cdd64f5401a653a0fe839ea2a3cf926e1af7de6e8498892bbd20876ea2ed808f97d4db1daa1a31f70c54533a00f73e834a547943d34f8a95543b1befeb5a9591
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
|
|
3
|
+
## [3.0.0] - 2026-02-05
|
|
4
|
+
|
|
5
|
+
### ⚠ Breaking Changes
|
|
6
|
+
* **Architecture Overhaul:** Complete rewrite implementing the "Active Record over AMQP" philosophy. The framework now simulates a RESTful architecture where messages contain "URLs" (via `type` header) that map to Controllers.
|
|
7
|
+
* **Resource Configuration:** Removed `routing_key_prefix` in favor of `resource_name`. By default, `resource_name` is now automatically pluralized (Rails-style) to determine routing keys and headers.
|
|
8
|
+
* **Schema-less Resources:** Removed strict `ActiveModel::Attributes` dependency. `BugBunny::Resource` no longer requires defining attributes manually. It now uses a dynamic storage (`@remote_attributes`) supporting arbitrary keys (including PascalCase for Docker APIs) via `method_missing`.
|
|
9
|
+
|
|
10
|
+
### 🚀 New Features
|
|
11
|
+
* **Middleware Stack:** Implemented an "Onion Architecture" for the `Client` similar to Faraday. Added support for middleware chains to intercept requests/responses.
|
|
12
|
+
* `Middleware::JsonResponse`: Automatically parses JSON bodies and provides `IndifferentAccess`.
|
|
13
|
+
* `Middleware::RaiseError`: Maps AMQP/HTTP status codes to Ruby exceptions (e.g., 404 to `BugBunny::NotFound`).
|
|
14
|
+
* **REST-over-AMQP Routing:** The `Consumer` now parses the `type` header as a URL (e.g., `users/show/12?active=true`) to dispatch actions to specific Controllers.
|
|
15
|
+
* **Direct Reply-To RPC:** Optimized RPC calls to use RabbitMQ's native `amq.rabbitmq.reply-to` feature, eliminating the need for temporary reply queues and improving performance.
|
|
16
|
+
* **Config Inheritance:** `BugBunny::Resource` configurations (like `connection_pool`, `exchange`) are now inherited by child classes, simplifying setup for groups of models.
|
|
17
|
+
|
|
18
|
+
### 🛠 Improvements
|
|
19
|
+
* **Connection Pooling:** Full integration with `connection_pool` to ensure thread safety in multi-threaded environments (Puma/Sidekiq).
|
|
20
|
+
* **Error Handling:** Unified exception hierarchy under `BugBunny::Error`, with specific classes for Client (4xx) and Server (5xx) errors.
|
|
21
|
+
* **Rails Integration:** Added `Railtie` with hooks for Puma and Spring to safely handle connection forks.
|
|
22
|
+
* **Documentation:** Added comprehensive YARD documentation for all core classes.
|
|
23
|
+
|
|
2
24
|
## Version 0.1.0
|
|
3
|
-
Migration bunny logic from utils
|
|
25
|
+
* Migration bunny logic from utils
|
data/README.md
CHANGED
|
@@ -1,183 +1,273 @@
|
|
|
1
|
-
# BugBunny
|
|
1
|
+
# 🐰 BugBunny
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**BugBunny** es un framework RPC para Ruby on Rails sobre **RabbitMQ**.
|
|
4
|
+
|
|
5
|
+
Su filosofía es **"Active Record over AMQP"**. Abstrae la complejidad de colas y exchanges transformando patrones de mensajería en una arquitectura **RESTful simulada**, donde los mensajes contienen "URLs" y "Query Params" que son enrutados automáticamente a controladores.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 📦 Instalación
|
|
10
|
+
|
|
11
|
+
Agrega la gema a tu `Gemfile`:
|
|
4
12
|
|
|
5
13
|
```ruby
|
|
6
|
-
|
|
7
|
-
BugBunny.configure do |config|
|
|
8
|
-
config.host = 'Host'
|
|
9
|
-
config.username = 'Username'
|
|
10
|
-
config.password = 'Password'
|
|
11
|
-
config.vhost = '/'
|
|
12
|
-
config.logger = Rails.logger
|
|
13
|
-
config.automatically_recover = false
|
|
14
|
-
config.network_recovery_interval = 5
|
|
15
|
-
config.connection_timeout = 10
|
|
16
|
-
config.read_timeout = 30
|
|
17
|
-
config.write_timeout = 30
|
|
18
|
-
config.heartbeat = 15
|
|
19
|
-
config.continuation_timeout = 15_000
|
|
20
|
-
end
|
|
14
|
+
gem 'bug_bunny'
|
|
21
15
|
```
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
### Rutas
|
|
17
|
+
Ejecuta el bundle:
|
|
26
18
|
|
|
19
|
+
```bash
|
|
20
|
+
bundle install
|
|
27
21
|
```
|
|
28
|
-
# config/rabbit_rest.yml
|
|
29
|
-
default: &default
|
|
30
|
-
healt_check:
|
|
31
|
-
up: 'healt_check/up'
|
|
32
|
-
manager:
|
|
33
|
-
services:
|
|
34
|
-
index: 'services/index'
|
|
35
|
-
create: 'services/create'
|
|
36
|
-
show: 'services/%<id>s/show'
|
|
37
|
-
update: 'services/%<id>s/update'
|
|
38
|
-
destroy: 'services/%<id>s/destroy'
|
|
39
|
-
swarm:
|
|
40
|
-
info: 'swarm/info'
|
|
41
|
-
version: 'swarm/version'
|
|
42
|
-
swarm: 'swarm/swarm'
|
|
43
|
-
tasks:
|
|
44
|
-
index: 'tasks/index'
|
|
45
|
-
|
|
46
|
-
development:
|
|
47
|
-
<<: *default
|
|
48
|
-
|
|
49
|
-
test:
|
|
50
|
-
<<: *default
|
|
51
|
-
|
|
52
|
-
production:
|
|
53
|
-
<<: *default
|
|
54
22
|
|
|
23
|
+
Corre el instalador para generar la configuración:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
rails g bug_bunny:install
|
|
55
27
|
```
|
|
56
28
|
|
|
57
|
-
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## ⚙️ Configuración
|
|
32
|
+
|
|
33
|
+
Configura tus credenciales y el Pool de conexiones en el inicializador.
|
|
58
34
|
|
|
59
35
|
```ruby
|
|
60
36
|
# config/initializers/bug_bunny.rb
|
|
61
|
-
BUG_BUNNY_ENDPOINTS = Rails.application.config_for(:rabbit_rest)
|
|
62
37
|
|
|
63
|
-
|
|
64
|
-
|
|
38
|
+
BugBunny.configure do |config|
|
|
39
|
+
config.host = ENV.fetch('RABBITMQ_HOST', 'localhost')
|
|
40
|
+
config.username = ENV.fetch('RABBITMQ_USER', 'guest')
|
|
41
|
+
config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
|
|
42
|
+
config.vhost = ENV.fetch('RABBITMQ_VHOST', '/')
|
|
43
|
+
|
|
44
|
+
# Timeouts y Recuperación
|
|
45
|
+
config.rpc_timeout = 10 # Segundos a esperar respuesta síncrona
|
|
46
|
+
config.network_recovery_interval = 5
|
|
65
47
|
end
|
|
48
|
+
|
|
49
|
+
# Definimos el Pool Global (Vital para Puma/Sidekiq)
|
|
50
|
+
BUG_BUNNY_POOL = ConnectionPool.new(size: ENV.fetch('RAILS_MAX_THREADS', 5).to_i, timeout: 5) do
|
|
51
|
+
BugBunny.create_connection
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Inyectamos el pool por defecto a los recursos
|
|
55
|
+
BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
|
|
66
56
|
```
|
|
67
57
|
|
|
68
|
-
|
|
58
|
+
---
|
|
69
59
|
|
|
70
|
-
|
|
60
|
+
## 🚀 Modo Resource (ORM / Active Record)
|
|
71
61
|
|
|
72
|
-
|
|
62
|
+
Define modelos que actúan como proxis de recursos remotos. BugBunny separa la **Lógica de Transporte** (RabbitMQ) de la **Lógica de Aplicación** (Controladores).
|
|
73
63
|
|
|
64
|
+
### Escenario A: Routing Dinámico (Topic / Estándar)
|
|
65
|
+
Ideal cuando quieres enrutar por acción. La Routing Key se genera automáticamente usando `resource_name.action`.
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
class RemoteUser < BugBunny::Resource
|
|
69
|
+
# --- Configuración ---
|
|
70
|
+
self.exchange = 'app.topic'
|
|
71
|
+
self.exchange_type = 'topic'
|
|
72
|
+
|
|
73
|
+
# Define el nombre lógico del recurso.
|
|
74
|
+
# 1. Routing Key: 'users.create', 'users.show.12'
|
|
75
|
+
# 2. Header Type: 'users/create', 'users/show/12'
|
|
76
|
+
self.resource_name = 'users'
|
|
77
|
+
|
|
78
|
+
# No necesitas definir atributos, BugBunny soporta atributos dinámicos (Schema-less)
|
|
79
|
+
end
|
|
74
80
|
```
|
|
75
|
-
class Rabbit::Publisher::Manager < BugBunny::Publisher
|
|
76
|
-
ROUTING_KEY = :manager
|
|
77
|
-
ROUTES = BUG_BUNNY_ENDPOINTS[:manager][:swarm]
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
obj.publish_and_consume!
|
|
82
|
-
end
|
|
82
|
+
### Escenario B: Routing Estático (Direct / Cola Dedicada)
|
|
83
|
+
Ideal cuando quieres enviar todo a una cola específica (ej: un Manager), independientemente de la acción.
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
```ruby
|
|
86
|
+
class BoxManager < BugBunny::Resource
|
|
87
|
+
# --- Configuración ---
|
|
88
|
+
self.exchange = 'warehouse.direct'
|
|
89
|
+
self.exchange_type = 'direct'
|
|
90
|
+
|
|
91
|
+
# FORZAMOS LA ROUTING KEY.
|
|
92
|
+
# Todo viaja con esta key, sin importar la acción.
|
|
93
|
+
self.routing_key = 'manager_queue'
|
|
94
|
+
|
|
95
|
+
# Define el nombre lógico para el Controlador.
|
|
96
|
+
# Header Type: 'box_manager/create', 'box_manager/show/12'
|
|
97
|
+
self.resource_name = 'box_manager'
|
|
88
98
|
end
|
|
89
99
|
```
|
|
90
100
|
|
|
91
|
-
|
|
101
|
+
### Consumiendo el Servicio (CRUD)
|
|
102
|
+
|
|
103
|
+
La API simula ActiveRecord. Por debajo, construye una "URL" en el header `type` para que el consumidor sepa qué hacer.
|
|
92
104
|
|
|
105
|
+
```ruby
|
|
106
|
+
# --- READ (Colección con Filtros) ---
|
|
107
|
+
# Header Type: "users/index?active=true" (Query Params)
|
|
108
|
+
# Routing Key: "users.index" (Dinámico) o "manager_queue" (Estático)
|
|
109
|
+
users = RemoteUser.where(active: true)
|
|
110
|
+
|
|
111
|
+
# --- READ (Singular) ---
|
|
112
|
+
# Header Type: "users/show/123" (ID en Path)
|
|
113
|
+
# Routing Key: "users.show.123" (Dinámico) o "manager_queue" (Estático)
|
|
114
|
+
user = RemoteUser.find(123)
|
|
115
|
+
puts user.name # Acceso dinámico a atributos
|
|
116
|
+
|
|
117
|
+
# --- CREATE ---
|
|
118
|
+
# Header Type: "users/create"
|
|
119
|
+
user = RemoteUser.create(email: "test@test.com")
|
|
120
|
+
|
|
121
|
+
# --- UPDATE ---
|
|
122
|
+
# Header Type: "users/update/123"
|
|
123
|
+
user.update(email: "edit@test.com")
|
|
124
|
+
# Dirty Tracking: Solo se envían los atributos modificados.
|
|
125
|
+
|
|
126
|
+
# --- DESTROY ---
|
|
127
|
+
# Header Type: "users/destroy/123"
|
|
128
|
+
user.destroy
|
|
93
129
|
```
|
|
94
|
-
class Rabbit::Publisher::Manager < BugBunny::Publisher
|
|
95
|
-
ROUTING_KEY = :manager
|
|
96
|
-
ROUTES = BUG_BUNNY_ENDPOINTS[:manager][:swarm]
|
|
97
130
|
|
|
98
|
-
|
|
99
|
-
obj = new(pool: NEW_BUNNY_POOL, exchange_name: exchange, action: self::ROUTES[:info], message: message)
|
|
100
|
-
obj.publish!
|
|
101
|
-
end
|
|
131
|
+
---
|
|
102
132
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
133
|
+
## 🔌 Modo Publisher (Cliente Manual)
|
|
134
|
+
|
|
135
|
+
Si no necesitas mapear un recurso o quieres enviar mensajes crudos ("Fire-and-Forget"), puedes usar `BugBunny::Client` directamente.
|
|
136
|
+
|
|
137
|
+
### 1. Instanciar el Cliente
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
# Puedes inyectar middlewares personalizados aquí si lo deseas
|
|
141
|
+
client = BugBunny::Client.new(pool: BUG_BUNNY_POOL) do |conn|
|
|
142
|
+
conn.use BugBunny::Middleware::JsonResponse
|
|
107
143
|
end
|
|
108
144
|
```
|
|
109
145
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
- content_type
|
|
113
|
-
- content_encoding
|
|
114
|
-
- correlation_id
|
|
115
|
-
- reply_to
|
|
116
|
-
- message_id
|
|
117
|
-
- timestamp
|
|
118
|
-
- priority
|
|
119
|
-
- expiration
|
|
120
|
-
- user_id
|
|
121
|
-
- app_id
|
|
122
|
-
- action
|
|
123
|
-
- aguments
|
|
124
|
-
- cluster_id
|
|
125
|
-
- persistent
|
|
126
|
-
- expiration
|
|
127
|
-
|
|
128
|
-
## Consumer
|
|
146
|
+
### 2. Publicar Asíncronamente (Fire-and-Forget)
|
|
147
|
+
Envía el mensaje y retorna inmediatamente. Ideal para eventos o logs.
|
|
129
148
|
|
|
149
|
+
```ruby
|
|
150
|
+
# publish(url_logica, opciones)
|
|
151
|
+
client.publish('notifications/alert',
|
|
152
|
+
exchange: 'events.topic',
|
|
153
|
+
exchange_type: 'topic',
|
|
154
|
+
routing_key: 'alerts.critical',
|
|
155
|
+
body: { message: 'CPU High', server: 'web-1' }
|
|
156
|
+
)
|
|
130
157
|
```
|
|
131
|
-
|
|
158
|
+
|
|
159
|
+
### 3. Petición Síncrona (RPC)
|
|
160
|
+
Envía el mensaje y bloquea el hilo esperando la respuesta del consumidor.
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
begin
|
|
164
|
+
# request(url_logica, opciones)
|
|
165
|
+
response = client.request('math/calculate',
|
|
166
|
+
exchange: 'rpc.direct',
|
|
167
|
+
routing_key: 'calculator',
|
|
168
|
+
body: { a: 10, b: 20 },
|
|
169
|
+
timeout: 5 # Segundos de espera máxima
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
puts response['body'] # => { "result": 30 }
|
|
173
|
+
|
|
174
|
+
rescue BugBunny::RequestTimeout
|
|
175
|
+
puts "El servidor tardó demasiado."
|
|
132
176
|
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
133
180
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
181
|
+
## 📡 Modo Servidor (El Worker)
|
|
182
|
+
|
|
183
|
+
BugBunny incluye un **Router Inteligente** que parsea el header `type` (la URL simulada), extrae parámetros y despacha al controlador.
|
|
184
|
+
|
|
185
|
+
### 1. Definir Controladores
|
|
186
|
+
|
|
187
|
+
Crea tus controladores en `app/rabbit/controllers/`. Heredan de `BugBunny::Controller`.
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
# app/rabbit/controllers/users_controller.rb
|
|
191
|
+
class UsersController < BugBunny::Controller
|
|
192
|
+
|
|
193
|
+
# Acción para type: "users/index?active=true"
|
|
194
|
+
def index
|
|
195
|
+
# params fusiona Query Params y Body
|
|
196
|
+
users = User.where(active: params[:active])
|
|
197
|
+
render status: 200, json: users
|
|
137
198
|
end
|
|
138
199
|
|
|
139
|
-
|
|
140
|
-
|
|
200
|
+
# Acción para type: "users/show/12"
|
|
201
|
+
def show
|
|
202
|
+
# params[:id] se extrae automáticamente del Path de la URL
|
|
203
|
+
user = User.find_by(id: params[:id])
|
|
204
|
+
|
|
205
|
+
if user
|
|
206
|
+
render status: 200, json: user
|
|
207
|
+
else
|
|
208
|
+
render status: 404, json: { error: 'Not Found' }
|
|
209
|
+
end
|
|
141
210
|
end
|
|
142
211
|
|
|
143
|
-
|
|
144
|
-
|
|
212
|
+
# Acción para type: "users/create"
|
|
213
|
+
def create
|
|
214
|
+
user = User.new(params)
|
|
215
|
+
if user.save
|
|
216
|
+
render status: 201, json: user
|
|
217
|
+
else
|
|
218
|
+
# Estos errores se propagan como BugBunny::UnprocessableEntity
|
|
219
|
+
render status: 422, json: { errors: user.errors }
|
|
220
|
+
end
|
|
145
221
|
end
|
|
146
222
|
end
|
|
147
|
-
|
|
148
223
|
```
|
|
149
224
|
|
|
150
|
-
|
|
151
|
-
Solo para recursos que se adaptan al crud de rails estoy utilizando automaticamente la logica de los publicadores. Los atributos solo se ponen si son necesarios, si no la dejas vacia y actua igual que active resource.
|
|
225
|
+
### 2. Ejecutar el Worker
|
|
152
226
|
|
|
227
|
+
```bash
|
|
228
|
+
bundle exec rake bug_bunny:work
|
|
153
229
|
```
|
|
154
|
-
class Manager::Application < BugBunny::Resource
|
|
155
|
-
self.resource_path = 'rabbit/publisher/manager'
|
|
156
|
-
|
|
157
|
-
attribute :id # 'ID'
|
|
158
|
-
attribute :version # 'Version'
|
|
159
|
-
attribute :created_at # 'CreatedAt'
|
|
160
|
-
attribute :update_at # 'UpdatedAt'
|
|
161
|
-
attribute :spec # 'Spec'
|
|
162
|
-
end
|
|
163
230
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 🏗 Arquitectura REST-over-AMQP
|
|
234
|
+
|
|
235
|
+
BugBunny desacopla el transporte de la lógica usando headers.
|
|
236
|
+
|
|
237
|
+
| Concepto | REST (HTTP) | BugBunny (AMQP) | Configuración |
|
|
238
|
+
| :--- | :--- | :--- | :--- |
|
|
239
|
+
| **Endpoint** | URL Path (`/users/1`) | Header `type` (`users/show/1`) | `resource_name` |
|
|
240
|
+
| **Filtros** | Query String (`?active=true`) | Header `type` (`users/index?active=true`) | Automático (`where`) |
|
|
241
|
+
| **Destino Físico** | IP/Dominio | Routing Key (`users.create` o `manager`) | `routing_key` (Estático) o `resource_name` (Dinámico) |
|
|
242
|
+
| **Payload** | Body (JSON) | Body (JSON) | N/A |
|
|
243
|
+
| **Status** | HTTP Code (200, 404) | JSON Response `status` | N/A |
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## 🛠 Middlewares
|
|
248
|
+
|
|
249
|
+
BugBunny usa una pila de middlewares para procesar respuestas.
|
|
167
250
|
|
|
251
|
+
```ruby
|
|
252
|
+
# Configuración global en el Resource
|
|
253
|
+
BugBunny::Resource.client_middleware do |conn|
|
|
254
|
+
# 1. Lanza excepciones Ruby para errores 4xx/5xx
|
|
255
|
+
conn.use BugBunny::Middleware::RaiseError
|
|
256
|
+
|
|
257
|
+
# 2. Parsea JSON a HashWithIndifferentAccess
|
|
258
|
+
conn.use BugBunny::Middleware::JsonResponse
|
|
259
|
+
end
|
|
168
260
|
```
|
|
169
261
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
- `BugBunny::ResponseError::UnprocessableEntity`: En este el error viene el error details a lo rails.
|
|
183
|
-
- `BugBunny::ResponseError::InternalServerError`
|
|
262
|
+
### Excepciones
|
|
263
|
+
|
|
264
|
+
* `BugBunny::UnprocessableEntity` (422): Error de validación.
|
|
265
|
+
* `BugBunny::NotFound` (404): Recurso no encontrado.
|
|
266
|
+
* `BugBunny::RequestTimeout`: Timeout RPC.
|
|
267
|
+
* `BugBunny::CommunicationError`: Fallo de red RabbitMQ.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## 📄 Licencia
|
|
272
|
+
|
|
273
|
+
Código abierto bajo [MIT License](https://opensource.org/licenses/MIT).
|
data/bin_client.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# bin_client.rb
|
|
2
|
+
require_relative 'lib/bug_bunny'
|
|
3
|
+
$stdout.sync = true # <--- Agrega esto
|
|
4
|
+
|
|
5
|
+
# 1. Configuración
|
|
6
|
+
BugBunny.configure do |config|
|
|
7
|
+
config.host = 'localhost'
|
|
8
|
+
config.username = 'wisproMQ'
|
|
9
|
+
config.password = 'wisproMQ'
|
|
10
|
+
config.vhost = 'sync.devel'
|
|
11
|
+
config.logger = Logger.new(STDOUT)
|
|
12
|
+
config.rpc_timeout = 5
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# 2. Pool
|
|
16
|
+
POOL = ConnectionPool.new(size: 2, timeout: 5) do
|
|
17
|
+
BugBunny.create_connection
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# 3. Cliente
|
|
21
|
+
client = BugBunny.new(pool: POOL)
|
|
22
|
+
|
|
23
|
+
# --- PRUEBA 1: Publish ---
|
|
24
|
+
puts "\n[1] Enviando mensaje asíncrono (Publish)..."
|
|
25
|
+
|
|
26
|
+
# AGREGADO: exchange_type: 'topic'
|
|
27
|
+
client.publish('test/ping', exchange: 'test_exchange', exchange_type: 'topic', routing_key: 'test.ping') do |req|
|
|
28
|
+
req.body = { msg: 'Hola, soy invisible' }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
puts " -> Enviado."
|
|
32
|
+
sleep 1
|
|
33
|
+
|
|
34
|
+
# --- PRUEBA 2: RPC ---
|
|
35
|
+
puts "\n[2] Enviando petición síncrona (Request)..."
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
# AGREGADO: exchange_type: 'topic'
|
|
39
|
+
response = client.request('test/123/ping', exchange: 'test_exchange', exchange_type: 'topic', routing_key: 'test.ping') do |req|
|
|
40
|
+
req.body = { data: 'Importante' }
|
|
41
|
+
req.timeout = 3
|
|
42
|
+
req.headers['X-Source'] = 'Terminal'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
puts " -> ✅ RESPUESTA RECIBIDA:"
|
|
46
|
+
puts " Status: #{response['status']}"
|
|
47
|
+
puts " Body: #{response['body']}"
|
|
48
|
+
|
|
49
|
+
rescue BugBunny::RequestTimeout
|
|
50
|
+
puts " -> ❌ Error: Timeout esperando respuesta."
|
|
51
|
+
end
|
data/bin_suite.rb
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# bin_suite.rb
|
|
2
|
+
require_relative 'test_helper'
|
|
3
|
+
require_relative 'test_resource' # Cargamos la clase TestUser
|
|
4
|
+
|
|
5
|
+
# Cliente "Raw" para pruebas manuales
|
|
6
|
+
raw_client = BugBunny.new(pool: TEST_POOL)
|
|
7
|
+
|
|
8
|
+
def assert(condition, msg)
|
|
9
|
+
if condition
|
|
10
|
+
puts " ✅ PASS: #{msg}"
|
|
11
|
+
else
|
|
12
|
+
puts " ❌ FAIL: #{msg}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
puts "\n🔎 --- INICIANDO SUITE DE PRUEBAS BUG BUNNY ---"
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------
|
|
19
|
+
# TEST 1: RPC Manual (Raw Client)
|
|
20
|
+
# ---------------------------------------------------------
|
|
21
|
+
puts "\n[1] Probando RPC Manual (Client#request)..."
|
|
22
|
+
begin
|
|
23
|
+
# Notar la routing key: test_user.ping
|
|
24
|
+
response = raw_client.request('test_user/ping', exchange: 'test_exchange', exchange_type: 'topic', routing_key: 'test_user.ping')
|
|
25
|
+
assert(response['body']['message'] == 'Pong!', "Respuesta RPC recibida correctamente")
|
|
26
|
+
rescue => e
|
|
27
|
+
assert(false, "Error RPC: #{e.message}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------
|
|
31
|
+
# TEST 2: Resource Finder (ORM)
|
|
32
|
+
# ---------------------------------------------------------
|
|
33
|
+
puts "\n[2] Probando BugBunny::Resource (Estilo Rails)..."
|
|
34
|
+
|
|
35
|
+
# YA NO NECESITAS with_scope
|
|
36
|
+
puts " -> Buscando usuario ID 123..."
|
|
37
|
+
user = TestUser.find(123)
|
|
38
|
+
|
|
39
|
+
assert(user.is_a?(TestUser), "El objeto retornado es un TestUser")
|
|
40
|
+
assert(user.name == "Gabriel", "El nombre cargó correctamente")
|
|
41
|
+
assert(user.persisted?, "El objeto figura como persistido")
|
|
42
|
+
# ---------------------------------------------------------
|
|
43
|
+
# TEST 3: Resource Create (ORM)
|
|
44
|
+
# ---------------------------------------------------------
|
|
45
|
+
puts "\n[3] Probando Resource Creation..."
|
|
46
|
+
puts " -> Creando usuario nuevo..."
|
|
47
|
+
new_user = TestUser.create(name: "Nuevo User", email: "new@test.com")
|
|
48
|
+
assert(new_user.persisted?, "El usuario se guardó y recibió ID")
|
|
49
|
+
assert(new_user.id.present?, "Tiene ID asignado por el worker (#{new_user.id})")
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------
|
|
52
|
+
# TEST 4: Validaciones Locales
|
|
53
|
+
# ---------------------------------------------------------
|
|
54
|
+
puts "\n[4] Probando Validaciones Locales..."
|
|
55
|
+
invalid_user = TestUser.new(email: "sin_nombre@test.com")
|
|
56
|
+
assert(invalid_user.valid? == false, "Usuario sin nombre es inválido")
|
|
57
|
+
assert(invalid_user.errors[:name].any?, "Tiene error en el campo :name")
|
|
58
|
+
|
|
59
|
+
puts "\n🏁 SUITE FINALIZADA"
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------
|
|
62
|
+
# TEST 5: Probando Configuración Dinámica (.with)...
|
|
63
|
+
# ---------------------------------------------------------
|
|
64
|
+
puts "\n[5] Probando Configuración Dinámica (.with)..."
|
|
65
|
+
|
|
66
|
+
# Probamos cambiar el routing key prefix temporalmente
|
|
67
|
+
# El worker escucha 'test_user.*', así que si cambiamos a 'bad_prefix', debería fallar o no encontrar nada
|
|
68
|
+
begin
|
|
69
|
+
# Forzamos una routing key que no existe para ver si respeta el cambio
|
|
70
|
+
TestUser.with(routing_key: 'ruta.incorrecta').find(123)
|
|
71
|
+
rescue BugBunny::RequestTimeout
|
|
72
|
+
puts " ✅ PASS: El override funcionó (timeout esperado en ruta incorrecta)"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Probamos que vuelve a la normalidad
|
|
76
|
+
user = TestUser.find(123)
|
|
77
|
+
assert(user.present?, " ✅ PASS: La configuración volvió a la normalidad")
|
data/bin_worker.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# bin_worker.rb
|
|
2
|
+
require_relative 'test_helper'
|
|
3
|
+
require_relative 'test_controller'
|
|
4
|
+
|
|
5
|
+
puts "🐰 WORKER INICIADO (Exchange: Topic)..."
|
|
6
|
+
|
|
7
|
+
# Creamos la conexión (o usamos una del pool si quisieras)
|
|
8
|
+
connection = BugBunny.create_connection
|
|
9
|
+
|
|
10
|
+
# Usamos el método de clase directo.
|
|
11
|
+
# Al no pasar 'block: false', esto bloqueará la ejecución aquí mismo eternamente.
|
|
12
|
+
BugBunny::Consumer.subscribe(
|
|
13
|
+
connection: connection,
|
|
14
|
+
queue_name: 'test_users_queue',
|
|
15
|
+
exchange_name: 'test_exchange',
|
|
16
|
+
exchange_type: 'topic',
|
|
17
|
+
routing_key: 'test_user.#'
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# ¡Ya no necesitas el loop! El subscribe mantiene vivo el proceso.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# config/initializers/bug_bunny.rb
|
|
2
|
+
|
|
3
|
+
# 1. Configuración Global de Conexión (Credenciales)
|
|
4
|
+
BugBunny.configure do |config|
|
|
5
|
+
config.host = ENV.fetch('RABBITMQ_HOST', 'localhost')
|
|
6
|
+
config.vhost = ENV.fetch('RABBITMQ_VHOST', '/')
|
|
7
|
+
config.username = ENV.fetch('RABBITMQ_USER', 'guest')
|
|
8
|
+
config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
|
|
9
|
+
config.logger = Rails.logger
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# 2. Crear el Pool de Conexiones (Vital para Puma/Sidekiq)
|
|
13
|
+
# Usamos una constante global o un singleton para mantener el pool vivo
|
|
14
|
+
BUG_BUNNY_POOL = ConnectionPool.new(size: ENV.fetch('RAILS_MAX_THREADS', 5).to_i, timeout: 5) do
|
|
15
|
+
BugBunny.create_connection
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 3. Inyectar el Pool en los Recursos
|
|
19
|
+
# Así todos tus modelos (User, Invoice, etc.) usan este pool por defecto
|
|
20
|
+
BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
|
|
21
|
+
|
|
22
|
+
# 4. (Opcional) Configurar Middlewares Globales
|
|
23
|
+
# Si en el futuro quieres que BugBunny::Resource use middlewares (logs, tracing),
|
|
24
|
+
# podrías inyectar un cliente pre-configurado en lugar del pool directo.
|
|
25
|
+
# Por ahora, con el pool es suficiente para la v1.0.
|
|
26
|
+
|
|
27
|
+
puts "🐰 BugBunny inicializado con Pool de #{BUG_BUNNY_POOL.size} conexiones."
|