Глава 4. Чтение сложных текстов: ошибки, логи, changelog, спецификации

378 просмотров
0 лайков
0 в избранном
Войдите, чтобы поставить лайк. Лайков:

Введение

Английский текст, который разработчик читает чаще всего, написан не для людей. Сообщение об ошибке сочинила библиотека, stack trace выплюнул рантайм, changelog собрался из коммитов, а спецификацию писал юрист от инженерии. Это не проза. Это плотный технический язык с жёсткими шаблонами — и именно поэтому его можно научиться читать быстро, даже если разговорный английский у тебя пока на уровне «hello, my name is».

Навык этой главы — не «перевести весь текст», а найти место, где написано, что именно сломалось. Тот, кто читает ошибку слева направо с первой строки, тратит минуты. Тот, кто знает, что смысл почти всегда сидит в последней строке, а в stack trace — в первом кадре с твоим файлом, тратит секунды. Разница не в уровне языка. Разница в стратегии чтения.

Дальше по порядку: анатомия сообщения об ошибке и два десятка формул, которые закрывают почти всё; stack trace по слоям; словарь логов; Keep a Changelog и SemVer; ключевые слова спецификаций; и напоследок — как сформулировать поисковый запрос, который что-то найдёт.

Анатомия сообщения об ошибке

Сообщение собрано из трёх частей, и порядок почти всегда один: где (компонент, модуль, файл), что за класс проблемы (тип ошибки или код) и детали (что ожидалось, что получено, какой ресурс, какое значение). Вот классика из PostgreSQL:

django.db.utils.IntegrityError: duplicate key value violates unique constraint "blog_post_slug_key"
DETAIL:  Key (slug)=(hello-world) already exists.

Первая часть — django.db.utils.IntegrityError — это «где и какого класса»: ошибка целостности, пришла из слоя БД. Вторая — duplicate key value violates unique constraint — формула: значение ключа дублируется и нарушает ограничение уникальности. Третья, после DETAIL, — конкретика: поле slug, значение hello-world, уже занято. Слово violates в отрыве от контекста знать необязательно. Формулу целиком — обязательно.

Дальше — направление чтения. В Python, Java и Ruby полезная строка стоит в конце, а всё выше — путь, по которому исполнение туда пришло. В Go и JavaScript наоборот: сообщение сверху, кадры вызовов под ним. Так что первое действие с простынёй в терминале — понять, где здесь сообщение, а где трасса. Их путают чаще, чем кажется, и потом полчаса чинят не тот файл.

Правило большого пальца: если строка начинается с пробелов, слова at или File — это кадр трассы, а не сообщение. Сообщение — строка без отступа, обычно с двоеточием после имени типа ошибки.

Типовые формулы: словарь на каждый день

Формул мало. Два десятка закрывают почти всё, что попадётся за год работы, — и они одинаковы в Python, Go, Node и в логах чужого сервиса.

ФормулаДословноЧто это значит на практике
expected string, got intожидалась строка, получено целоеНесовпадение типов. Ищи место, где значение попало в функцию.
cannot resolve symbol / moduleне удаётся разрешить символИмя не найдено: опечатка, забытый импорт, не установленный пакет.
permission deniedв доступе отказаноПрава на файл, каталог, сокет; не тот пользователь в контейнере.
connection refusedсоединение отвергнутоПо адресу никто не слушает: сервис не поднялся или не тот порт.
deadline exceeded / timed outсрок превышенОтвет не пришёл за отведённое время. Смотри в сторону сети и медленных запросов.
unexpected tokenнеожиданный токенСинтаксис сломан раньше указанного места: лишняя запятая, незакрытая скобка.
x is not a functionx не является функциейВызвали то, что функцией не является: не тот импорт, undefined, опечатка.
nil pointer dereferenceразыменование нулевого указателяОбратились к полю у пустого значения. Проверь, что конструктор реально вернул объект.
deadlock detectedобнаружена взаимная блокировкаДве транзакции ждут друг друга. Обычно лечится порядком захвата блокировок.
address already in useадрес уже занятПорт держит другой процесс — часто предыдущий запуск не умер.
no such file or directoryнет такого файла или каталогаПуть неверен относительно рабочего каталога процесса.
connection reset by peerсоединение сброшено удалённой сторонойДругая сторона закрыла соединение молча: рестарт, kill, лимит.

