auto: sync OpenClaw config 2026-07-28 14:00

This commit is contained in:
2026-07-28 14:00:29 +08:00
parent f3c5e82269
commit 0f85da3245
32 changed files with 4142 additions and 11 deletions
+148
View File
@@ -0,0 +1,148 @@
# Debugging — Symptom to Cause
Work symptom-first. Each chain is ordered by probability, and every step is a check you can run, not a guess. Wrong results outrank slow results: a fast query returning the wrong rows never raises an error.
Contents: First Three · Wrong Row Count · Wrong Totals · Query Slow · Was Fast Yesterday · Index Ignored · Hangs · Deadlock · Connection Errors · Constraint Violations · Disk Full · Encoding · Works In One Client · Migration Failed · Replica Disagrees
## The Universal First Three
1. **Read the actual statement, not the intent.** For ORM-generated SQL, turn on query logging and copy the emitted text; for dynamic SQL, log the final string with parameters bound.
2. **Count before you optimize.** `SELECT COUNT(*)` on each base table with only its own filters. If a base count already surprises you, the bug is upstream of the join.
3. **`EXPLAIN (ANALYZE, BUFFERS)`** the real statement with real parameters. A plan for `user_id = 1` can differ from `user_id = 99` when one value is a most-common-value in the stats.
## Wrong Row Count (too many, too few, duplicates)
1. Strip the query to one table plus its `WHERE`. Add joins back one at a time and re-count after each — the join that changes the count names the bug.
2. Count grew → 1:N fan-out. Check the join key's uniqueness: `SELECT key, COUNT(*) FROM t GROUP BY key HAVING COUNT(*) > 1`. Fix by pre-aggregating, not by `DISTINCT` (SKILL.md Traps).
3. Count shrank → a `LEFT JOIN` with a predicate in `WHERE` (now an inner join, with no error), or a `NOT IN` against a subquery containing NULL (SKILL.md rule 6).
4. Count differs between two "equivalent" queries → collation or case sensitivity: MySQL's `_ci` collations match `'A' = 'a'`, PostgreSQL does not.
5. Trailing-space equality: `CHAR(n)` pads, and some engines ignore trailing spaces in comparison. `'a ' = 'a'` is true on MySQL `CHAR`, false on PostgreSQL `TEXT`.
6. Count changes between runs of the same query → concurrent writes, or a non-deterministic `LIMIT` without `ORDER BY`.
## Wrong Totals (sums, averages, percentages)
- Sum too high → fan-out (above). Verify by comparing `SUM(x)` against `SUM(x) / COUNT(DISTINCT join_key)`-shaped intuition, then rewrite with a pre-aggregated CTE.
- Average is off → `AVG` skips NULLs, so the denominator is not the row count. `SUM(x) / COUNT(*)` and `AVG(x)` differ whenever `x` is nullable.
- Money off by cents → `FLOAT`/`REAL` storage. `0.1 + 0.2 != 0.3` accumulates over aggregation; the schema needs `NUMERIC`/`DECIMAL` (SKILL.md rule 7).
- Integer division: `SELECT 1/2` is `0` in PostgreSQL, MySQL, and SQL Server for integer operands. Cast one side: `x * 100.0 / y`.
- Percentages don't sum to 100 → rounding each row instead of the total; round once at the end.
- Totals differ from the dashboard → different timezone boundary for "today" or a soft-delete filter one query applies and the other doesn't.
## Query Slow
1. Is this query even the problem? Rank by total cost first (SKILL.md rule 9).
2. `EXPLAIN (ANALYZE, BUFFERS)`: find the node with the largest actual time, not the largest estimate.
3. Estimated vs actual rows off by more than 10× → stale statistics; `ANALYZE` the table and re-plan.
4. Sequential scan with a selective filter → the index is missing or disabled (→ Index Ignored).
5. Correct index, still slow → the query returns too many rows to matter. A million rows through the network is slow no matter the plan; paginate or aggregate server-side.
6. Fast standalone, slow in the app → parameter sniffing, a different search_path/session setting, or lock waiting rather than work (→ Hangs).
7. Everything is slow, not one query → server-level: cache hit ratio, connection saturation, or another workload.
## "It Was Fast Yesterday"
| Cause | Check |
|---|---|
| Data crossed a size threshold and the plan flipped | Compare current plan against the shape you remember; look for a switch from index scan to seq scan |
| Statistics went stale after a bulk load | `ANALYZE`, re-plan; PostgreSQL: `last_analyze` in `pg_stat_user_tables` |
| An index was dropped or left INVALID by a failed concurrent build | PostgreSQL: `SELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid` |
| Bloat from a long-running transaction | Oldest `xact_start` in `pg_stat_activity` |
| A new index made writes slow, not reads | Compare index count against write latency; every index is written on every insert |
| Cache no longer holds the working set | Cache hit ratio dropped below the 99% OLTP threshold |
| Someone added a `LEFT JOIN` for one column | Read the diff of the query, not just the plan |
| Version upgrade changed the planner | Compare plans across the versions before blaming data |
## Index Exists But Is Not Used
Check in this order; the first four cover most cases.
1. **Function or cast on the column.** `WHERE lower(email) = ?`, `WHERE created_at::date = ?`, or an implicit cast from a type mismatch all hide the column. Fix the predicate or add an expression index that matches the query text exactly.
2. **Wrong column order.** A composite `(a, b)` cannot serve `WHERE b = ?` (SKILL.md rule 3).
3. **Low selectivity.** Matching more than roughly 5-10% of rows, a scan is genuinely cheaper. Confirm with `SELECT COUNT(*) FILTER (WHERE <predicate>) * 100.0 / COUNT(*) FROM t`.
4. **Stale stats** making the planner think the predicate is unselective. `ANALYZE`, retry.
5. **`OR` across columns**, leading wildcard `LIKE '%x'`, or a non-C locale with `LIKE 'x%'` in PostgreSQL (SKILL.md Index Strategy).
6. **Different collation** between the index and the query's comparison (MySQL joins across `utf8mb4_general_ci` and `utf8mb4_0900_ai_ci` cannot use the index).
7. **The index is invalid or being built.** PostgreSQL `indisvalid = false`; MySQL `SHOW INDEX` `Comment` column.
8. Prove it before rewriting: force the choice temporarily (`SET enable_seqscan = off` in PostgreSQL, `FORCE INDEX` in MySQL) and compare actual times. If forcing the index is slower, the planner was right — never ship the hint as the fix.
## Query Hangs (no result, no error)
1. Is it running or waiting? PostgreSQL `pg_stat_activity.wait_event_type = 'Lock'` means waiting; MySQL `SHOW PROCESSLIST` state `Waiting for table metadata lock`.
2. Waiting → find the blocker (PostgreSQL `pg_blocking_pids(pid)`, MySQL `sys.innodb_lock_waits`) and decide: cancel it, or wait if it is nearly done.
3. Running with no output → it may be working correctly on too much data; `EXPLAIN` without `ANALYZE` returns instantly and shows the plan it chose.
4. Client shows nothing while the server is idle → the result is buffered by the driver, or the app never fetched; check the driver's cursor/fetch mode.
5. `ALTER TABLE` hangs → the lock queue, not the DDL. Every new query is now queued behind it: cancel, set `lock_timeout`, retry.
6. An open transaction in a REPL or notebook is the most common self-inflicted hang: an uncommitted `BEGIN` in another window holds the lock.
## Deadlock Detected
- The engine already picked a victim and rolled it back; the error is the report, not the failure. Read the log: PostgreSQL logs both statements, MySQL exposes the last one in `SHOW ENGINE INNODB STATUS`.
- Root cause is almost always inconsistent lock order across two code paths. Fix by ordering (`ORDER BY id FOR UPDATE`), not by retrying blindly.
- Deadlocks that appear only under load with no obvious two-row pattern → gap/next-key locks in MySQL REPEATABLE READ, or foreign-key locks taken on the parent row.
- Every application that writes concurrently needs a bounded retry on deadlock and serialization errors regardless.
## Connection Errors
| Error | Real cause | Move |
|---|---|---|
| "too many connections" / "remaining slots reserved" | App pools × instances exceed the server limit | Size pools from cores, add PgBouncer instead of raising `max_connections` |
| "connection refused" | Server not listening on that address, or wrong port | Check bind address and port before credentials |
| "no pg_hba.conf entry" / "Host is not allowed" | Host-based auth rules, not a bad password | Fix the auth rule for the client's network |
| "password authentication failed" for one app only | Different role than you tested with, or a rotated secret | Compare the role, not the password |
| "SSL required" / cert errors | Managed providers force TLS | Set the driver's SSL mode; do not disable verification to move on |
| Connections work then die after minutes | Idle timeout in a proxy/load balancer below the pool's `max_lifetime` | Set pool `max_lifetime` under the infrastructure timeout |
| Intermittent failures under load only | Pool exhaustion — waiters timing out, not the database refusing | Instrument pool wait time before touching the server |
## Constraint Violations
- Unique violation on insert that "should not exist" → a soft-deleted row still occupies the value, or the unique index is on a different column set than assumed.
- Unique violation under concurrency despite a check-then-insert → the check-then-insert race; use `INSERT ... ON CONFLICT`/`ON DUPLICATE KEY` or catch the violation.
- Foreign key violation on delete → children exist; decide `RESTRICT` vs `CASCADE` deliberately, and confirm the child FK column is indexed (SKILL.md rule 4).
- Foreign key violation on insert with a valid-looking id → different tenant, or the parent row was inserted in another uncommitted transaction.
- NOT NULL violation only in production → a default exists in one environment's schema and not the other; diff the schemas, don't reason about them.
- CHECK violation after a data import → the import brought values the constraint never saw; the constraint is right.
## Disk Full / Database Won't Accept Writes
1. Locate the consumer before deleting anything: table and index sizes ranked descending.
2. Common consumers in order: an unpartitioned events/audit/log table, bloat from dead tuples pinned by a long transaction, WAL retained by an inactive replication slot, an abandoned temp/sort spill.
3. `DELETE` does not return space to the filesystem on PostgreSQL or MySQL/InnoDB — it creates dead rows the vacuum must clean. Dropping a partition does.
4. Never `DROP` to free space during an incident until the backup is verified restorable.
5. PostgreSQL specifically: an inactive replication slot retains WAL forever and is the classic unannounced disk killer — check `pg_replication_slots` for `active = false`.
## Encoding and Character Problems
- `?` or `????` in place of accents/emoji → MySQL `utf8` (3-byte) instead of `utf8mb4`; the column, the table default, AND the client connection charset must all be `utf8mb4`.
- Mojibake (`é` for `é`) → UTF-8 bytes read as Latin-1 somewhere in the chain; find the single wrong layer rather than re-encoding twice.
- Import fails on "invalid byte sequence" → the file is not the encoding you declared; detect and convert before loading.
- Sorting looks wrong → collation, not encoding. `ORDER BY` follows the column's collation; changing it after the fact requires reindexing.
- Invisible characters (zero-width space, non-breaking space) pasted into data make equality fail with identical-looking strings; compare `length()` against the visible character count.
## Works In One Client, Fails In Another
| Difference | Check |
|---|---|
| Autocommit on/off | The GUI committed; your script left an open transaction |
| Session settings (`search_path`, `sql_mode`, timezone) | Compare `SHOW ALL` / `SHOW VARIABLES` between sessions |
| Different role or database | Same host, different privileges: diff the `GRANT`s and any RLS policy on the table |
| Statement/lock timeouts set per role | A short `statement_timeout` on the app role only |
| Prepared vs literal statements | The generic plan for a prepared statement can differ from the plan for literals |
| Driver-side type conversion | The driver casts a parameter to a type the index can't use |
## Migration Failed Halfway
1. Check the runner's state table first — a "dirty" flag means it knows it stopped mid-way; resolve that before rerunning anything.
2. MySQL has no transactional DDL: half the statements are already committed. Determine what actually applied by inspecting the schema, not the migration file.
3. Re-running a partially applied migration usually fails on "already exists". Write the repair as a NEW migration; never edit an applied one.
4. A migration that timed out may still be running server-side — kill the backend before retrying, or the retry deadlocks against it.
5. Prevention: run every migration against a restored copy of production-shaped data before it reaches production.
## Replica Returns Different Data
- Read-after-write on an async replica misses the write that just committed; route read-your-own-writes traffic to the primary.
- A long analytics query on a replica gets canceled by WAL replay conflicts — that is not a query bug.
- Replica lag rises with zero write traffic on the primary: the time-based lag metric grows unbounded when there is nothing to replay; alert only while writes are flowing.
- Counts differ permanently → the replica broke and is no longer replaying, or someone wrote to it. Compare replay position, not row counts.
## When You Are Truly Stuck
Reduce to the smallest reproduction: one table, minimal columns, the fewest rows that still show the behavior, on a scratch copy. Most "impossible" SQL bugs resolve at the moment the reproduction stops reproducing — the step you removed is the cause.