bug_bunny 4.8.0 → 4.8.1
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/.agents/skills/documentation-writer/SKILL.md +45 -0
- data/.agents/skills/gem-release/SKILL.md +114 -0
- data/.agents/skills/quality-code/SKILL.md +51 -0
- data/.agents/skills/sentry/SKILL.md +135 -0
- data/.agents/skills/sentry/references/api-endpoints.md +147 -0
- data/.agents/skills/sentry/scripts/sentry.rb +194 -0
- data/.agents/skills/skill-builder/SKILL.md +232 -0
- data/.agents/skills/skill-manager/SKILL.md +172 -0
- data/.agents/skills/skill-manager/scripts/sync.rb +310 -0
- data/.agents/skills/yard/SKILL.md +311 -0
- data/.agents/skills/yard/references/tipos.md +144 -0
- data/CHANGELOG.md +8 -0
- data/CLAUDE.md +28 -231
- data/lib/bug_bunny/version.rb +1 -1
- data/skill/SKILL.md +230 -0
- data/skill/references/client-middleware.md +144 -0
- data/skill/references/consumer.md +104 -0
- data/skill/references/controller.md +105 -0
- data/skill/references/errores.md +97 -0
- data/skill/references/resource.md +116 -0
- data/skill/references/routing.md +82 -0
- data/skill/references/testing.md +138 -0
- data/skills.lock +24 -0
- data/skills.yml +19 -0
- metadata +24 -28
- data/.claude/commands/gem-ai-setup.md +0 -174
- data/.claude/commands/pr.md +0 -53
- data/.claude/commands/release.md +0 -52
- data/.claude/commands/rubocop.md +0 -22
- data/.claude/commands/service-ai-setup.md +0 -168
- data/.claude/commands/test.md +0 -28
- data/.claude/commands/yard.md +0 -46
- data/docs/_index.md +0 -50
- data/docs/ai/_index.md +0 -56
- data/docs/ai/antipatterns.md +0 -166
- data/docs/ai/api.md +0 -251
- data/docs/ai/architecture.md +0 -92
- data/docs/ai/errors.md +0 -158
- data/docs/ai/faq_external.md +0 -133
- data/docs/ai/faq_internal.md +0 -86
- data/docs/ai/glossary.md +0 -45
- data/docs/concepts.md +0 -140
- data/docs/howto/controller.md +0 -194
- data/docs/howto/middleware_client.md +0 -119
- data/docs/howto/middleware_consumer.md +0 -127
- data/docs/howto/rails.md +0 -214
- data/docs/howto/resource.md +0 -200
- data/docs/howto/routing.md +0 -133
- data/docs/howto/testing.md +0 -259
- data/docs/howto/tracing.md +0 -119
data/docs/ai/api.md
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
## Public API
|
|
2
|
-
|
|
3
|
-
### BugBunny.configure
|
|
4
|
-
|
|
5
|
-
```ruby
|
|
6
|
-
BugBunny.configure do |config|
|
|
7
|
-
config.host = 'localhost' # String, required
|
|
8
|
-
config.port = 5672 # Integer 1..65535, required
|
|
9
|
-
config.username = 'guest' # String, required
|
|
10
|
-
config.password = 'guest' # String, required
|
|
11
|
-
config.vhost = '/' # String, required
|
|
12
|
-
|
|
13
|
-
config.rpc_timeout = 10 # Integer 1..3600, seconds
|
|
14
|
-
config.channel_prefetch = 1 # Integer 1..10000
|
|
15
|
-
config.max_reconnect_attempts = nil # nil = infinite
|
|
16
|
-
config.max_reconnect_interval = 60 # seconds, backoff ceiling
|
|
17
|
-
config.network_recovery_interval = 5 # seconds, backoff base
|
|
18
|
-
|
|
19
|
-
config.exchange_options = { durable: true }
|
|
20
|
-
config.queue_options = { durable: true }
|
|
21
|
-
|
|
22
|
-
config.logger = Rails.logger
|
|
23
|
-
config.health_check_file = Rails.root.join('tmp/bug_bunny_health').to_s
|
|
24
|
-
|
|
25
|
-
config.controller_namespace = 'Rabbit::Controllers'
|
|
26
|
-
|
|
27
|
-
# Trace propagation hooks
|
|
28
|
-
config.rpc_reply_headers = -> { { 'X-Trace-Id' => Tracer.current_header } }
|
|
29
|
-
config.on_rpc_reply = ->(headers) { Tracer.hydrate(headers['X-Trace-Id']) }
|
|
30
|
-
end
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
`validate!` is called automatically at the end of `configure`. Raises `BugBunny::ConfigurationError` on invalid values.
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
### BugBunny.routes
|
|
38
|
-
|
|
39
|
-
```ruby
|
|
40
|
-
BugBunny.routes.draw do
|
|
41
|
-
resources :users # index, show, create, update, destroy
|
|
42
|
-
resources :orders do
|
|
43
|
-
member { post :cancel } # POST orders/:id/cancel
|
|
44
|
-
collection { get :pending } # GET orders/pending
|
|
45
|
-
end
|
|
46
|
-
namespace :admin do
|
|
47
|
-
resources :reports # Admin::Controllers::ReportsController
|
|
48
|
-
end # path prefix: admin/reports
|
|
49
|
-
end
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
**recognize:**
|
|
53
|
-
```ruby
|
|
54
|
-
BugBunny.routes.recognize('GET', '/users/42')
|
|
55
|
-
# => { controller: 'users', action: 'show', params: { 'id' => '42' }, namespace: nil }
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
### BugBunny::Controller
|
|
61
|
-
|
|
62
|
-
Controllers live in the namespace configured by `config.controller_namespace` (default: `BugBunny::Controllers`).
|
|
63
|
-
|
|
64
|
-
```ruby
|
|
65
|
-
class UsersController < BugBunny::Controller
|
|
66
|
-
before_action :authenticate!, only: [:create, :update, :destroy]
|
|
67
|
-
after_action :emit_audit_event, only: [:create, :update, :destroy]
|
|
68
|
-
around_action :wrap_transaction, only: [:create]
|
|
69
|
-
|
|
70
|
-
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
|
|
71
|
-
|
|
72
|
-
def index
|
|
73
|
-
render status: :ok, json: User.all
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def show
|
|
77
|
-
user = User.find(params[:id])
|
|
78
|
-
render status: :ok, json: user
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def create
|
|
82
|
-
user = User.new(user_params)
|
|
83
|
-
if user.save
|
|
84
|
-
render status: :created, json: user
|
|
85
|
-
else
|
|
86
|
-
render status: :unprocessable_entity, json: { errors: user.errors }
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
private
|
|
91
|
-
|
|
92
|
-
def authenticate!
|
|
93
|
-
render status: :unauthorized, json: { error: 'Unauthorized' } unless valid_token?
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def render_not_found(e)
|
|
97
|
-
render status: :not_found, json: { error: e.message }
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
**`render` signature:**
|
|
103
|
-
```ruby
|
|
104
|
-
render(status:, json: nil, headers: {})
|
|
105
|
-
# status: HTTP symbol (:ok, :created, :not_found, ...) or Integer
|
|
106
|
-
# headers: merged into response_headers — received by on_rpc_reply
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
**`params`** — `HashWithIndifferentAccess` containing:
|
|
110
|
-
- Query string parameters
|
|
111
|
-
- `:id` extracted from the route
|
|
112
|
-
- JSON body (parsed automatically if `content_type` includes `json`)
|
|
113
|
-
|
|
114
|
-
**`raw_string`** — the unparsed body when content type is not JSON.
|
|
115
|
-
|
|
116
|
-
**`self.call(headers:, body:)`** — entry point called by the Consumer; returns `{ status:, headers:, body: }`.
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
### BugBunny::Resource
|
|
121
|
-
|
|
122
|
-
```ruby
|
|
123
|
-
class Order < BugBunny::Resource
|
|
124
|
-
self.exchange = 'orders_exchange'
|
|
125
|
-
self.exchange_type = 'direct'
|
|
126
|
-
self.routing_key = 'orders'
|
|
127
|
-
|
|
128
|
-
attribute :id, :integer
|
|
129
|
-
attribute :status, :string
|
|
130
|
-
attribute :total, :float
|
|
131
|
-
|
|
132
|
-
validates :status, presence: true
|
|
133
|
-
|
|
134
|
-
before_save :set_defaults
|
|
135
|
-
end
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
**Class methods:**
|
|
139
|
-
|
|
140
|
-
| Method | HTTP | Description |
|
|
141
|
-
|---|---|---|
|
|
142
|
-
| `find(id)` | GET `resource/id` | Returns instance or `nil` on 404 |
|
|
143
|
-
| `where(filters)` | GET `resource` | Returns Array, empty on 404 |
|
|
144
|
-
| `all` | GET `resource` | Alias for `where({})` |
|
|
145
|
-
| `create(attrs)` | POST `resource` | Returns instance (may have errors) |
|
|
146
|
-
|
|
147
|
-
**Instance methods:**
|
|
148
|
-
|
|
149
|
-
| Method | HTTP | Description |
|
|
150
|
-
|---|---|---|
|
|
151
|
-
| `save` | POST or PUT | POST if new, PUT if persisted. Returns Boolean. |
|
|
152
|
-
| `update(attrs)` | PUT | `assign_attributes` + `save` |
|
|
153
|
-
| `destroy` | DELETE | Returns Boolean |
|
|
154
|
-
| `persisted?` | — | True after successful save or find |
|
|
155
|
-
| `changed?` | — | True if typed or dynamic attributes changed |
|
|
156
|
-
|
|
157
|
-
**`.with` — per-call context override:**
|
|
158
|
-
```ruby
|
|
159
|
-
# Block form (thread-safe, restores context after block)
|
|
160
|
-
Order.with(exchange: 'other_exchange') do
|
|
161
|
-
Order.find(1)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# Single-call proxy (single-use, raises on second call)
|
|
165
|
-
Order.with(routing_key: 'vip_orders').where(status: 'pending')
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
**Class-level config:**
|
|
169
|
-
```ruby
|
|
170
|
-
Order.connection_pool = MY_POOL
|
|
171
|
-
Order.exchange = 'orders_exchange'
|
|
172
|
-
Order.exchange_type = 'direct'
|
|
173
|
-
Order.routing_key = 'orders'
|
|
174
|
-
Order.resource_name = 'orders' # default: class name pluralized/underscored
|
|
175
|
-
Order.param_key = 'order' # default: model_name.element
|
|
176
|
-
Order.exchange_options = { durable: true }
|
|
177
|
-
Order.queue_options = { durable: true }
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
---
|
|
181
|
-
|
|
182
|
-
### BugBunny::Client
|
|
183
|
-
|
|
184
|
-
For cases where `Resource` is too high-level:
|
|
185
|
-
|
|
186
|
-
```ruby
|
|
187
|
-
client = BugBunny::Client.new(pool: MY_POOL) do |stack|
|
|
188
|
-
stack.use MyTracingMiddleware
|
|
189
|
-
stack.use BugBunny::Middleware::RaiseError
|
|
190
|
-
stack.use BugBunny::Middleware::JsonResponse
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
# RPC (blocking)
|
|
194
|
-
response = client.request('users/1',
|
|
195
|
-
method: :get,
|
|
196
|
-
exchange: 'users_exchange',
|
|
197
|
-
exchange_type: 'direct',
|
|
198
|
-
routing_key: 'users'
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
# Fire-and-forget
|
|
202
|
-
client.publish('events',
|
|
203
|
-
method: :post,
|
|
204
|
-
exchange: 'events_exchange',
|
|
205
|
-
routing_key: 'events',
|
|
206
|
-
body: { type: 'user.created', user_id: 42 }
|
|
207
|
-
)
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
---
|
|
211
|
-
|
|
212
|
-
### BugBunny::ConsumerMiddleware
|
|
213
|
-
|
|
214
|
-
```ruby
|
|
215
|
-
# Register globally
|
|
216
|
-
BugBunny.consumer_middlewares.use MyMiddleware
|
|
217
|
-
|
|
218
|
-
# Or via configuration
|
|
219
|
-
BugBunny.configure do |config|
|
|
220
|
-
config.consumer_middlewares.use TracingMiddleware
|
|
221
|
-
end
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
**Writing a middleware:**
|
|
225
|
-
```ruby
|
|
226
|
-
class MyMiddleware
|
|
227
|
-
def initialize(app)
|
|
228
|
-
@app = app
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def call(delivery_info, properties, body)
|
|
232
|
-
# before
|
|
233
|
-
@app.call(delivery_info, properties, body)
|
|
234
|
-
# after
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
---
|
|
240
|
-
|
|
241
|
-
### BugBunny.create_connection
|
|
242
|
-
|
|
243
|
-
```ruby
|
|
244
|
-
conn = BugBunny.create_connection # Returns a connected Bunny::Session
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
Used to populate a `ConnectionPool`:
|
|
248
|
-
```ruby
|
|
249
|
-
MY_POOL = ConnectionPool.new(size: 5, timeout: 5) { BugBunny.create_connection }
|
|
250
|
-
BugBunny::Resource.connection_pool = MY_POOL
|
|
251
|
-
```
|
data/docs/ai/architecture.md
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
## Architecture
|
|
2
|
-
|
|
3
|
-
### Component Map
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
Publisher side (Service A) Consumer side (Service B)
|
|
7
|
-
───────────────────────────── ─────────────────────────────────────
|
|
8
|
-
Resource.find / .where / .save Consumer#subscribe (blocking loop)
|
|
9
|
-
└─ Client#request / #publish └─ ConsumerMiddleware::Stack#call
|
|
10
|
-
└─ Middleware::Stack (onion) └─ Consumer#process_message
|
|
11
|
-
└─ Producer#rpc / #fire └─ Router#recognize
|
|
12
|
-
└─ Session#exchange └─ Controller.call
|
|
13
|
-
└─ Bunny channel └─ action method
|
|
14
|
-
└─ RabbitMQ └─ render(...)
|
|
15
|
-
└─ Consumer#reply
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
### Session
|
|
19
|
-
|
|
20
|
-
`BugBunny::Session` wraps a Bunny channel. It caches exchange and queue objects by name to avoid redundant AMQP declarations. Cache writes are protected by a `Mutex` (double-checked locking pattern). One Session per connection slot — created lazily in `Client#session_for` and stored as an ivar on the Bunny connection object.
|
|
21
|
-
|
|
22
|
-
### Producer
|
|
23
|
-
|
|
24
|
-
`BugBunny::Producer` publishes messages. One Producer per connection slot — cached alongside the Session. Caching is mandatory: the Producer registers a `basic_consume` on the channel to listen for Direct Reply-to responses. Creating a second Producer on the same channel would trigger an AMQP error (double-consumer).
|
|
25
|
-
|
|
26
|
-
**RPC flow:**
|
|
27
|
-
1. `Producer#rpc` assigns a `correlation_id` and publishes with `reply_to: 'amq.rabbitmq.reply-to'`.
|
|
28
|
-
2. A `Concurrent::IVar` (`future`) is registered in an in-memory hash keyed by `correlation_id`.
|
|
29
|
-
3. A reply-listener thread sets `future.set(payload)` when the reply arrives.
|
|
30
|
-
4. The calling thread blocks on `future.value(timeout)`.
|
|
31
|
-
5. On success: `on_rpc_reply&.call(headers)` is invoked, then the response is parsed.
|
|
32
|
-
6. On timeout: `BugBunny::RequestTimeout` is raised.
|
|
33
|
-
|
|
34
|
-
**Fire-and-forget flow:**
|
|
35
|
-
`Producer#fire` publishes without `reply_to`. No blocking.
|
|
36
|
-
|
|
37
|
-
### Client
|
|
38
|
-
|
|
39
|
-
`BugBunny::Client` implements the Faraday-style Onion Middleware pattern. The final action in the chain is the call to `Producer#rpc` or `Producer#fire`. Middlewares wrap that action, each calling `app.call(request)` to continue the chain.
|
|
40
|
-
|
|
41
|
-
Built-in middlewares for `Resource`:
|
|
42
|
-
- `Middleware::RaiseError` — converts non-2xx status codes to exceptions.
|
|
43
|
-
- `Middleware::JsonResponse` — parses the JSON body and normalizes the response hash.
|
|
44
|
-
|
|
45
|
-
### Consumer
|
|
46
|
-
|
|
47
|
-
`BugBunny::Consumer` is a blocking subscribe loop intended to run in a dedicated process (not inside Puma). Responsibilities:
|
|
48
|
-
1. Declare exchange, queue, and binding.
|
|
49
|
-
2. Start a background `Concurrent::TimerTask` as a health check.
|
|
50
|
-
3. For each message: invoke `ConsumerMiddleware::Stack`, then `process_message`.
|
|
51
|
-
|
|
52
|
-
`process_message` flow:
|
|
53
|
-
1. Extract `path` from `properties.type` (or `headers['path']`).
|
|
54
|
-
2. Extract HTTP method from `headers['x-http-method']`.
|
|
55
|
-
3. Parse query string from the path.
|
|
56
|
-
4. Call `BugBunny.routes.recognize(method, path)` → controller + action + params.
|
|
57
|
-
5. Constantize the controller class; verify it inherits from `BugBunny::Controller` (RCE prevention).
|
|
58
|
-
6. Call `ControllerClass.call(headers:, body:)` → response hash.
|
|
59
|
-
7. If `reply_to` is present: publish reply with `rpc_reply_headers` injected.
|
|
60
|
-
8. ACK the delivery tag.
|
|
61
|
-
|
|
62
|
-
On any error: publish a 500 reply (so the RPC caller doesn't timeout), then NACK/reject.
|
|
63
|
-
|
|
64
|
-
### ConsumerMiddleware::Stack
|
|
65
|
-
|
|
66
|
-
A pipeline of middleware objects. Registration is protected by a `Mutex`. Each middleware is a class implementing `#initialize(app)` and `#call(delivery_info, properties, body)`. The terminal app is the controller dispatch lambda. Middlewares run in registration order (first registered = outermost wrapper).
|
|
67
|
-
|
|
68
|
-
### Router
|
|
69
|
-
|
|
70
|
-
`BugBunny::Routing::RouteSet` stores an array of `Route` objects. `recognize(method, path)` iterates routes looking for a match. Routes are registered via the DSL:
|
|
71
|
-
- `resources :users` → 5 standard routes (index, show, create, update, destroy) + member/collection.
|
|
72
|
-
- `namespace :admin { resources :users }` → same routes with path prefix `admin/` and namespace `Admin::Controllers`.
|
|
73
|
-
|
|
74
|
-
### Resource
|
|
75
|
-
|
|
76
|
-
`BugBunny::Resource` is an ActiveModel class. It resolves AMQP config (exchange, routing key, pool) via a 3-level cascade: thread-local (set by `.with`) → class-level → superclass. Dirty tracking covers both typed `attribute` columns (via ActiveModel::Dirty) and dynamic attributes (via `@extra_attributes` + `@dynamic_changes`).
|
|
77
|
-
|
|
78
|
-
### Configuration Cascade (Resource)
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
Thread.current["bb_#{object_id}_exchange"] ← .with(exchange:) sets this
|
|
82
|
-
↓ (nil fallback)
|
|
83
|
-
Resource.exchange= ← class-level static config
|
|
84
|
-
↓ (nil fallback)
|
|
85
|
-
ParentResource.exchange= ← walks superclass chain
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Same cascade for: `routing_key`, `exchange_type`, `pool`, `exchange_options`, `queue_options`.
|
|
89
|
-
|
|
90
|
-
### Observability
|
|
91
|
-
|
|
92
|
-
All BugBunny classes include `BugBunny::Observability`. `safe_log` formats structured log lines as `component=x event=clase.evento [key=value ...]` and filters sensitive keys (`password`, `token`, `secret`, `api_key`, `auth`, etc.). Log failures are swallowed — they never affect the main flow.
|
data/docs/ai/errors.md
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
## Errors
|
|
2
|
-
|
|
3
|
-
All BugBunny exceptions inherit from `BugBunny::Error < StandardError`. Catch `BugBunny::Error` to handle all gem-level errors.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
### BugBunny::ConfigurationError
|
|
8
|
-
|
|
9
|
-
**Cause:** `BugBunny.configure` block completed with invalid values. Triggered by `validate!` at the end of `configure`.
|
|
10
|
-
|
|
11
|
-
**Common triggers:**
|
|
12
|
-
- `host` is nil or empty string
|
|
13
|
-
- `port` is outside `1..65535`
|
|
14
|
-
- `rpc_timeout` is not an Integer or is outside `1..3600`
|
|
15
|
-
|
|
16
|
-
**How to reproduce:**
|
|
17
|
-
```ruby
|
|
18
|
-
BugBunny.configure { |c| c.host = '' }
|
|
19
|
-
# => BugBunny::ConfigurationError: host is required
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
**Resolution:** Check all required fields. See `Configuration::VALIDATIONS` for the full list of validated attributes and their constraints.
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
### BugBunny::CommunicationError
|
|
27
|
-
|
|
28
|
-
**Cause:** TCP-level failure connecting to or communicating with RabbitMQ. Usually wraps a Bunny internal exception.
|
|
29
|
-
|
|
30
|
-
**Common triggers:**
|
|
31
|
-
- RabbitMQ is not running
|
|
32
|
-
- Wrong host/port
|
|
33
|
-
- Firewall blocking the connection
|
|
34
|
-
- Network interruption during message exchange
|
|
35
|
-
|
|
36
|
-
**Resolution:** Check RabbitMQ is reachable (`telnet host 5672`). Review `network_recovery_interval` and `max_reconnect_attempts` settings. The Consumer retries automatically with exponential backoff.
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
### BugBunny::SecurityError
|
|
41
|
-
|
|
42
|
-
**Cause:** The Consumer received a message with a `type` header that resolves to a class not inheriting from `BugBunny::Controller`.
|
|
43
|
-
|
|
44
|
-
**How to reproduce:**
|
|
45
|
-
```ruby
|
|
46
|
-
# Publish a message with type: "Kernel"
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
**Resolution:** This is an intentional RCE prevention check. Ensure all controller classes inherit from `BugBunny::Controller`. If legitimate, verify `config.controller_namespace` is set correctly.
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
### BugBunny::RequestTimeout
|
|
54
|
-
|
|
55
|
-
**Cause:** An RPC call did not receive a reply within `config.rpc_timeout` seconds.
|
|
56
|
-
|
|
57
|
-
**Common triggers:**
|
|
58
|
-
- The Consumer is not running
|
|
59
|
-
- The Consumer is overwhelmed (increase `channel_prefetch`)
|
|
60
|
-
- `rpc_timeout` is too low for the workload
|
|
61
|
-
- The remote controller raised an exception before sending a reply (check consumer logs)
|
|
62
|
-
|
|
63
|
-
**Resolution:** Check that the Consumer process is alive. Review `consumer.execution_error` log entries on the consumer side. Increase `rpc_timeout` if the operation is legitimately slow.
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
### BugBunny::NotFound (404)
|
|
68
|
-
|
|
69
|
-
**Cause:** The remote service responded with HTTP 404. Raised by `Middleware::RaiseError`.
|
|
70
|
-
|
|
71
|
-
**In Resource context:** `find` and `where` catch this internally and return `nil` / `[]` respectively.
|
|
72
|
-
|
|
73
|
-
**In Client context:** Propagates unless caught by the caller.
|
|
74
|
-
|
|
75
|
-
**Resolution:** Verify the resource ID exists. Ensure the Consumer's route for that path is registered.
|
|
76
|
-
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
### BugBunny::BadRequest (400)
|
|
80
|
-
|
|
81
|
-
**Cause:** The remote service returned 400. Also raised locally if the JSON body cannot be parsed.
|
|
82
|
-
|
|
83
|
-
**Local trigger:**
|
|
84
|
-
```ruby
|
|
85
|
-
# In Controller#prepare_params — body is not valid JSON and content_type includes 'json'
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
**Resolution:** Verify the request body is valid JSON when `content_type: 'application/json'` is used.
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
### BugBunny::Conflict (409)
|
|
93
|
-
|
|
94
|
-
**Cause:** The remote service returned 409 — the request is technically valid but conflicts with business rules or existing data.
|
|
95
|
-
|
|
96
|
-
**Resolution:** Handle the conflict in application logic. Inspect `e.message` for details from the remote service.
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
### BugBunny::UnprocessableEntity (422)
|
|
101
|
-
|
|
102
|
-
**Cause:** The remote service returned 422 (validation failure).
|
|
103
|
-
|
|
104
|
-
**Attributes:**
|
|
105
|
-
- `e.error_messages` — `Hash`, `Array`, or `String` extracted from `{ "errors": ... }` in the response body.
|
|
106
|
-
- `e.raw_response` — The raw response body.
|
|
107
|
-
|
|
108
|
-
**In Resource context:** `save` catches this, loads errors into `resource.errors`, and returns `false`.
|
|
109
|
-
|
|
110
|
-
**In Client context:** Raised directly.
|
|
111
|
-
|
|
112
|
-
**Resolution:**
|
|
113
|
-
```ruby
|
|
114
|
-
resource = Order.create(attrs)
|
|
115
|
-
unless resource.persisted?
|
|
116
|
-
resource.errors.full_messages # => ["Status can't be blank"]
|
|
117
|
-
end
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
### BugBunny::NotAcceptable (406)
|
|
123
|
-
|
|
124
|
-
**Cause:** The remote service returned 406 — it cannot produce a response matching the requested content type.
|
|
125
|
-
|
|
126
|
-
**Resolution:** Ensure the client and server agree on content type. BugBunny uses `application/json` by default.
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
### BugBunny::InternalServerError (500)
|
|
131
|
-
|
|
132
|
-
**Cause:** The remote service returned 500. Also sent by the Consumer when an unhandled exception occurs during `process_message`.
|
|
133
|
-
|
|
134
|
-
**Resolution:** Check `consumer.execution_error` log entries on the consumer side for the actual exception. Fix the underlying error in the controller.
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
### BugBunny::Error: "Connection pool missing for ClassName"
|
|
139
|
-
|
|
140
|
-
**Cause:** `Resource.bug_bunny_client` was called before `Resource.connection_pool` was set.
|
|
141
|
-
|
|
142
|
-
**Resolution:** Set `BugBunny::Resource.connection_pool = MY_POOL` in the initializer before any Resource calls.
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
### BugBunny::Error: "ScopeProxy is single-use"
|
|
147
|
-
|
|
148
|
-
**Cause:** A `ScopeProxy` returned by `.with(...)` without a block was called more than once.
|
|
149
|
-
|
|
150
|
-
**Resolution:** Use the block form: `Resource.with(...) { ... }` or call `.with(...)` again for each subsequent call.
|
|
151
|
-
|
|
152
|
-
---
|
|
153
|
-
|
|
154
|
-
### BugBunny::Error: "Exchange not defined for ClassName"
|
|
155
|
-
|
|
156
|
-
**Cause:** `Resource.current_exchange` was called but no exchange was configured at any level (thread-local, class, or superclass).
|
|
157
|
-
|
|
158
|
-
**Resolution:** Set `self.exchange = 'exchange_name'` in the Resource class definition or use `.with(exchange:)`.
|
data/docs/ai/faq_external.md
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
## FAQ — External (Integrator / Consumer of BugBunny)
|
|
2
|
-
|
|
3
|
-
### How do I set up BugBunny in a Rails app?
|
|
4
|
-
|
|
5
|
-
Add to Gemfile: `gem 'bug_bunny'` and `gem 'connection_pool'`. Run `rails generate bug_bunny:install`. Edit `config/initializers/bug_bunny.rb` with your RabbitMQ credentials. Create a pool: `MY_POOL = ConnectionPool.new(size: 5) { BugBunny.create_connection }`. Assign it: `BugBunny::Resource.connection_pool = MY_POOL`. See `docs/howto/rails.md` for the complete setup.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
### How do I make an RPC call (blocking request)?
|
|
10
|
-
|
|
11
|
-
Use `Resource.find` / `Resource.where` / `Resource.create` — they all do RPC internally. For lower-level control: `client.request('users/1', method: :get, exchange: 'users_exchange', routing_key: 'users')`. The call blocks until the remote service replies or `rpc_timeout` expires.
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
### How do I publish without waiting for a reply?
|
|
16
|
-
|
|
17
|
-
Use `client.publish('events', method: :post, exchange: 'events_exchange', routing_key: 'events', body: { ... })`. Fire-and-forget — does not block. No RPC timeout applies.
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
### What happens when `find` returns nil vs raises?
|
|
22
|
-
|
|
23
|
-
`Resource.find` returns `nil` on 404 (does not raise). `Resource.where` returns `[]` on 404. Both raise `BugBunny::RequestTimeout` if the remote service does not reply within `rpc_timeout`. They raise `BugBunny::ServerError` on 5xx responses.
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
### How do I handle validation errors from the remote service?
|
|
28
|
-
|
|
29
|
-
`resource.save` returns `false` on 422 and loads the remote errors into `resource.errors`. Check `resource.valid?` then `resource.errors.full_messages`. The remote service must render `{ errors: { field: ['message'] } }` for the errors to be auto-loaded.
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
### How do I use `.with` to override the exchange per call?
|
|
34
|
-
|
|
35
|
-
```ruby
|
|
36
|
-
# Block form (preferred — thread-safe, restores after block)
|
|
37
|
-
Order.with(exchange: 'priority_exchange', routing_key: 'priority') do
|
|
38
|
-
Order.find(id)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Single-call proxy
|
|
42
|
-
Order.with(exchange: 'priority_exchange').find(id)
|
|
43
|
-
# ScopeProxy is single-use — calling a second method raises BugBunny::Error
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
### How do I define a Resource with typed attributes?
|
|
49
|
-
|
|
50
|
-
```ruby
|
|
51
|
-
class User < BugBunny::Resource
|
|
52
|
-
self.exchange = 'users_exchange'
|
|
53
|
-
|
|
54
|
-
attribute :id, :integer
|
|
55
|
-
attribute :name, :string
|
|
56
|
-
attribute :email, :string
|
|
57
|
-
|
|
58
|
-
validates :name, presence: true
|
|
59
|
-
end
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Typed attributes get ActiveModel coercion and dirty tracking. Attributes not declared with `attribute` are handled dynamically via `method_missing` and tracked in `@extra_attributes`.
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
### How do I add client-side middleware to a Resource?
|
|
67
|
-
|
|
68
|
-
```ruby
|
|
69
|
-
class Order < BugBunny::Resource
|
|
70
|
-
client_middleware do |stack|
|
|
71
|
-
stack.use MyRetryMiddleware
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Middlewares are inherited by subclasses and applied in registration order (first = outermost). `RaiseError` and `JsonResponse` are always added as the innermost middlewares by `Resource` — do not add them manually.
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
### How do I run the Consumer in a Rails app?
|
|
81
|
-
|
|
82
|
-
The Consumer is a blocking loop. Run it in a separate process, not inside Puma:
|
|
83
|
-
|
|
84
|
-
```ruby
|
|
85
|
-
# lib/tasks/rabbit.rake
|
|
86
|
-
task consumer: :environment do
|
|
87
|
-
conn = BugBunny.create_connection
|
|
88
|
-
consumer = BugBunny::Consumer.new(conn)
|
|
89
|
-
trap('TERM') { consumer.shutdown; exit }
|
|
90
|
-
trap('INT') { consumer.shutdown; exit }
|
|
91
|
-
consumer.subscribe(
|
|
92
|
-
queue_name: 'my_queue', exchange_name: 'my_exchange', routing_key: 'my_key'
|
|
93
|
-
)
|
|
94
|
-
end
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
### How do I write a Controller?
|
|
100
|
-
|
|
101
|
-
Subclass `BugBunny::Controller`. Place it in the namespace from `config.controller_namespace` (default: `BugBunny::Controllers`). Implement action methods (`index`, `show`, `create`, `update`, `destroy` or custom). Call `render(status:, json:)` to respond. The Consumer routes messages to `YourController.call(headers:, body:)`.
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
### How do I propagate trace context through RabbitMQ?
|
|
106
|
-
|
|
107
|
-
On the consumer side, inject headers into replies:
|
|
108
|
-
```ruby
|
|
109
|
-
config.rpc_reply_headers = -> { { 'X-Trace-Id' => Tracer.current } }
|
|
110
|
-
```
|
|
111
|
-
On the producer side, hydrate context from the reply:
|
|
112
|
-
```ruby
|
|
113
|
-
config.on_rpc_reply = ->(headers) { Tracer.hydrate(headers['X-Trace-Id']) }
|
|
114
|
-
```
|
|
115
|
-
For consumer-side middleware (propagating from incoming request), use `ConsumerMiddleware`.
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
### How do I configure health checks for Kubernetes?
|
|
120
|
-
|
|
121
|
-
Set `config.health_check_file = '/app/tmp/bug_bunny_health'`. BugBunny touches that file every `health_check_interval` seconds (default: 60) after verifying the RabbitMQ connection. Add a `livenessProbe` in your Kubernetes manifest checking for that file's existence.
|
|
122
|
-
|
|
123
|
-
---
|
|
124
|
-
|
|
125
|
-
### What is the default RPC timeout and how do I change it?
|
|
126
|
-
|
|
127
|
-
Default: 10 seconds. Override globally: `config.rpc_timeout = 30`. Override per request: `client.request('users/1', method: :get, exchange: ..., timeout: 5)`. When exceeded, `BugBunny::RequestTimeout` is raised.
|
|
128
|
-
|
|
129
|
-
---
|
|
130
|
-
|
|
131
|
-
### Do I need to declare exchanges and queues manually?
|
|
132
|
-
|
|
133
|
-
No. BugBunny declares them automatically when `subscribe` (consumer) or the first RPC call (producer) is made. Use `config.exchange_options` and `config.queue_options` for global defaults (e.g., `{ durable: true }`). Override per resource with `Resource.exchange_options=` or per call with `.with(exchange_options:)`.
|