Пары expected/actual и want/got запомни отдельно — они встречаются в assert-сообщениях тестов, в валидаторах схем, в компиляторах. Порядок в паре не случаен: слева то, что должно было быть, справа — то, что реально пришло. Перепутаешь — и будешь чинить исправное.

--- FAIL: TestRefundAmount (0.00s)
    refund_test.go:41: refund amount mismatch: want 12990, got 1299

Сумма отличается ровно в десять раз — значит, где-то потерялось преобразование копеек в рубли. Код открывать не пришлось: хватило двух слов и умения делить.

Как читать stack trace

Stack trace — история вызовов на момент падения. Целиком его не читают. Читают, деля кадры на три категории: твой код, библиотеки и рантайм с фреймворком. Падает почти всегда чужой код — а причина сидит в твоём, в последнем твоём кадре перед тем, как управление ушло в библиотеку. Туда и смотри.

Traceback (most recent call last):
  File "/app/.venv/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/app/blog/views/post.py", line 42, in post_detail
    score = popularity_score(post)
  File "/app/blog/services/popularity.py", line 18, in popularity_score
    return views * weights["views"] + likes * weights["likes"]
KeyError: 'likes'

Разбор занимает пять секунд. Первая же строка честно предупреждает: most recent call last — самый свежий вызов внизу, читаем снизу вверх. Внизу KeyError: 'likes': в словаре нет такого ключа. Кадр над ним — popularity.py, строка 18, наш код, здесь и живёт баг. Ещё выше post.py — тоже наш, показывает, кто вызвал. Самый верхний, site-packages/django/..., можно не читать: Django просто дёрнул наш вью. Маркеры чужого кода запоминаются мгновенно — site-packages, node_modules, vendor, /usr/lib.

В JavaScript всё наоборот — сообщение сверху, кадры под ним от свежего к старому:

TypeError: Cannot read properties of undefined (reading 'map')
    at ProductList (src/components/ProductList.tsx:31:22)
    at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:14985:18)
    at mountIndeterminateComponent (node_modules/react-dom/cjs/react-dom.development.js:17811:13)

Cannot read properties of undefined (reading 'map') означает: мы попытались прочитать свойство map у значения undefined. Массив, по которому собирались пройтись, не пришёл. Первый же кадр — наш файл, строка 31, колонка 22. Ниже node_modules, туда не лезем.

Go лаконичнее всех, но пугает новичков словом panic:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x6f2a1c]

goroutine 1 [running]:
main.(*Server).handleOrder(0x0, {0x8a12c0, 0xc0000b4000})
        /app/internal/api/order.go:57 +0x1c

Panic в Go — не «паника», а термин: аварийное завершение горутины. Invalid memory address or nil pointer dereference — обращение к полю через nil-указатель. Числа 0x6f2a1c и +0x1c — адреса и смещения, они тебе ни о чём не скажут. Нужны путь и номер строки: order.go:57. А вот 0x0 первым аргументом метода — деталь ценная: это и есть тот самый nil-получатель, о котором ругается рантайм.

Отдельный жанр — chained exceptions, цепочки. Python вставляет между блоками одну из двух фраз: The above exception was the direct cause of the following exception (ошибка выше была прямой причиной следующей) или During handling of the above exception, another exception occurred (пока обрабатывали одну, случилась вторая). Разница принципиальная. В первом случае корень наверху. Во втором наверху то, что вы пытались обработать, а внизу — новая ошибка, часто маскирующая исходную: классика, когда в except падает логирование и в Sentry прилетает KeyError из форматирования строки вместо реального таймаута.

Лексика логов и уровни серьёзности

У логов свой словарь, и он небольшой. Начнём с уровней (severity levels) — от самого болтливого к самому страшному:

УровеньСмыслРеакция дежурного
TRACE / DEBUGподробности для отладкиНикакой; в проде обычно выключен.
INFOштатное событиеЧитать при разборе полётов.
WARN / WARNINGчто-то не так, но система справиласьСмотреть на тренд: рост числа warning — предвестник инцидента.
ERRORоперация не выполненаРазбираться; пользователь, скорее всего, увидел ошибку.
FATAL / CRITICAL / PANICпроцесс не может продолжать работуНемедленно. Приложение обычно уже умерло.

