Learning Platform
Глоссарий Troubleshooting
Урок 03.03 · 25 мин
Продвинутый
parserpartial-parsingmsgpack

Partial parsing: partial_parse.msgpack изнутри

Parse phase — самый дорогой шаг pipeline на больших проектах. Без оптимизации 1500 моделей парсятся 30-60 секунд. Partial parsing — встроенная в dbt-core оптимизация, сводящая warm parse к 1-3 сек.

В этом уроке мы разберём: что записывается в partial_parse.msgpack, как считается hash для инвалидации, что инвалидирует кэш, и какие senior-gotchas с env vars, jinja-функциями, dbt deps.


Что такое partial parsing

Идея простая: если файлы проекта не изменились с прошлого parse, повторно парсить их незачем. Можно сохранить результат parse (Manifest) на диск и загрузить при следующем запуске.

partial_parse.msgpack — это msgpack-сериализованный snapshot Manifest + дополнительные metadata для инвалидации. Расположение — target/partial_parse.msgpack (или другой target_path если кастомный).

При следующем dbt run:

  1. ManifestLoader сначала проверяет partial_parse.msgpack.
  2. Если есть и метаданные совпадают — загружает Manifest из msgpack, пропускает full parse.
  3. Если файлы изменились — пере-парсит изменённые, обновляет Manifest, сохраняет новый snapshot.
# core/dbt/parser/manifest.py (упрощённо)
class ManifestLoader:
    @classmethod
    def get_full_manifest(cls, config, reset=False):
        loader = cls(config, all_projects)
        
        # 1. Try partial parse
        if not reset and config.partial_parse:
            saved_manifest = loader.read_manifest_for_partial_parse()
            if saved_manifest and loader.matches_current_state(saved_manifest):
                # Adjust for changed files
                manifest = loader.partial_parse(saved_manifest)
                return manifest
        
        # 2. Fallback to full parse
        manifest = loader.full_parse()
        loader.write_manifest_for_partial_parse(manifest)
        return manifest

Что хранится в msgpack

partial_parse.msgpack содержит сериализованный Manifest + project_hash + env_vars hash + files metadata:

@dataclass
class PartialManifestData:
    manifest: Manifest                    # сам Manifest object
    project_hash: str                     # hash dbt_project.yml + profiles.yml
    env_vars_hash: str                    # hash всех env vars, использованных при render
    files: Dict[str, FileMetadata]        # {file_path: FileMetadata(checksum, mtime)}

FileMetadata для каждого parsed файла:

@dataclass
class FileMetadata:
    checksum: str  # sha256 содержимого
    mtime: float   # mtime для quick check

Msgpack chosen потому что:

  • Быстрее JSON для serialization/deserialization (~3-5x на больших объектах)
  • Меньше размер — typical 50MB Manifest становится 15-20MB msgpack
  • Schema-less — не требует жёсткой схемы, dataclasses сериализуются нативно

Алгоритм инвалидации

При следующем dbt run:

def matches_current_state(self, saved_manifest_data) -> bool:
    # Check 1: project_hash
    current_project_hash = compute_project_hash(self.root_config)
    if current_project_hash != saved_manifest_data.project_hash:
        log("dbt_project.yml or profiles.yml changed — full re-parse")
        return False
    
    # Check 2: env_vars_hash
    current_env_hash = compute_env_vars_hash(saved_manifest_data.env_vars_used)
    if current_env_hash != saved_manifest_data.env_vars_hash:
        log("env vars changed — full re-parse")
        return False
    
    # Check 3: files changed
    # Будет partial — не full re-parse
    return True


def partial_parse(self, saved_manifest_data) -> Manifest:
    manifest = saved_manifest_data.manifest
    
    # Find changed files
    changed_files = self.detect_changed_files(saved_manifest_data.files)
    
    # Re-parse only changed
    for file_path in changed_files:
        self.re_parse_file(manifest, file_path)
    
    # Process refs again (могут измениться зависимости)
    self._process_refs()
    self._build_parent_child_maps()
    
    return manifest

Три уровня инвалидации:

  1. Project-level (dbt_project.yml, profiles.yml changed) — full re-parse.
  2. Env-vars-level (any env_var used in render changed) — full re-parse.
  3. File-level (specific .sql, .yml changed) — partial re-parse only those files.
env_var(): чтение environment variables и секреты (dbt I)

Что попадает в env_vars_hash

dbt отслеживает все env vars, к которым обращались macros/configs/Jinja:

# Псевдокод of env var tracking
env_vars_used = set()

def env_var(name: str, default=None) -> str:
    env_vars_used.add(name)  # tracked
    return os.environ.get(name, default)

При сохранении msgpack:

env_vars_snapshot = {name: os.environ.get(name) for name in env_vars_used}
env_vars_hash = sha256(serialize(env_vars_snapshot))

При следующем run:

current_env_snapshot = {name: os.environ.get(name) for name in env_vars_used}
current_env_hash = sha256(serialize(current_env_snapshot))
if current_env_hash != saved.env_vars_hash:
    # Re-parse — env var changed
WARNING

Самая частая senior-проблема: вы добавили {{ env_var('NEW_VAR', 'default') }} в одну из YAML configs. Каждый CI run set’ит NEW_VAR=ci_value, локально не set. Каждый CI run инвалидирует partial parse cache. Symptom: CI parsing медленный.


File detection algorithm

Для каждого parsed файла dbt сохраняет mtime + checksum.

def detect_changed_files(self, saved_files: Dict[str, FileMetadata]) -> List[str]:
    changed = []
    current_files = walk_all_project_files()
    
    # Removed files
    for path in saved_files:
        if path not in current_files:
            changed.append(path)  # marked for removal
    
    # Added files
    for path in current_files:
        if path not in saved_files:
            changed.append(path)  # new file
    
    # Modified files
    for path, saved_meta in saved_files.items():
        if path not in current_files:
            continue
        current_mtime = os.path.getmtime(path)
        if current_mtime != saved_meta.mtime:
            # mtime changed — quick check failed, check checksum
            current_checksum = compute_checksum(path)
            if current_checksum != saved_meta.checksum:
                changed.append(path)
    
    return changed

Two-stage check:

  1. mtime — fast (filesystem call). Если mtime same, file unchanged.
  2. checksum — slow (read file, sha256). Только если mtime отличается.

Это оптимизация: 99% случаев mtime даёт ответ без чтения. Только если git pull или touch trigger’ит mtime изменение без content change — checksum проверяет.


Что инвалидирует partial parse cache

Партикулярные триггеры инвалидации
dbt_project.yml changeЛюбое изменение в dbt_project.yml инвалидирует ВСЁ. Full re-parse. Это потому что project-level configs могут менять materialization, schema, vars для всех моделей.
profiles.yml changeИзменение target.type, credentials, threads, schema в profiles.yml — full re-parse. Изменение пароля или host обычно не инвалидирует (если они не используются в Jinja).
env var changeЛюбая env_var(), к которой обращались на parse phase — если её значение изменилось, full re-parse.
packages.yml changeИзменение зависимостей в packages.yml — full re-parse (могут добавиться/удалиться макросы).
dbt-core versionUpgrade dbt-core версии — full re-parse. Manifest schema может измениться.
adapter versionUpgrade adapter package — full re-parse. Adapter-specific macros могут измениться.
model.sql changeТолько этот файл re-parsed. Refs пересчитываются. partial re-parse.
_models.yml changeТолько этот YAML re-parsed. Patches applied to existing nodes. partial re-parse.
--vars CLI changeЕсли --vars передан c новыми значениями, инвалидирует кэш если эти vars использованы в render.
--target changeИзменение --target (dev -> prod) инвалидирует — target используется в configs, generate_schema_name.

Senior-gotchas

Gotcha 1: env var с default не инвалидирует, если default тот же

{{ env_var('MY_VAR', 'default_value') }}

Если MY_VAR не set, dbt использует 'default_value'. Если в следующем run MY_VAR set’нут в 'default_value' — hash будет same, кэш валиден. Если в 'other_value' — invalid.

Gotcha 2: env var в macros, который не вызывается на parse

{% macro my_macro() %}
  {{ env_var('RARELY_USED_VAR') }}
{% endmacro %}

Если my_macro() не вызывается на parse phase (только runtime), env var не tracked. Изменение RARELY_USED_VAR не инвалидирует кэш — но также и не использовано в Manifest, что нормально.

Gotcha 3: dynamic generate_schema_name

{% macro generate_schema_name(custom_schema_name, node) %}
  {% if env_var('CI', 'false') == 'true' %}
    {{ custom_schema_name | trim }}
  {% else %}
    {{ env_var('USER') }}_{{ custom_schema_name | trim }}
  {% endif %}
{% endmacro %}

USER env var — это login имя в Unix, отличается между разработчиками. Каждый разработчик имеет свой кэш. На общей CI machine — USER=runner или USER=jenkins, hash same между runs (until разработчики push).

Если у вас CI каждый раз делает full re-parse — посмотрите какие env vars вы используете на parse.

Gotcha 4: file mtime после git pull

git pull устанавливает mtime файлов в момент checkout, не сохраняет original mtime. Если 100 файлов изменились в pull — все 100 будут partial re-parsed (что нормально). Но если только metadata изменилась — partial parse спасает.

Gotcha 5: —no-partial-parse флаг

dbt parse --no-partial-parse — full re-parse forced. Полезно для дебага, CI clean builds. Не нужен в обычной работе.

Gotcha 6: —partial-parse-file-diff

Скрытая опция (1.10+): --partial-parse-file-diff <file> — указать explicitly список изменённых файлов. Используется в advanced CI sequences, когда вы знаете diff.


Дебаг partial parse

