data_drain 0.2.0 → 0.2.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/.rubocop.yml +2 -0
- data/CHANGELOG.md +34 -0
- data/CLAUDE.md +3 -1
- data/README.md +3 -2
- data/docs/IMPROVEMENT_PLAN.md +1417 -0
- data/docs/execution/archive/v0.2.0.agente-review.md +125 -0
- data/docs/execution/archive/v0.2.0.md +812 -0
- data/docs/execution/v0.2.2.md +891 -0
- data/docs/glue_pyspark_example.py +60 -0
- data/lib/data_drain/configuration.rb +49 -5
- data/lib/data_drain/engine.rb +1 -0
- data/lib/data_drain/file_ingestor.rb +1 -0
- data/lib/data_drain/glue_runner.rb +22 -10
- data/lib/data_drain/observability.rb +4 -2
- data/lib/data_drain/record.rb +2 -1
- data/lib/data_drain/storage/s3.rb +33 -37
- data/lib/data_drain/version.rb +1 -1
- data/skill/SKILL.md +1 -0
- data/skill/references/antipatrones.md +21 -4
- data/skill/references/api-detallada.md +18 -5
- data/skill/references/eventos-telemetria.md +5 -0
- data/skill/references/postgres-tuning.md +129 -0
- metadata +7 -1
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
# Plan de Ejecución — v0.2.2
|
|
2
|
+
|
|
3
|
+
**Release objetivo:** v0.2.2 — Robustez operacional (items P1 cheap)
|
|
4
|
+
**Items del roadmap:** 9, 7, 8, 11a ([ver IMPROVEMENT_PLAN.md](../IMPROVEMENT_PLAN.md#p1--performance-y-robustez-v021--v030))
|
|
5
|
+
**Branch sugerido:** `feature/v0.2.2`
|
|
6
|
+
**Base:** `main` (contiene v0.2.0 + v0.2.1)
|
|
7
|
+
**Estado:** No iniciado
|
|
8
|
+
**Última actualización:** 2026-04-14
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Contexto
|
|
13
|
+
|
|
14
|
+
v0.2.0 cerró los P0 (security + testing). v0.2.1 fue un patch CI + PR feedback. Este release mete los items P1 "baratos" que aportan valor operacional sin refactors grandes:
|
|
15
|
+
|
|
16
|
+
| Item | Resumen | Estimación |
|
|
17
|
+
|------|---------|------------|
|
|
18
|
+
| 9 | Filtro secretos por regex en Observability | 30min |
|
|
19
|
+
| 7 | `max_wait_seconds` en `GlueRunner.run_and_wait` | 1-2h |
|
|
20
|
+
| 8 | `Configuration#validate!` | 2-3h |
|
|
21
|
+
| 11a | Docs Postgres tuning por tamaño de tabla | 4-6h |
|
|
22
|
+
| cleanup A1 | Fix typo `依赖` en CHANGELOG v0.2.1 | 2min |
|
|
23
|
+
| cleanup A2 | Comment en `disconnect!` rescue | 2min |
|
|
24
|
+
| cleanup A3 | Rename + agregar test string/symbol keys en `record_spec.rb` | 10min |
|
|
25
|
+
| cleanup A4 | Cerrar `db`+`conn` en `record_spec.rb#before(:all)` | 5min |
|
|
26
|
+
| cleanup B1 | Reordenar `public`/`private` en `storage/s3.rb` | 15min |
|
|
27
|
+
|
|
28
|
+
**Total estimado:** 1-2 días de trabajo enfocado.
|
|
29
|
+
|
|
30
|
+
**Excluidos intencionalmente** (se mueven a v0.3.0 por ser refactor/más costosos):
|
|
31
|
+
- Item 5 (VACUUM post-purga) — toca purge path crítico, requiere tests con Postgres real
|
|
32
|
+
- Item 6 (sandboxing Record) — requiere validación con S3 real
|
|
33
|
+
- Item 10 (refactor Engine#call) — refactor mayor
|
|
34
|
+
- Item 11b (warning runtime purga lenta) — requiere mocks elaborados
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Review de agentes — incorporado
|
|
39
|
+
|
|
40
|
+
Revisión por **big-pickle (opencode)** 2026-04-14 ([ClickUp task 86b9dka0c](https://app.clickup.com/t/86b9dka0c)). 4 riesgos planteados, todos incorporados:
|
|
41
|
+
|
|
42
|
+
| Riesgo | Resolución | Ubicación en este plan |
|
|
43
|
+
|--------|-----------|----------------------|
|
|
44
|
+
| 1: `db_port` con default 5432 — validación pasa siempre | Excluido de `validate_db_config!` con nota explicativa | Fase 3.1 |
|
|
45
|
+
| 2: `db_pass` no se valida sin justificar | Excluido con nota: auth peer/trust/IAM pueden tener nil | Fase 3.1 + test nuevo 3.2 |
|
|
46
|
+
| 3: Mock `Process.clock_gettime` puede ser flakey | Plan B ya contempla Timecop como fallback | Sin cambio |
|
|
47
|
+
| 4: Acceso al monorepo Wispro no alcanzable desde gema | Marcado como step manual explícito | Fase 3.3 |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Orden de ejecución y dependencias
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Fase 0: setup branch + baseline + A1 (fix CHANGELOG v0.2.1)
|
|
55
|
+
│
|
|
56
|
+
▼
|
|
57
|
+
Fase 1: Item 9 (filtro secretos regex) ──► isolated, warm-up
|
|
58
|
+
│
|
|
59
|
+
▼
|
|
60
|
+
Fase 2: Item 7 (max_wait_seconds GlueRunner) ──► isolated
|
|
61
|
+
│
|
|
62
|
+
▼
|
|
63
|
+
Fase 3: Item 8 (Configuration#validate!) ──► toca Engine/FileIngestor/GlueRunner
|
|
64
|
+
│ (depende de item 1 de v0.2.0 ya mergeado)
|
|
65
|
+
▼
|
|
66
|
+
Fase 4: Item 11a (docs Postgres tuning) ──► pure docs, sin deps
|
|
67
|
+
│
|
|
68
|
+
▼
|
|
69
|
+
Fase 4.5: Cleanup del review de v0.2.0 (A2, A3, A4, B1)
|
|
70
|
+
│
|
|
71
|
+
▼
|
|
72
|
+
Fase 5: Release (CHANGELOG, version bump, tag)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Razonamiento:**
|
|
76
|
+
- 9 primero: XS, isolated a Observability, warm-up.
|
|
77
|
+
- 7 segundo: isolated a GlueRunner, patrón simple.
|
|
78
|
+
- 8 tercero: toca 3 clases, establece el patrón de `raise ConfigurationError`. Depende de item 1 (v0.2.0) — la validación NO debe exigir `aws_access_key_id` porque ahora es opcional con `credential_chain`.
|
|
79
|
+
- 11a último: docs puras, puede generarse con la skill `postgresql-optimization` como apoyo.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Pre-requisitos (Fase 0)
|
|
84
|
+
|
|
85
|
+
### 0.1 Verificar entorno
|
|
86
|
+
|
|
87
|
+
- [ ] `git checkout main && git pull`
|
|
88
|
+
- [ ] Confirmar versión actual en `lib/data_drain/version.rb` = `"0.2.1"`
|
|
89
|
+
- [ ] `bundle exec rspec` pasa en main (112 specs, coverage ≥ 80%)
|
|
90
|
+
- [ ] `bundle exec rubocop` sin ofensas en `lib/` (specs excluidas por `.rubocop.yml`)
|
|
91
|
+
|
|
92
|
+
### 0.2 Crear branch
|
|
93
|
+
|
|
94
|
+
- [ ] `git checkout -b feature/v0.2.2`
|
|
95
|
+
|
|
96
|
+
### 0.3 Revisar skill `postgresql-optimization`
|
|
97
|
+
|
|
98
|
+
Para item 11a. Ubicada en `.agents/skills/postgresql-optimization/SKILL.md`. Tiene material sobre:
|
|
99
|
+
- Índices composite/parcial/expression/covering
|
|
100
|
+
- EXPLAIN ANALYZE
|
|
101
|
+
- pg_stat_statements, pg_stat_activity
|
|
102
|
+
- Particionamiento declarativo
|
|
103
|
+
- VACUUM
|
|
104
|
+
|
|
105
|
+
- [ ] Abrir skill para consulta durante Fase 4
|
|
106
|
+
|
|
107
|
+
### 0.4 Cleanup A1 — Fix typo `依赖` en CHANGELOG v0.2.1
|
|
108
|
+
|
|
109
|
+
- [ ] Editar `CHANGELOG.md`, encontrar la línea:
|
|
110
|
+
```
|
|
111
|
+
CI: Descarga binario pre-compilado de DuckDB en vez de依赖 del sistema (`libduckdb-dev`).
|
|
112
|
+
```
|
|
113
|
+
Reemplazar `en vez de依赖 del sistema` por `en vez de depender del sistema`.
|
|
114
|
+
- [ ] Commit: `fix(changelog): typo en CHANGELOG v0.2.1 (cleanup A1)`
|
|
115
|
+
|
|
116
|
+
### Checkpoint Fase 0
|
|
117
|
+
|
|
118
|
+
- [ ] Branch creado
|
|
119
|
+
- [ ] Baseline tests verdes
|
|
120
|
+
- [ ] Entorno limpio
|
|
121
|
+
- [ ] CHANGELOG v0.2.1 corregido
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Fase 1 — Item 9: Filtro secretos por regex (P1)
|
|
126
|
+
|
|
127
|
+
**Roadmap:** [Item 9](../IMPROVEMENT_PLAN.md#item-9--filtro-de-secretos-por-regex-en-observability)
|
|
128
|
+
|
|
129
|
+
### Contexto
|
|
130
|
+
|
|
131
|
+
`Observability#safe_log` filtra solo claves exactas (`%i[password token secret api_key auth]`). No filtra variantes como `db_password`, `aws_secret_access_key`, `bearer_token`. Cambio trivial pero aumenta defensa en profundidad significativamente.
|
|
132
|
+
|
|
133
|
+
### 1.1 Implementación
|
|
134
|
+
|
|
135
|
+
- [ ] Editar `lib/data_drain/observability.rb`:
|
|
136
|
+
- Agregar constante al módulo (antes de `module_function` equivalente):
|
|
137
|
+
```ruby
|
|
138
|
+
SENSITIVE_KEY_PATTERN = /password|passwd|pass|secret|token|api_key|apikey|auth|credential|private_key/i
|
|
139
|
+
```
|
|
140
|
+
- En `safe_log`, reemplazar:
|
|
141
|
+
```ruby
|
|
142
|
+
val = %i[password token secret api_key auth].include?(k.to_sym) ? "[FILTERED]" : v
|
|
143
|
+
```
|
|
144
|
+
por:
|
|
145
|
+
```ruby
|
|
146
|
+
val = SENSITIVE_KEY_PATTERN.match?(k.to_s) ? "[FILTERED]" : v
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 1.2 Tests
|
|
150
|
+
|
|
151
|
+
- [ ] Editar `spec/data_drain/observability_spec.rb`. Agregar al bloque `describe "#safe_log"`:
|
|
152
|
+
```ruby
|
|
153
|
+
it "filtra db_password (regex)" do
|
|
154
|
+
instance.emit(:info, "test", db_password: "x")
|
|
155
|
+
expect(test_logger.string).to include("db_password=[FILTERED]")
|
|
156
|
+
expect(test_logger.string).not_to include("db_password=x")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "filtra aws_secret_access_key (regex)" do
|
|
160
|
+
instance.emit(:info, "test", aws_secret_access_key: "akia123")
|
|
161
|
+
expect(test_logger.string).to include("aws_secret_access_key=[FILTERED]")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "filtra bearer_token (regex)" do
|
|
165
|
+
instance.emit(:info, "test", bearer_token: "eyJhbGc...")
|
|
166
|
+
expect(test_logger.string).to include("bearer_token=[FILTERED]")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "filtra private_key (regex)" do
|
|
170
|
+
instance.emit(:info, "test", private_key: "-----BEGIN RSA")
|
|
171
|
+
expect(test_logger.string).to include("private_key=[FILTERED]")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it "filtra credential" do
|
|
175
|
+
instance.emit(:info, "test", aws_credential: "x")
|
|
176
|
+
expect(test_logger.string).to include("aws_credential=[FILTERED]")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it "no filtra campos no sensibles" do
|
|
180
|
+
instance.emit(:info, "test", count: 42, table: "versions", user_id: 5)
|
|
181
|
+
expect(test_logger.string).to include("count=42")
|
|
182
|
+
expect(test_logger.string).to include("user_id=5")
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 1.3 Validación local
|
|
187
|
+
|
|
188
|
+
- [ ] `bundle exec rspec spec/data_drain/observability_spec.rb`
|
|
189
|
+
- [ ] `bundle exec rubocop lib/data_drain/observability.rb`
|
|
190
|
+
|
|
191
|
+
### 1.4 Docs
|
|
192
|
+
|
|
193
|
+
- [ ] Actualizar `CLAUDE.md` sección "Logging" — mencionar regex filter
|
|
194
|
+
- [ ] Actualizar `skill/references/api-detallada.md` sección Observability#safe_log
|
|
195
|
+
- [ ] Alinear con `/Users/gabriel/.claude/CLAUDE.md` línea `Filter sensitive keys (password|pass|passwd|secret|token|api_key|auth) → [FILTERED]` — el regex de v0.2.2 es **superset** (agrega `apikey`, `credential`, `private_key`), así que cumple con el estándar global.
|
|
196
|
+
|
|
197
|
+
### 1.5 Commit
|
|
198
|
+
|
|
199
|
+
- [ ] `git add lib/data_drain/observability.rb spec/data_drain/observability_spec.rb`
|
|
200
|
+
- [ ] `git add CLAUDE.md skill/`
|
|
201
|
+
- [ ] Commit: `feat(security): filtro secretos por regex en Observability (item 9)`
|
|
202
|
+
|
|
203
|
+
### Checkpoint Fase 1
|
|
204
|
+
|
|
205
|
+
- [ ] Tests verdes
|
|
206
|
+
- [ ] Rubocop limpio
|
|
207
|
+
- [ ] Coverage no bajó
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Fase 2 — Item 7: `max_wait_seconds` en GlueRunner (P1)
|
|
212
|
+
|
|
213
|
+
**Roadmap:** [Item 7](../IMPROVEMENT_PLAN.md#item-7--max_wait_seconds-en-gluerunnerrun_and_wait)
|
|
214
|
+
|
|
215
|
+
### Contexto
|
|
216
|
+
|
|
217
|
+
`GlueRunner.run_and_wait` no tiene timeout máximo. Si Glue queda colgado en `RUNNING`, bloquea indefinidamente.
|
|
218
|
+
|
|
219
|
+
### 2.1 Implementación
|
|
220
|
+
|
|
221
|
+
- [ ] Editar `lib/data_drain/glue_runner.rb`:
|
|
222
|
+
- Agregar parámetro `max_wait_seconds:` con default `nil`:
|
|
223
|
+
```ruby
|
|
224
|
+
def self.run_and_wait(job_name, arguments = {}, polling_interval: 30, max_wait_seconds: nil)
|
|
225
|
+
```
|
|
226
|
+
- En el loop, antes del `get_job_run`, agregar guard:
|
|
227
|
+
```ruby
|
|
228
|
+
loop do
|
|
229
|
+
if max_wait_seconds &&
|
|
230
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) > max_wait_seconds
|
|
231
|
+
safe_log(:error, "glue_runner.timeout", {
|
|
232
|
+
job: job_name,
|
|
233
|
+
run_id: run_id,
|
|
234
|
+
max_wait_seconds: max_wait_seconds
|
|
235
|
+
})
|
|
236
|
+
raise DataDrain::Error,
|
|
237
|
+
"Glue Job #{job_name} (Run ID: #{run_id}) excedió max_wait_seconds=#{max_wait_seconds}"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
run_info = client.get_job_run(...).job_run
|
|
241
|
+
# ... resto del case actual
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
- Actualizar YARD:
|
|
245
|
+
```ruby
|
|
246
|
+
# @param max_wait_seconds [Integer, nil] Timeout máximo en segundos.
|
|
247
|
+
# nil = sin límite (comportamiento anterior).
|
|
248
|
+
# @raise [DataDrain::Error] si max_wait_seconds excede antes de SUCCEEDED
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### 2.2 Tests
|
|
252
|
+
|
|
253
|
+
- [ ] Editar `spec/data_drain/glue_runner_spec.rb`. Agregar:
|
|
254
|
+
```ruby
|
|
255
|
+
it "levanta DataDrain::Error cuando max_wait_seconds se excede" do
|
|
256
|
+
start_response = double("start_resp", job_run_id: "run-timeout")
|
|
257
|
+
running_info = double("run_info", job_run_state: "RUNNING", error_message: nil)
|
|
258
|
+
|
|
259
|
+
allow(mock_client).to receive(:start_job_run).and_return(start_response)
|
|
260
|
+
allow(mock_client).to receive(:get_job_run)
|
|
261
|
+
.and_return(double("get_resp", job_run: running_info))
|
|
262
|
+
|
|
263
|
+
# Simular tiempo: start_time es t0, primer check pasa, segundo check excede
|
|
264
|
+
times = [0.0, 0.1, 200.0]
|
|
265
|
+
allow(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC) do
|
|
266
|
+
times.shift || 300.0
|
|
267
|
+
end
|
|
268
|
+
allow(Kernel).to receive(:sleep)
|
|
269
|
+
|
|
270
|
+
expect do
|
|
271
|
+
described_class.run_and_wait("slow-job", {}, polling_interval: 1, max_wait_seconds: 60)
|
|
272
|
+
end.to raise_error(DataDrain::Error, /max_wait_seconds=60/)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it "sin max_wait_seconds mantiene comportamiento anterior (no timeout local)" do
|
|
276
|
+
start_response = double("start_resp", job_run_id: "run-ok")
|
|
277
|
+
succeeded_info = double("run_info", job_run_state: "SUCCEEDED", error_message: nil)
|
|
278
|
+
|
|
279
|
+
allow(mock_client).to receive(:start_job_run).and_return(start_response)
|
|
280
|
+
allow(mock_client).to receive(:get_job_run)
|
|
281
|
+
.and_return(double("get_resp", job_run: succeeded_info))
|
|
282
|
+
|
|
283
|
+
# max_wait_seconds nil por default — no se chequea
|
|
284
|
+
expect { described_class.run_and_wait("ok-job") }.not_to raise_error
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### 2.3 Validación local
|
|
289
|
+
|
|
290
|
+
- [ ] `bundle exec rspec spec/data_drain/glue_runner_spec.rb`
|
|
291
|
+
- [ ] `bundle exec rubocop lib/data_drain/glue_runner.rb`
|
|
292
|
+
|
|
293
|
+
### 2.4 Docs
|
|
294
|
+
|
|
295
|
+
- [ ] `skill/references/api-detallada.md` sección GlueRunner — agregar `max_wait_seconds`
|
|
296
|
+
- [ ] `skill/references/eventos-telemetria.md` — agregar evento `glue_runner.timeout`
|
|
297
|
+
- [ ] `skill/references/antipatrones.md` — actualizar antipatrón 14 ("Confiar en que GlueRunner tiene timeout máximo") para mencionar que ahora sí se puede con `max_wait_seconds:`
|
|
298
|
+
|
|
299
|
+
### 2.5 Commit
|
|
300
|
+
|
|
301
|
+
- [ ] Commit: `feat(glue): max_wait_seconds en GlueRunner.run_and_wait (item 7)`
|
|
302
|
+
|
|
303
|
+
### Checkpoint Fase 2
|
|
304
|
+
|
|
305
|
+
- [ ] Tests verdes
|
|
306
|
+
- [ ] Antipatrón 14 actualizado
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Fase 3 — Item 8: `Configuration#validate!` (P1)
|
|
311
|
+
|
|
312
|
+
**Roadmap:** [Item 8](../IMPROVEMENT_PLAN.md#item-8--configurationvalidate)
|
|
313
|
+
|
|
314
|
+
### Contexto
|
|
315
|
+
|
|
316
|
+
`Configuration` no valida invariantes. Errores típicos (storage_mode inválido, aws_region faltante con `:s3`, db_* faltantes en Engine) se manifiestan tarde con errores oscuros (`NoMethodError`, `Aws::Errors`, `PG::ConnectionBad`).
|
|
317
|
+
|
|
318
|
+
**Importante:** item 1 de v0.2.0 hizo `aws_access_key_id`/`aws_secret_access_key` **opcionales** (credential_chain). `validate!` NO debe exigirlos, solo `aws_region`.
|
|
319
|
+
|
|
320
|
+
### 3.1 Implementación
|
|
321
|
+
|
|
322
|
+
- [ ] Editar `lib/data_drain/configuration.rb`:
|
|
323
|
+
```ruby
|
|
324
|
+
# Valida invariantes generales (storage_mode + AWS si aplica).
|
|
325
|
+
# Llamado por FileIngestor#initialize y GlueRunner.run_and_wait.
|
|
326
|
+
#
|
|
327
|
+
# @raise [DataDrain::ConfigurationError]
|
|
328
|
+
def validate!
|
|
329
|
+
validate_storage_mode!
|
|
330
|
+
validate_aws_config! if storage_mode.to_sym == :s3
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Valida además las credenciales PostgreSQL.
|
|
334
|
+
# Llamado por Engine#initialize.
|
|
335
|
+
#
|
|
336
|
+
# @raise [DataDrain::ConfigurationError]
|
|
337
|
+
def validate_for_engine!
|
|
338
|
+
validate!
|
|
339
|
+
validate_db_config!
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
private
|
|
343
|
+
|
|
344
|
+
def validate_storage_mode!
|
|
345
|
+
return if %i[local s3].include?(storage_mode.to_sym)
|
|
346
|
+
|
|
347
|
+
raise DataDrain::ConfigurationError,
|
|
348
|
+
"storage_mode debe ser :local o :s3, recibido #{storage_mode.inspect}"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def validate_aws_config!
|
|
352
|
+
return unless aws_region.nil? || aws_region.to_s.empty?
|
|
353
|
+
|
|
354
|
+
raise DataDrain::ConfigurationError,
|
|
355
|
+
"aws_region es obligatorio con storage_mode = :s3"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# NOTA (review big-pickle riesgo 1): db_port se excluye porque tiene
|
|
359
|
+
# default 5432 — la validación siempre pasaría, sería código muerto.
|
|
360
|
+
# Si alguien lo setea a nil intencionalmente, Postgres rompe con error
|
|
361
|
+
# descriptivo igual.
|
|
362
|
+
#
|
|
363
|
+
# NOTA (review big-pickle riesgo 2): db_pass se excluye porque puede
|
|
364
|
+
# ser nil cuando Postgres usa auth peer/trust (sockets locales) o IAM
|
|
365
|
+
# (RDS IAM authentication). Requerirlo rompería esos casos válidos.
|
|
366
|
+
def validate_db_config!
|
|
367
|
+
%i[db_host db_user db_name].each do |attr|
|
|
368
|
+
val = public_send(attr)
|
|
369
|
+
next unless val.nil? || val.to_s.empty?
|
|
370
|
+
|
|
371
|
+
raise DataDrain::ConfigurationError,
|
|
372
|
+
"config.#{attr} es obligatorio para Engine (storage_mode=#{storage_mode})"
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
- [ ] Editar `lib/data_drain/engine.rb#initialize`. Después de capturar `@config`:
|
|
378
|
+
```ruby
|
|
379
|
+
@config = DataDrain.configuration
|
|
380
|
+
@config.validate_for_engine!
|
|
381
|
+
@logger = @config.logger
|
|
382
|
+
@adapter = DataDrain::Storage.adapter
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
- [ ] Editar `lib/data_drain/file_ingestor.rb#initialize`. Después de capturar `@config`:
|
|
386
|
+
```ruby
|
|
387
|
+
@config = DataDrain.configuration
|
|
388
|
+
@config.validate!
|
|
389
|
+
@logger = @config.logger
|
|
390
|
+
@adapter = DataDrain::Storage.adapter
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
- [ ] Editar `lib/data_drain/glue_runner.rb.run_and_wait`. Al principio:
|
|
394
|
+
```ruby
|
|
395
|
+
def self.run_and_wait(job_name, arguments = {}, polling_interval: 30, max_wait_seconds: nil)
|
|
396
|
+
config = DataDrain.configuration
|
|
397
|
+
config.validate!
|
|
398
|
+
# ... resto
|
|
399
|
+
end
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### 3.2 Tests
|
|
403
|
+
|
|
404
|
+
- [ ] Crear `spec/data_drain/configuration_validate_spec.rb` (o agregar a `configuration_spec.rb`):
|
|
405
|
+
```ruby
|
|
406
|
+
RSpec.describe DataDrain::Configuration do
|
|
407
|
+
describe "#validate!" do
|
|
408
|
+
it "no levanta con storage_mode :local" do
|
|
409
|
+
config = described_class.new
|
|
410
|
+
config.storage_mode = :local
|
|
411
|
+
expect { config.validate! }.not_to raise_error
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
it "levanta con storage_mode :foo" do
|
|
415
|
+
config = described_class.new
|
|
416
|
+
config.storage_mode = :foo
|
|
417
|
+
expect { config.validate! }.to raise_error(DataDrain::ConfigurationError, /storage_mode/)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
it "levanta con storage_mode :s3 sin aws_region" do
|
|
421
|
+
config = described_class.new
|
|
422
|
+
config.storage_mode = :s3
|
|
423
|
+
config.aws_region = nil
|
|
424
|
+
expect { config.validate! }.to raise_error(DataDrain::ConfigurationError, /aws_region/)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
it "no levanta con storage_mode :s3 + aws_region, sin credenciales (credential_chain)" do
|
|
428
|
+
config = described_class.new
|
|
429
|
+
config.storage_mode = :s3
|
|
430
|
+
config.aws_region = "us-east-1"
|
|
431
|
+
expect { config.validate! }.not_to raise_error
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
describe "#validate_for_engine!" do
|
|
436
|
+
it "levanta sin db_host" do
|
|
437
|
+
config = described_class.new
|
|
438
|
+
config.db_host = nil
|
|
439
|
+
config.db_user = "u"
|
|
440
|
+
config.db_name = "d"
|
|
441
|
+
expect { config.validate_for_engine! }.to raise_error(DataDrain::ConfigurationError, /db_host/)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
it "levanta sin db_name" do
|
|
445
|
+
config = described_class.new
|
|
446
|
+
config.db_user = "u"
|
|
447
|
+
config.db_name = nil
|
|
448
|
+
expect { config.validate_for_engine! }.to raise_error(DataDrain::ConfigurationError, /db_name/)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
it "no levanta con todos los campos requeridos seteados" do
|
|
452
|
+
config = described_class.new
|
|
453
|
+
config.db_user = "u"
|
|
454
|
+
config.db_name = "d"
|
|
455
|
+
# db_pass intencionalmente nil — válido con auth peer/trust/IAM
|
|
456
|
+
expect { config.validate_for_engine! }.not_to raise_error
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
it "no levanta con db_pass nil (auth peer/trust/IAM)" do
|
|
460
|
+
config = described_class.new
|
|
461
|
+
config.db_user = "u"
|
|
462
|
+
config.db_pass = nil
|
|
463
|
+
config.db_name = "d"
|
|
464
|
+
expect { config.validate_for_engine! }.not_to raise_error
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
- [ ] Agregar a `engine_spec.rb` bloque `describe "validación de configuración"`:
|
|
471
|
+
```ruby
|
|
472
|
+
it "levanta ConfigurationError si db_name falta" do
|
|
473
|
+
DataDrain.configure { |c| c.db_user = "u"; c.db_name = nil }
|
|
474
|
+
expect do
|
|
475
|
+
described_class.new(base_options.merge(table_name: "versions"))
|
|
476
|
+
end.to raise_error(DataDrain::ConfigurationError, /db_name/)
|
|
477
|
+
ensure
|
|
478
|
+
DataDrain.reset_configuration!
|
|
479
|
+
end
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
- [ ] Tests similares en `file_ingestor_spec.rb` y `glue_runner_spec.rb`.
|
|
483
|
+
|
|
484
|
+
### 3.3 Impacto backward-compat
|
|
485
|
+
|
|
486
|
+
- [ ] **Riesgo:** si algún caller actual tiene config con `db_user=""` o `db_name=nil` (estaba roto pero no se ejecutaba el path), ahora romperá en `Engine.new`.
|
|
487
|
+
- [ ] **⚠️ Step manual (review big-pickle riesgo 4):** agentes en esta gema aislada NO tienen acceso al monorepo Wispro. Ejecutar manualmente desde el monorepo:
|
|
488
|
+
```bash
|
|
489
|
+
cd ~/src/wispro-monorepo # o donde esté
|
|
490
|
+
rg "DataDrain.configure" -A 20 --type ruby
|
|
491
|
+
rg "DataDrain::Engine.new" -A 10 --type ruby
|
|
492
|
+
```
|
|
493
|
+
Verificar que todos los campos requeridos (db_host, db_user, db_name; aws_region si :s3) están seteados. Documentar callers encontrados:
|
|
494
|
+
> Callers con config incompleta: ___________________
|
|
495
|
+
- [ ] Documentar en CHANGELOG como "BREAKING preventivo" (similar al item 2 de v0.2.0).
|
|
496
|
+
|
|
497
|
+
### 3.4 Validación local
|
|
498
|
+
|
|
499
|
+
- [ ] `bundle exec rspec`
|
|
500
|
+
- [ ] `bundle exec rubocop lib/`
|
|
501
|
+
|
|
502
|
+
### 3.5 Docs
|
|
503
|
+
|
|
504
|
+
- [ ] `skill/references/api-detallada.md` sección Configuration — agregar `#validate!` y `#validate_for_engine!`
|
|
505
|
+
- [ ] `CLAUDE.md` sección "Configuración" con nota sobre validación automática
|
|
506
|
+
- [ ] `skill/references/antipatrones.md` — agregar nuevo antipatrón: "No llamar `Engine.new` con `db_name` faltante esperando que 'use el default' — ahora falla rápido con error descriptivo".
|
|
507
|
+
|
|
508
|
+
### 3.6 Commit
|
|
509
|
+
|
|
510
|
+
- [ ] Commit: `feat(config): Configuration#validate! invocada en Engine/FileIngestor/GlueRunner (item 8)`
|
|
511
|
+
|
|
512
|
+
### Checkpoint Fase 3
|
|
513
|
+
|
|
514
|
+
- [ ] Tests verdes (incluyendo engine_spec / file_ingestor_spec actualizados)
|
|
515
|
+
- [ ] Coverage no bajó
|
|
516
|
+
- [ ] Documentado como BREAKING preventivo en CHANGELOG (borrador)
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## Fase 4 — Item 11a: Docs Postgres tuning por tamaño de tabla (P1)
|
|
521
|
+
|
|
522
|
+
**Roadmap:** [Item 11a](../IMPROVEMENT_PLAN.md#item-11a--documentación-de-postgres-tuning-por-tamaño-de-tabla)
|
|
523
|
+
|
|
524
|
+
### Contexto
|
|
525
|
+
|
|
526
|
+
DataDrain no documenta tuning de Postgres para purgas masivas. Items recurrentes: ¿qué índice ayuda al DELETE en lotes? ¿cuándo migrar a particionamiento? ¿cómo diagnosticar purgas lentas?
|
|
527
|
+
|
|
528
|
+
La skill `postgresql-optimization` (en `.agents/skills/`) aporta material base sobre índices, EXPLAIN ANALYZE, `pg_stat_statements`, particionamiento.
|
|
529
|
+
|
|
530
|
+
### 4.1 Crear `skill/references/postgres-tuning.md`
|
|
531
|
+
|
|
532
|
+
Estructura sugerida:
|
|
533
|
+
|
|
534
|
+
```markdown
|
|
535
|
+
# Postgres Tuning para DataDrain
|
|
536
|
+
|
|
537
|
+
Guía operacional para tablas que DataDrain archiva y purga. Cubre índices,
|
|
538
|
+
VACUUM, particionamiento y diagnóstico.
|
|
539
|
+
|
|
540
|
+
## Tabla de decisión por tamaño
|
|
541
|
+
|
|
542
|
+
| Tamaño | Estrategia |
|
|
543
|
+
|--------|-----------|
|
|
544
|
+
| <10GB | Índice composite `(created_at, pk)` con `CREATE INDEX CONCURRENTLY` |
|
|
545
|
+
| 10-100GB | Mismo + `SET maintenance_work_mem='4GB'` + checklist |
|
|
546
|
+
| 100GB-1TB | Particionamiento declarativo por mes |
|
|
547
|
+
| >1TB | Particionamiento obligatorio + `DROP PARTITION` reemplaza DELETE |
|
|
548
|
+
|
|
549
|
+
## Índice recomendado
|
|
550
|
+
|
|
551
|
+
Para tablas <100GB, DataDrain se beneficia de un índice composite:
|
|
552
|
+
|
|
553
|
+
CREATE INDEX CONCURRENTLY idx_versions_created_at_id
|
|
554
|
+
ON versions (created_at, id);
|
|
555
|
+
|
|
556
|
+
El DELETE en batches usa `WHERE created_at >= X AND created_at < Y` + `IN (SELECT id LIMIT N)`.
|
|
557
|
+
El índice composite lo convierte en index scan por rango + acceso directo al id.
|
|
558
|
+
|
|
559
|
+
### Checklist pre-`CREATE INDEX CONCURRENTLY`
|
|
560
|
+
|
|
561
|
+
- [ ] Tamaño actual: `SELECT pg_size_pretty(pg_total_relation_size('versions'));`
|
|
562
|
+
- [ ] Espacio libre disco (>2x tabla)
|
|
563
|
+
- [ ] `SET maintenance_work_mem = '4GB';` (sesión)
|
|
564
|
+
- [ ] `SET statement_timeout = 0;`
|
|
565
|
+
- [ ] Ventana de baja carga
|
|
566
|
+
- [ ] Plan rollback: `DROP INDEX CONCURRENTLY` si satura I/O
|
|
567
|
+
|
|
568
|
+
### Riesgos de `CONCURRENTLY`
|
|
569
|
+
|
|
570
|
+
1. **Dos pasadas** (puede tardar horas en 500GB)
|
|
571
|
+
2. **I/O sostenido** (satura IOPS en EBS gp3 sin provisioned)
|
|
572
|
+
3. **Puede fallar y dejar índice INVALID** → recuperar con `DROP INDEX CONCURRENTLY idx; CREATE INDEX CONCURRENTLY idx ...`
|
|
573
|
+
4. **Espacio en disco alto** durante build (sort externo si `maintenance_work_mem` bajo)
|
|
574
|
+
|
|
575
|
+
## VACUUM ANALYZE post-purga
|
|
576
|
+
|
|
577
|
+
En tablas no particionadas, purgar millones de rows deja dead tuples.
|
|
578
|
+
Sin VACUUM, el espacio no se libera y los seq scan recorren páginas vacías.
|
|
579
|
+
|
|
580
|
+
VACUUM ANALYZE versions;
|
|
581
|
+
|
|
582
|
+
Item 5 del roadmap agrega `config.vacuum_after_purge` para automatizar esto.
|
|
583
|
+
Hasta v0.3.0, correr manualmente después de cada `Engine#call` en tablas
|
|
584
|
+
grandes no particionadas.
|
|
585
|
+
|
|
586
|
+
**NO usar `VACUUM FULL`** — bloquea la tabla entera (ACCESS EXCLUSIVE lock).
|
|
587
|
+
|
|
588
|
+
## Diagnóstico de purga lenta
|
|
589
|
+
|
|
590
|
+
-- Plan del DELETE en lotes
|
|
591
|
+
EXPLAIN (ANALYZE, BUFFERS)
|
|
592
|
+
DELETE FROM versions
|
|
593
|
+
WHERE id IN (
|
|
594
|
+
SELECT id FROM versions
|
|
595
|
+
WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01'
|
|
596
|
+
LIMIT 5000
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
-- Sesiones activas sobre la tabla
|
|
600
|
+
SELECT pid, state, wait_event, query_start, query
|
|
601
|
+
FROM pg_stat_activity
|
|
602
|
+
WHERE query LIKE '%versions%'
|
|
603
|
+
AND state != 'idle';
|
|
604
|
+
|
|
605
|
+
-- Estadísticas de la tabla
|
|
606
|
+
SELECT relname, n_live_tup, n_dead_tup, last_vacuum, last_autovacuum
|
|
607
|
+
FROM pg_stat_user_tables
|
|
608
|
+
WHERE relname = 'versions';
|
|
609
|
+
|
|
610
|
+
-- Top queries lentas (requiere pg_stat_statements)
|
|
611
|
+
SELECT substring(query, 1, 100) AS query, calls, mean_exec_time, rows
|
|
612
|
+
FROM pg_stat_statements
|
|
613
|
+
WHERE query LIKE '%versions%'
|
|
614
|
+
ORDER BY mean_exec_time DESC
|
|
615
|
+
LIMIT 10;
|
|
616
|
+
|
|
617
|
+
## Particionamiento declarativo (tablas > 100GB)
|
|
618
|
+
|
|
619
|
+
Migrar a tabla particionada cambia DataDrain de "DELETE masivo throttled" a
|
|
620
|
+
"DROP PARTITION instantáneo".
|
|
621
|
+
|
|
622
|
+
### Setup
|
|
623
|
+
|
|
624
|
+
-- 1. Crear tabla particionada (vacía, misma estructura que versions)
|
|
625
|
+
CREATE TABLE versions_new (
|
|
626
|
+
id UUID PRIMARY KEY,
|
|
627
|
+
created_at TIMESTAMP NOT NULL,
|
|
628
|
+
... -- resto de columnas
|
|
629
|
+
) PARTITION BY RANGE (created_at);
|
|
630
|
+
|
|
631
|
+
-- 2. Crear partición por mes
|
|
632
|
+
CREATE TABLE versions_2026_03 PARTITION OF versions_new
|
|
633
|
+
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
|
|
634
|
+
|
|
635
|
+
-- 3. Migrar datos (lotes, una partición por vez)
|
|
636
|
+
INSERT INTO versions_2026_03
|
|
637
|
+
SELECT * FROM versions
|
|
638
|
+
WHERE created_at >= '2026-03-01' AND created_at < '2026-04-01';
|
|
639
|
+
|
|
640
|
+
-- 4. Swap nombres (downtime mínimo)
|
|
641
|
+
BEGIN;
|
|
642
|
+
ALTER TABLE versions RENAME TO versions_old;
|
|
643
|
+
ALTER TABLE versions_new RENAME TO versions;
|
|
644
|
+
COMMIT;
|
|
645
|
+
|
|
646
|
+
### Beneficio para DataDrain
|
|
647
|
+
|
|
648
|
+
-- v0.2.x: DELETE en lotes, VACUUM después, horas en TB
|
|
649
|
+
DataDrain::Engine.new(...).call
|
|
650
|
+
|
|
651
|
+
-- Con particiones: DataDrain sigue funcionando pero si el rango
|
|
652
|
+
-- coincide con una partición, el operador puede hacer:
|
|
653
|
+
DROP TABLE versions_2026_03; -- instantáneo, sin bloat
|
|
654
|
+
|
|
655
|
+
DataDrain no detecta particiones automáticamente (futuro item). Hoy el
|
|
656
|
+
operador decide.
|
|
657
|
+
|
|
658
|
+
## Referencias
|
|
659
|
+
|
|
660
|
+
- Skill: `.agents/skills/postgresql-optimization/SKILL.md`
|
|
661
|
+
- PG docs: https://www.postgresql.org/docs/current/ddl-partitioning.html
|
|
662
|
+
- Item 5 roadmap (VACUUM automático): ../IMPROVEMENT_PLAN.md#item-5
|
|
663
|
+
- Item 11b roadmap (warning runtime): ../IMPROVEMENT_PLAN.md#item-11b
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### 4.2 Cross-references
|
|
667
|
+
|
|
668
|
+
- [ ] Agregar link en `skill/SKILL.md` sección "Referencias":
|
|
669
|
+
```markdown
|
|
670
|
+
- [Postgres Tuning](references/postgres-tuning.md) — Índices, VACUUM, particionamiento y diagnóstico
|
|
671
|
+
```
|
|
672
|
+
- [ ] Agregar sección "Postgres tuning" en `CLAUDE.md` con link al doc completo (dejar resumen de 5-10 líneas en CLAUDE.md y link al doc extenso en `skill/references/`).
|
|
673
|
+
- [ ] Agregar en `README.md` sección "Performance" (o "Tuning") breve nota apuntando a la skill para detalles.
|
|
674
|
+
|
|
675
|
+
### 4.3 Validación
|
|
676
|
+
|
|
677
|
+
- [ ] `bundle exec rspec` (no toca código, debería pasar igual)
|
|
678
|
+
- [ ] Verificar links relativos en markdown (abrir local en editor/GitHub preview)
|
|
679
|
+
|
|
680
|
+
### 4.4 Commit
|
|
681
|
+
|
|
682
|
+
- [ ] Commit: `docs: postgres tuning por tamaño de tabla (item 11a)`
|
|
683
|
+
|
|
684
|
+
### Checkpoint Fase 4
|
|
685
|
+
|
|
686
|
+
- [ ] `postgres-tuning.md` creado y linkeado
|
|
687
|
+
- [ ] CLAUDE.md y README actualizados
|
|
688
|
+
- [ ] SKILL.md sección Referencias actualizada
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## Fase 4.5 — Cleanup del review de v0.2.0 PR (A2, A3, A4, B1)
|
|
693
|
+
|
|
694
|
+
**Contexto:** items detectados durante el review de v0.2.0 (PR #6) que quedaron abiertos. Cheap, limpian deuda mientras el contexto está fresco.
|
|
695
|
+
|
|
696
|
+
### 4.5.1 A2 — Comment en `disconnect!` rescue
|
|
697
|
+
|
|
698
|
+
- [ ] Editar `lib/data_drain/record.rb`. Encontrar:
|
|
699
|
+
```ruby
|
|
700
|
+
rescue StandardError # rubocop:disable Lint/SuppressedException
|
|
701
|
+
end
|
|
702
|
+
```
|
|
703
|
+
Reemplazar por:
|
|
704
|
+
```ruby
|
|
705
|
+
rescue StandardError
|
|
706
|
+
# Silenciamos para no romper el flujo del thread durante cleanup.
|
|
707
|
+
# disconnect! se invoca típicamente en middlewares (Sidekiq/Puma);
|
|
708
|
+
# una excepción acá propagaría a jobs no relacionados.
|
|
709
|
+
nil
|
|
710
|
+
end
|
|
711
|
+
```
|
|
712
|
+
Nota: agregar `nil` explícito permite quitar el `rubocop:disable Lint/SuppressedException`.
|
|
713
|
+
|
|
714
|
+
### 4.5.2 A3 — Rename test + agregar cobertura real string/symbol keys
|
|
715
|
+
|
|
716
|
+
- [ ] Editar `spec/data_drain/record_spec.rb`, bloque `describe ".build_query_path"`:
|
|
717
|
+
- Renombrar test actual:
|
|
718
|
+
```ruby
|
|
719
|
+
it "interpola symbol values como strings" do
|
|
720
|
+
path = record_class.send(:build_query_path, { year: :integer, month: :integer })
|
|
721
|
+
expect(path).to include("year=integer")
|
|
722
|
+
end
|
|
723
|
+
```
|
|
724
|
+
- Agregar test real de string keys:
|
|
725
|
+
```ruby
|
|
726
|
+
it "acepta string keys en el hash de particiones" do
|
|
727
|
+
path = record_class.send(:build_query_path, { "year" => 2026, "month" => 3 })
|
|
728
|
+
expect(path).to include("year=2026")
|
|
729
|
+
expect(path).to include("month=3")
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
it "combina string keys y symbol keys en el mismo hash" do
|
|
733
|
+
path = record_class.send(:build_query_path, { "year" => 2026, month: 3 })
|
|
734
|
+
expect(path).to include("year=2026")
|
|
735
|
+
expect(path).to include("month=3")
|
|
736
|
+
end
|
|
737
|
+
```
|
|
738
|
+
Razón: el código hace `partitions[k.to_sym] || partitions[k.to_s]`; sin estos tests la rama string-keys no está cubierta.
|
|
739
|
+
|
|
740
|
+
### 4.5.3 A4 — Cerrar DuckDB en `record_spec.rb#before(:all)`
|
|
741
|
+
|
|
742
|
+
- [ ] Editar `spec/data_drain/record_spec.rb`, bloque `before(:all)`:
|
|
743
|
+
```ruby
|
|
744
|
+
before(:all) do
|
|
745
|
+
path = "spec/fixtures/test_archive"
|
|
746
|
+
FileUtils.rm_rf(path)
|
|
747
|
+
db = DuckDB::Database.open(":memory:")
|
|
748
|
+
conn = db.connect
|
|
749
|
+
conn.query(<<~SQL)
|
|
750
|
+
COPY (...) TO '#{path}' (...);
|
|
751
|
+
SQL
|
|
752
|
+
conn.close # ← agregar
|
|
753
|
+
db.close # ← agregar
|
|
754
|
+
end
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
### 4.5.4 B1 — Reordenar `public`/`private` en `storage/s3.rb`
|
|
758
|
+
|
|
759
|
+
- [ ] Editar `lib/data_drain/storage/s3.rb`:
|
|
760
|
+
- Actualmente tiene toggling `private` → definiciones → `public` → `build_path` → `destroy_partitions` → `private` → `delete_in_batches`.
|
|
761
|
+
- Reordenar: todos los métodos públicos primero (`setup_duckdb`, `build_path`, `destroy_partitions`), luego `private` una sola vez, luego todos los privados (`create_s3_secret`, `escape_sql`, `delete_in_batches`).
|
|
762
|
+
- No cambia lógica, solo orden.
|
|
763
|
+
- [ ] Verificar que quita el `public` re-toggle.
|
|
764
|
+
|
|
765
|
+
### 4.5.5 Validación Fase 4.5
|
|
766
|
+
|
|
767
|
+
- [ ] `bundle exec rspec` — todo verde
|
|
768
|
+
- [ ] `bundle exec rubocop lib/` — sin ofensas
|
|
769
|
+
- [ ] `bundle exec rubocop lib/data_drain/storage/s3.rb` específicamente (por el reorder)
|
|
770
|
+
|
|
771
|
+
### 4.5.6 Commits
|
|
772
|
+
|
|
773
|
+
Hacer commits separados por cleanup (son independientes):
|
|
774
|
+
- [ ] `fix(record): comment en disconnect! rescue (cleanup A2)`
|
|
775
|
+
- [ ] `test(record): agregar cobertura string vs symbol keys (cleanup A3)`
|
|
776
|
+
- [ ] `test(record): cerrar DuckDB conn+db en before(:all) (cleanup A4)`
|
|
777
|
+
- [ ] `refactor(storage/s3): reordenar public/private (cleanup B1)`
|
|
778
|
+
|
|
779
|
+
### Checkpoint Fase 4.5
|
|
780
|
+
|
|
781
|
+
- [ ] 4 commits cleanup (A2, A3, A4, B1)
|
|
782
|
+
- [ ] Coverage estable o sube (A3 agrega ramas cubiertas)
|
|
783
|
+
- [ ] Sin regresiones
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
## Fase 5 — Release
|
|
788
|
+
|
|
789
|
+
### 5.1 Lint global
|
|
790
|
+
|
|
791
|
+
- [ ] `bundle exec rubocop lib/` sin ofensas
|
|
792
|
+
- [ ] `bundle exec rspec` pasa, coverage ≥ 80%
|
|
793
|
+
|
|
794
|
+
### 5.2 CHANGELOG
|
|
795
|
+
|
|
796
|
+
- [ ] Editar `CHANGELOG.md`, agregar al tope:
|
|
797
|
+
```markdown
|
|
798
|
+
## [0.2.2] - 2026-XX-XX
|
|
799
|
+
|
|
800
|
+
### Security
|
|
801
|
+
- `Observability#safe_log` filtra secretos con regex en lugar de claves exactas.
|
|
802
|
+
Ahora captura `db_password`, `aws_secret_access_key`, `bearer_token`, `private_key`,
|
|
803
|
+
`*credential*`, etc. (item 9)
|
|
804
|
+
|
|
805
|
+
### Features
|
|
806
|
+
- `GlueRunner.run_and_wait` acepta `max_wait_seconds:` para evitar bloqueo
|
|
807
|
+
indefinido en jobs colgados. Default `nil` (sin límite, comportamiento previo).
|
|
808
|
+
Emite `glue_runner.timeout` y levanta `DataDrain::Error`. (item 7)
|
|
809
|
+
- `Configuration#validate!` y `Configuration#validate_for_engine!` invocados
|
|
810
|
+
automáticamente en `Engine`, `FileIngestor` y `GlueRunner`. Falla rápido con
|
|
811
|
+
errores descriptivos si falta configuración (ej. `aws_region` con `:s3`,
|
|
812
|
+
`db_*` con Engine). (item 8)
|
|
813
|
+
|
|
814
|
+
### Docs
|
|
815
|
+
- `skill/references/postgres-tuning.md`: guía de tuning de Postgres por tamaño
|
|
816
|
+
de tabla (índices, VACUUM, particionamiento, diagnóstico). (item 11a)
|
|
817
|
+
|
|
818
|
+
### Cleanups (review PR #6)
|
|
819
|
+
- Fix typo `依赖` en CHANGELOG v0.2.1 (A1).
|
|
820
|
+
- Comment explicativo en `Record.disconnect!` rescue (A2).
|
|
821
|
+
- Cobertura real string-keys vs symbol-keys en `Record.build_query_path` (A3).
|
|
822
|
+
- Cerrar conn+db en `record_spec.rb#before(:all)` para evitar memory leak en suite (A4).
|
|
823
|
+
- Reorder `public`/`private` en `storage/s3.rb` (B1).
|
|
824
|
+
|
|
825
|
+
### BREAKING (preventivo)
|
|
826
|
+
- `Engine.new` / `FileIngestor.new` / `GlueRunner.run_and_wait` ahora levantan
|
|
827
|
+
`DataDrain::ConfigurationError` en el boot si la configuración está incompleta.
|
|
828
|
+
Antes fallaban tarde con errores oscuros (`NoMethodError`, `PG::ConnectionBad`).
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### 5.3 Bump versión
|
|
832
|
+
|
|
833
|
+
- [ ] Editar `lib/data_drain/version.rb`: `VERSION = "0.2.2"`
|
|
834
|
+
- [ ] `bundle install` (actualiza `Gemfile.lock`)
|
|
835
|
+
|
|
836
|
+
### 5.4 Actualizar roadmap
|
|
837
|
+
|
|
838
|
+
- [ ] Editar `docs/IMPROVEMENT_PLAN.md`:
|
|
839
|
+
- Items 7, 8, 9, 11a: `[ ]` → `[x]`
|
|
840
|
+
- Actualizar fecha "Última actualización"
|
|
841
|
+
|
|
842
|
+
### 5.5 Commit release
|
|
843
|
+
|
|
844
|
+
- [ ] Commit: `chore: release v0.2.2 — items P1 (filtros, timeout Glue, config validate, docs PG)`
|
|
845
|
+
|
|
846
|
+
### 5.6 Merge y tag
|
|
847
|
+
|
|
848
|
+
- [ ] `git push origin feature/v0.2.2`
|
|
849
|
+
- [ ] Abrir PR a `main` con cuerpo basado en CHANGELOG
|
|
850
|
+
- [ ] Esperar CI verde
|
|
851
|
+
- [ ] Mergear
|
|
852
|
+
- [ ] Tag: `git tag v0.2.2 && git push origin v0.2.2`
|
|
853
|
+
|
|
854
|
+
### 5.7 Post-merge
|
|
855
|
+
|
|
856
|
+
- [ ] Archivar este plan: `git mv docs/execution/v0.2.2.md docs/execution/archive/v0.2.2.md`
|
|
857
|
+
- [ ] Commit: `chore: archive v0.2.2 plan, mark items 7/8/9/11a [x]`
|
|
858
|
+
|
|
859
|
+
---
|
|
860
|
+
|
|
861
|
+
## Validación final
|
|
862
|
+
|
|
863
|
+
- [ ] `bundle exec rspec` verde, coverage ≥ 80%
|
|
864
|
+
- [ ] `bundle exec rubocop lib/` sin ofensas
|
|
865
|
+
- [ ] CHANGELOG completo con fecha real
|
|
866
|
+
- [ ] Version = `0.2.2`
|
|
867
|
+
- [ ] Tag `v0.2.2` creado y pusheado
|
|
868
|
+
- [ ] Items 7, 8, 9, 11a marcados `[x]` en roadmap
|
|
869
|
+
- [ ] Plan archivado
|
|
870
|
+
|
|
871
|
+
---
|
|
872
|
+
|
|
873
|
+
## Plan B — si algún item se atasca
|
|
874
|
+
|
|
875
|
+
| Si... | Entonces... |
|
|
876
|
+
|-------|-------------|
|
|
877
|
+
| Item 8 rompe callers en monorepo Wispro (validate_for_engine!) | Relajar validación: log warning en lugar de raise. Re-habilitar raise en v0.3.0 tras coordinar. |
|
|
878
|
+
| Item 7 tests con mock de Process.clock_gettime son flakey | Usar `Timecop` o abstraer el monotonic clock a un método de clase stubeable. |
|
|
879
|
+
| Item 11a toma más de 1 día | Cortar a lo esencial: tabla decisión + checklist índice + diagnóstico SQL. Particionamiento puede ir a v0.3.0. |
|
|
880
|
+
| CI falla por coverage | Revisar SimpleCov — probablemente los nuevos branches de `validate!` (item 8) no están cubiertos. Agregar tests específicos. |
|
|
881
|
+
|
|
882
|
+
---
|
|
883
|
+
|
|
884
|
+
## Notas para el agente que ejecuta
|
|
885
|
+
|
|
886
|
+
- **Cada fase cierra con commit atómico.** No commit si tests rojos.
|
|
887
|
+
- **Antes de cada commit:** `bundle exec rspec` + `bundle exec rubocop lib/`.
|
|
888
|
+
- **Item 8 (validate!) es el más invasivo.** Hacer última revisión de callers del monorepo antes de mergear.
|
|
889
|
+
- **Items 9, 7, 11a pueden paralelizarse** si hay múltiples agentes. Item 8 debe ir solo (toca 3 archivos core).
|
|
890
|
+
- **Si CI agrega Ruby 3.2, 3.3 al matrix (item 14c pendiente)**, verificar que los cambios pasan en todas.
|
|
891
|
+
- **Actualizar `skill/` en cada fase** — no acumular deuda de docs para el final.
|