Теперь слова. Почти все они описывают не ошибку в коде, а поведение системы под нагрузкой — и в этом их ценность: по ним видно, как сервис деградирует, ещё до того, как что-то упало.

10:22:03Z WARN  redis: connection reset by peer, retrying in 200ms (attempt 2/5)
10:22:04Z WARN  cache entry is stale, falling back to origin
10:22:05Z ERROR upstream request timed out after 30s; 12 events dropped
10:22:06Z INFO  client throttled: returned 429, retry-after=60
10:22:07Z WARN  worker queue backlog is growing: 4200 pending jobs
10:22:09Z INFO  circuit breaker for payments opened; skipping calls for 30s

Ключевые слова по одному. Retrying — повторная попытка, система ещё не сдалась. Stale — «протухший»: данные устарели, но формально в кэше лежат; противоположность — fresh. Dropped — события выброшены безвозвратно. Вот это всегда потеря данных и всегда повод остановиться и разобраться, даже если строка помечена как ERROR, а не FATAL. Throttled (или rate limited) — клиента намеренно притормозили. Backlog — накопившаяся очередь. Fall back to — переключиться на запасной вариант; существительное fallback пишется слитно. Circuit breaker opened — предохранитель разомкнулся, вызовы к сломанному сервису временно не делаются.

Шесть строк выше, если читать их подряд, складываются в сюжет: Redis отвалился, кэш протух, запросы пошли в origin, тот не успел за 30 секунд, часть событий потеряна, клиентов начали резать по лимиту, очередь растёт, платежи отключили предохранителем. Ни одной строки FATAL — а сервис фактически лежит. Умение прочитать такую последовательность как связный рассказ стоит дороже, чем знание отдельных слов.

Пары, которые легко перепутать: failed to connect (не смогли подключиться — обычно наша сторона) против connection refused (нас отвергли — обычно та сторона); timeout (ждали и не дождались) против cancelled (перестали ждать сами, например пользователь закрыл вкладку).

Changelog и release notes

Changelog — письмо от авторов библиотеки лично тебе. Широко принятая конвенция называется Keep a Changelog: файл CHANGELOG.md, версии в обратном хронологическом порядке, внутри версии — фиксированный набор разделов. Шесть слов, одинаковых во всей индустрии. Выучи их один раз и читай любой changelog по диагонали:

## [3.2.0] - 2025-02-11

### Added
- `POST /orders/{id}/refund` endpoint for partial refunds.

### Changed
- **Breaking:** `Order.status` is now returned in lowercase (`paid`, not `PAID`).

### Deprecated
- `OrderSerializer.total_price` is deprecated in favour of `total_amount`
  and will be removed in 4.0.

### Removed
- Dropped support for Python 3.8.

### Fixed
- Fixed a race condition where two workers could process the same webhook.

### Security
- Bumped `requests` to 2.32.4 to address a header-injection issue.

Переводим по смыслу, а не по словарю. Added — новая функциональность, обновление безопасно. Changed — поведение существующего изменилось; читать обязательно. Deprecated — объявлено устаревшим: пока работает, но авторы просят слезть. Removed — удалено; если ты этим пользовался, обновление тебя сломает. Fixed — исправления. Security — уязвимости; такие релизы ставят вне очереди, не дожидаясь спринта.

Deprecated — самое игнорируемое слово в индустрии. Оно всегда идёт со сроком и заменой: will be removed in 4.0, use X instead, in favour of Y, scheduled for removal. Заводи задачу в тот же день, пока помнишь контекст. Через год ты не вспомнишь, зачем вообще звал эту функцию.

Слова опасности выучи наизусть: breaking change, backward-incompatible, no longer supported, has been renamed to, now returns / now raises, required вместо optional. Самая коварная из них — конструкция с now. Ничего не удалено, никаких ошибок при обновлении, поведение просто стало другим.