# Debug log показывает что произошло с partial parse
DBT_LOG_LEVEL=debug dbt parse 2>&1 | grep -i "partial\|parse"

Типичный вывод:

[debug] Loading partial parse manifest from disk
[debug] partial parse: 1 file changed, 0 files added, 0 files removed
[debug] partial parsing changed file models/staging/stg_users.sql
[debug] Loaded existing manifest from disk

Или (full re-parse):

[debug] Full parse of project because: env var DBT_FOO changed

dbt-core конкретно говорит причину full re-parse. Это invaluable для debug.


Программный доступ к msgpack

Senior может прочитать partial_parse.msgpack напрямую:

import msgpack
from dbt.contracts.graph.manifest import Manifest

with open('target/partial_parse.msgpack', 'rb') as f:
    data = msgpack.unpackb(f.read())

# data — это dict с keys: manifest, project_hash, env_vars_hash, files
print(data['files'])  # {file_path: {checksum, mtime}}
print(data['env_vars_hash'])  # hash

Использование:

  • Дебаг инвалидации — сравнить saved env_vars_hash и текущий.
  • Observability — кто изменил какие файлы между двумя runs.
  • CI cache key — использовать file checksums как cache key для GitHub Actions cache.

Performance numbers

На production проекте 1500 моделей, измерения примерные:

СценарийTimeComment
Cold (no msgpack)30-60 секFull parse — first run или после rm
Warm (msgpack hit)1-3 секAll files unchanged
Warm + 1 file changed1.5-3 секOne model re-parsed, refs re-processed
Warm + 10 files changed2-5 сек10 models re-parsed
Warm + project file changed30-60 секFull re-parse triggered
Warm + env var changed30-60 секFull re-parse triggered

Production lesson: minimize project-file и env var changes в active development. Если возможно — keep env vars stable, change в profiles.yml через --target switches.


Попробуй сам

  1. Замерьте cold vs warm:

    rm -rf target/
    time dbt parse  # cold
    time dbt parse  # warm
  2. Поменяйте одну модель, replay:

    touch models/staging/stg_users.sql
    time dbt parse  # должно быть warm — 1-3 sec
  3. Поменяйте dbt_project.yml, replay:

    echo "" >> dbt_project.yml  # добавить пустую строку
    time dbt parse  # full re-parse
  4. Trigger env var inval:

    export TEST_VAR=foo
    dbt parse  # cache invalidated, full re-parse
    export TEST_VAR=foo  # same value
    dbt parse  # cache valid (same hash)
    export TEST_VAR=bar
    dbt parse  # cache invalidated

    Но только если env_var('TEST_VAR') где-то в проекте на parse phase.

  5. Прочитайте msgpack программно:

    python -c "
    import msgpack
    with open('target/partial_parse.msgpack', 'rb') as f:
        data = msgpack.unpackb(f.read())
    print('Project hash:', data.get('project_hash'))
    print('Env vars hash:', data.get('env_vars_hash'))
    print('Files tracked:', len(data.get('files', {})))
    "

Проверка знанийKnowledge check
У вас в CI каждый PR делает full re-parse, занимая 60 сек. Хочется кэшировать partial_parse.msgpack между runs. Какие 3-4 проблемы нужно решить, чтобы кэш реально работал?
ОтветAnswer
Несколько критических моментов. (1) Сохранение msgpack между CI runs — GitHub Actions cache по умолчанию не persists target/. Нужен actions/cache step с ключом include git SHA prev commit (чтобы cache из previous build): cache key 'partial-parse-' + github.sha, restore-keys: 'partial-parse-' (fuzzy match). (2) Env vars consistency — CI каждый раз set'ит DBT_PROFILES_DIR, DBT_TARGET, secrets. Если эти vars используются на parse (например, в profiles.yml), их изменения инвалидируют кэш. Решение: hardcode constant vars на parse phase, dynamic vars только на runtime через target. (3) File mtime instability — git checkout не сохраняет original mtime, использует current time. Это значит mtime изменится для всех файлов на каждом checkout, что заставит dbt-core fallback на checksum check для каждого файла. Это слегка медленнее (10-30 sec instead of 1-3) но не triggers full re-parse. Решение: use git restore-mtime tools (например, git-restore-mtime), которые восстанавливают mtime по git log. (4) Project file changes — каждый PR меняющий dbt_project.yml или profiles.yml будет full re-parse. Это unavoidable, accept или batch dbt_project.yml changes. (5) packages.yml stability — если dbt deps regenerates dbt_packages/, mtime файлов в packages меняется. Cache dbt_packages/ separately. (6) Cache size — partial_parse.msgpack 15-20MB на 1500 моделей. Помещается в GitHub Actions cache (10GB free). Effect: cold 60sec -> warm 3-10 sec, что критично для PR iterations.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. В CI каждый PR full re-parse 60 sec. Senior хочет настроить partial parse cache. Какие 4 проблемы нужно решить?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4