Меня однажды на этом поймали. В release notes стояло the default value of ... is now True, я прочитал строку по диагонали как «добавили настройку» и выкатил обновление в пятницу. Ошибок не было ни одной — ни в логах, ни в Sentry. Просто у части пользователей в профилях ссылки молча переехали с http:// на https://, и у тех, чьи сайты жили без сертификата, картинки перестали открываться. Тикет пришёл во вторник, и полдня мы искали регрессию в своём коде.

Работает всё это в связке с семантическим версионированием. Версия имеет вид MAJOR.MINOR.PATCH: major растёт, когда что-то ломается обратно несовместимо; minor — когда добавили функциональность, не ломая старую; patch — когда только починили баги. Практическое следствие: 2.4.1 → 2.4.7 обычно катят не глядя, 2.4 → 2.9 — прочитав «Added», а 2.x → 3.0 — только прочитав раздел про миграцию целиком. Суффиксы -alpha.1, -beta, -rc.2 означают предрелизы; rc — release candidate, «кандидат в релиз».

Спецификации и RFC: скимминг вместо чтения

Спецификации не читают целиком. Их сканируют. Любой RFC устроен по одной схеме: Abstract (аннотация в абзац — читай всегда), Introduction (мотивация), Terminology или Conventions (определения — открывай, когда споткнулся о термин), тело документа, Security Considerations (читай, если реализуешь), IANA Considerations (регистрация имён — почти всегда мимо), References.

Главный приём: нормативные требования в спеках помечены специальными словами, зафиксированными отдельным стандартом. Это MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, OPTIONAL. Нужно понять, что ты обязан сделать, — жми Ctrl+F и ищи заглавные MUST. Восемьдесят процентов работы с документом на этом заканчивается.

The client MUST send an `Idempotency-Key` header with every POST request
to `/payments`. The server MUST reject a request without this header with
`400 Bad Request`.

A client SHOULD reuse the same key when retrying a failed request. Servers
MAY expire stored keys after 24 hours; clients MUST NOT rely on a key
being remembered longer than that.

Читается так: заголовок обязателен, сервер обязан отклонить запрос без него, переиспользовать ключ при повторе — настоятельно рекомендуется (можно иначе, но нужны причины), а срок жизни ключа отдан на усмотрение сервера, и рассчитывать на большее нельзя. Вся разница между MUST и SHOULD в этом: первое нарушать нельзя, второе — можно, если понимаешь, чем платишь.

Ещё несколько конструкций, за которые цепляется глаз при скимминге: unless otherwise specified (если не указано иное), for backward compatibility (ради обратной совместимости), implementation-defined / implementation-specific (на усмотрение реализации — то есть опираться нельзя), undefined behaviour (может быть вообще что угодно, включая то, что вчера работало), at least once / at most once / exactly once (гарантии доставки — для очередей это самая важная строка в документе).

Как искать ответ в документации

Справочная страница функции собрана из одних и тех же блоков, и важно знать, какой читать первым. Signature — сигнатура, самая плотная часть страницы: имена и типы аргументов, тип результата. Parameters / Arguments — что значит каждый аргумент и что будет по умолчанию. Returns — что вернётся. Raises (Python), Throws (JS и Java), Errors (Go) — чем эта функция умеет падать. See also — соседние функции, лучший источник ответа «а как правильно». И Notes с Caveats — оговорки. Именно там обычно и лежит ответ на твой вопрос, потому что вопрос у тебя возник как раз про нестандартный случай.

bulk_create(objs, batch_size=None, ignore_conflicts=False,
            update_conflicts=False, update_fields=None,
            unique_fields=None)

Из одной сигнатуры видно больше, чем из абзаца прозы: аргументы с = необязательны, названия говорят сами за себя, а пара ignore_conflicts / update_conflicts прямо сообщает, что конфликт уникальности здесь предусмотрен и решается флагом. Привычка сначала смотреть на сигнатуру и только потом читать текст экономит часы в год.

Теперь поиск. Плохой запрос — это перевод твоего вопроса с русского: «почему не работает bulk_create в джанго». Хороший состоит из трёх частей: технология + точный текст ошибки в кавычках + то, что ты делал. Кавычки заставляют поисковик искать фразу целиком, а не набор слов вразброс.

django "duplicate key value violates unique constraint" bulk_create
site:docs.djangoproject.com bulk_create ignore_conflicts
"psycopg.errors.UniqueViolation" ON CONFLICT DO NOTHING
node "ENOENT: no such file or directory" fs.promises.readFile docker
go "all goroutines are asleep - deadlock" unbuffered channel

Правила, которые действительно работают. Вычищай из текста ошибки всё уникальное для твоей машины — пути, UUID, номера строк, порты. Иначе результатов будет ровно ноль, и ты решишь, что столкнулся с чем-то небывалым. Оператор site: прижимает выдачу к официальной документации и спасает от статей-пересказов пятилетней давности. Если ищешь способ сделать, а не причину падения, формулируй как «how to ... in ...»: так написаны заголовки англоязычных ответов, и совпадение находится быстрее. И держи в голове разницу между словами, которыми описывают проблему: issue — запись в трекере, bug — сам дефект, feature request — просьба о новой возможности, regression — то, что работало и сломалось после изменения. Назовёшь регрессию багом — получишь ответ не о том.

Кейс из реального проекта: читаем release notes мажорной версии

Ситуация: Django-монолит на 5.1, вышла 6.0, тимлид просит оценить «что нам будет» — желательно сегодня. В документе сотни строк, читать сверху вниз бессмысленно. Действуем по протоколу.

Шаг 1. Ищем раздел с ломающими изменениями. В release notes любого крупного проекта он называется предсказуемо: Backwards incompatible changes, Breaking changes, Upgrade notes, Migration guide. Остальное — потом.

## Backwards incompatible changes in 6.0

### Database backend API
- The `DatabaseOperations.field_cast_sql()` method is removed.
  Use `lookup_cast()` instead.

### Miscellaneous
- Support for PostgreSQL 13 is removed.
- `django.utils.timezone.utc` is removed; use `datetime.timezone.utc`.
- The default value of `FORMS_URLFIELD_ASSUME_HTTPS` is now `True`.

Шаг 2. Переводим каждый пункт в вопрос «есть ли это у нас». Здесь английский встречается с грепом. Is removed — удалено, код с этим вызовом упадёт сразу после обновления, и это, как ни странно, лучший случай: падение видно. Use X instead — замену уже назвали, искать не надо. А the default value ... is now — то самое коварное молчаливое изменение, ради которого мы вообще читаем этот документ.

rg -F "timezone.utc" --type py
rg "field_cast_sql|lookup_cast" --type py
psql -c "SHOW server_version;"

Шаг 3. Смотрим раздел Deprecated. Он про следующую мажорную версию: всё перечисленное ещё работает, но уже приговорено. Чинить лучше сейчас, пока контекст в голове, а не через год в чужом спринте.

## Features deprecated in 6.0

- `CheckConstraint.check` is deprecated in favour of `CheckConstraint.condition`
  and will be removed in Django 7.0.
- Passing positional arguments to `Model.save()` is deprecated; pass them
  as keyword arguments. Support will be dropped in Django 7.0.

Шаг 4. Пишем вывод для команды на английском — задачу всё равно заводить в трекере, и читать её будут в том числе те, кто по-русски не читает.

Django 6.0 upgrade — impact assessment

Blockers (must fix before upgrade):
- `django.utils.timezone.utc` is used in 3 modules (blog, comments, tutorials).
  Replace with `datetime.timezone.utc`. Low risk, mechanical change.
- Production runs PostgreSQL 13, which is no longer supported.
  Needs a DB upgrade to 14+ first — this is the long pole.

Silent behaviour changes (need a test before upgrade):
- `FORMS_URLFIELD_ASSUME_HTTPS` now defaults to True. Our profile form
  accepts bare-domain input, so stored values will change from
  `http://` to `https://`. Add a regression test.

Deprecations (follow-up ticket, not a blocker):
- `CheckConstraint.check` -> `condition` in two migrations.

Not affected: database backend API (we use the stock PostgreSQL backend).

Посмотри на язык этого письма. Он не переводной, а рабочий: короткие категории (blockers, silent behaviour changes, deprecations, not affected), в каждом пункте факт и следствие, никаких рассуждений. Идиома the long pole — «самая длинная жердь», то, что определяет сроки — из проектного жаргона. Такие выражения полезно узнавать в чужом тексте, а в своём достаточно честного this will take the most time.

И главное, что видно из этого разбора: обновление упирается не в код. Оно упирается в строку «PostgreSQL 13 больше не поддерживается» — то есть в неделю работы с базой, о которой никто не планировал. Поэтому release notes читают до того, как кто-то поменял цифру в requirements.txt. После — это уже не оценка, а разбор полётов.

Типичные ошибки

Читать всё подряд вместо целенаправленного поиска

Самая дорогая привычка из всех. Увидев стену английского, человек добросовестно переводит с первой строки, тратит десять минут и находит нужное в конце — там, где оно и лежало с самого начала. А структура известна заранее: в traceback смысл в последней строке, в release notes — в разделе про несовместимости, в справке функции — в сигнатуре и в Raises. Сначала найди нужный блок, потом читай его внимательно. Не наоборот. Скимминг — это не «читать невнимательно», это отдельный навык: скользить по тексту, пока не зацепишься за ключевое слово.

Игнорировать deprecation-предупреждения

Строка DeprecationWarning: on_delete will be required in 6.0 в выводе тестов выглядит безобидно, и её обычно гасят фильтром, чтобы не мешала. Проходит год. Приходит мажорное обновление — и безобидное предупреждение превращается в сотню красных тестов в спринте, где на это нет ни одного свободного дня. Deprecated, will be removed, scheduled for removal, legacy — это не информация к сведению, это дедлайн с отсрочкой. Увидел deprecation — заводи задачу в тот же день, с цитатой предупреждения и номером версии, в которой всё сломается.

Читать машинный перевод сообщения об ошибке

Соблазн понятный: скопировал строку, вставил в переводчик, получил русский текст. Беда в том, что переводчик не знает контекста и уверенно превращает термины в бытовые слова. Deadline exceeded становится «крайний срок превышен» вместо «истёк таймаут». Connection refused — «в подключении отказано», и это звучит как проблема с правами, хотя на самом деле по адресу просто никто не слушает. Fatal — «фатальный» вместо «процесс остановлен». Panic в Go — «паника» вместо «аварийное завершение». Но хуже другое: переведённую формулу невозможно загуглить. Текст ошибки — это ключ поиска, его сохраняют как есть. Переводи свои мысли о проблеме, а не саму ошибку.

Пропускать отрицание в длинном предложении

Классическая ловушка уровня B1. Английское отрицание прячется в середине конструкции, а не в начале, и при быстром чтении глаз его проскакивает. The server MUST send the header и The server MUST NOT send the header — одно слово разницы и ровно противоположная реализация. Из той же серии: unless (если НЕ), fail to (fails to parse — не удалось разобрать), no longer (больше не), not supported и unsupported (это одно и то же), disallow, prevent from, omit (опустить, не указывать). Приём простой и слегка дурацкий: в критичных местах — требования спеки, условия в документации, тексты про безопасность — проговаривай предложение вслух и спрашивай себя, есть ли в нём отрицание. Двойное отрицание опаснее всего: it is not uncommon for the cache to be empty значит «пустым бывает довольно часто», а не «пустым не бывает». Именно так и рождаются гипотезы, на проверку которых уходит день.

Итог

Там, где раньше была стена текста, теперь видна структура. Сообщение об ошибке распадается на «где — какой класс — детали», и смысл сидит в предсказуемом месте. Stack trace делится на твой код, библиотеки и рантайм; причина живёт в последнем твоём кадре. Логи обходятся полусотней слов, и половина из них про повторы, таймауты и очереди. Changelog устроен по конвенции из шести разделов, а номер версии сам сообщает, насколько страшно обновляться. Спецификации читаются по MUST и SHOULD, документация — с сигнатуры. И поисковый запрос — тоже технический английский: формула в кавычках плюс контекст.

Дальше переходим от чтения к письму. Начнём с текста, который ты пишешь ежедневно, не считая это письмом: английский в коде. Чем fetch отличается от get, почему active_user, а не user_active, чем комментарий-объяснение отличается от комментария-пересказа и как выглядит докстринг, который не стыдно показать.

Комментарии 0

Для добавления комментариев необходимо войти или зарегистрироваться.

Пока нет комментариев. Станьте первым!