Skip to content

fix(sdm): precompile dependency bytecode in the SDM image - #1222

Merged
Patrick Nilan (pnilan) merged 3 commits into
mainfrom
devin/1791497733-sdm-precompile-bytecode
Oct 22, 2026
Merged

Patrick Nilan (pnilan) merged 3 commits into
mainfrom
devin/1791497733-sdm-precompile-bytecode

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Resolves https://github.com/airbytehq/airbyte-internal-issues/issues/17922:

Close on merge: yes — internally filed, no customer to confirm with. (The post-release benchmark row for airbytehq/airbyte-internal-issues#17920 is a separate acceptance item.)

Requested by Patrick Nilan (Patrick Nilan (@pnilan)).

The airbyte/source-declarative-manifest image ships no .pyc for its dependencies. Poetry 1.8 (poetry install --only main) does not compile bytecode unless --compile is passed; only the CDK wheel (installed with pip) gets .pyc. The container runs as airbyte, which can't write to the root-owned site-packages, so every connector process (spec/check/discover/read) recompiles pandas, numpy, pydantic, etc. on start.

Changes:

  • Dockerfile: after the installs and before USER airbyte, add RUN python -m compileall -q -j 0 -x '/tests?/' /usr/local/lib/python3.13/site-packages || echo "WARNING: ...". Used compileall rather than poetry install --compile because it doesn't depend on the installer and covers every package in site-packages. Test directories (*/test/, */tests/, mostly in pandas/numpy; nothing imports them at runtime) are skipped. A compile failure warns instead of failing the build, so an uncompilable file from a dependency update can't publish the CDK to PyPI while the SDM image build fails; the CI check below still reports any such file as missing a .pyc, so the PR that introduces it goes red.
  • .github/workflows/docker-build-check.yml (sdm-docker-build-check): the multi-platform build is push: false with no load, so an extra linux/amd64 build with load: true (layers come from the buildx cache) is run, and a check runs inside it that fails if any .py under site-packages (outside test directories, same /tests?/ pattern as the Dockerfile) has no .pyc, or if a .pyc is not a valid timestamp-based file for its source: short header, wrong magic number, hash-based flags, or a timestamp/size header that no longer matches the source (any of which makes Python ignore or bypass it).

Timestamps: neither the CDK publish workflow nor the monorepo connector image build sets rewrite-timestamp/SOURCE_DATE_EPOCH. In the published 7.33.0 image, the existing pip-compiled airbyte_cdk .pyc headers all still match their sources (the check reports 0 with stale .pyc), so the publish pipeline preserves mtimes.

Before / after (local, linux/amd64, --cpus=1, back to back)

baseline (main) this PR
site-packages .py (outside test dirs) without .pyc 2894 / 5019 0 / 5019
SDM import source_declarative_manifest.run (median of 5, first revision of this PR) 2889 ms 1352 ms
SDM image size 303.1 MB 317.1 MB

Measured on the first revision of this PR, which compiled everything including test dirs: source-pokeapi built on it, import as airbyte, 2898 ms → 1323 ms; spec 3.62 s → 2.11 s; image 326.0 MB. Skipping test dirs did not change import time: in a later run on the same machine, the first revision took a median of 1085 ms and this revision 1054 ms; importing the SDM entrypoint loads 1489 modules, none from a test directory. The compileall layer takes 3.7 s locally.

The prerelease image 7.34.0.post2.dev39677594442, published from the first revision, passed the bytecode check on both linux/amd64 and linux/arm64. So the publish pipeline keeps source mtimes and the timestamp-based .pyc files stay valid.

Declarative-First Evaluation

Not applicable: this changes the SDM image build and CI only. No manifest or custom Python component is touched.

Test Coverage

The new CI step is the regression test for this bug. Run locally against images built from main and from this branch:

  • baseline (main): 5019 .py files, 2894 without .pyc, 0 with stale .pyc → exit 1
  • this PR: 5019 .py files, 0 without .pyc, 0 with stale .pyc → exit 0
  • this PR plus a deliberately broken .py: the image build completes and prints the WARNING; the check reports 1 missing → exit 1
  • pydantic recompiled as checked-hash: 105 stale → exit 1
  • one .pyc truncated to 10 bytes: 1 stale, no crash → exit 1

actionlint passes on the workflow. No Python unit tests apply; no CDK code changed.

Review notes

  • Not a breaking change; no CDK version bump in-repo (released via release drafter). Connectors get this when their SDM base image is bumped.
  • Out of scope (unchanged): airbyte_cdk/manifest_server/Dockerfile, the monorepo docker-images/Dockerfile.python-connector, and the existing poetry install ... || true (Poetry in the built image is broken with ModuleNotFoundError: No module named 'filelock'; this PR doesn't change that).
  • Suggested reviewers (recent Dockerfile authors): Anatolii Yatsuk (@tolik0), David Gold (@dbgold17).

Link to Devin session: https://app.devin.ai/sessions/e58fcb5f001640438eb753278d7bfb4b
Open in Devin Desktop: https://app.devin.ai/desktop/session/e58fcb5f001640438eb753278d7bfb4b?variant=devin

…nifest image

Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

👋 Greetings, Airbyte Team Member!

Here are some helpful tips and reminders for your convenience.

💡 Show Tips and Tricks

Testing This CDK Version

You can test this version of the CDK using the following:

# Run the CLI from this branch:
uvx 'git+https://github.com/airbytehq/airbyte-python-cdk.git@devin/1791497733-sdm-precompile-bytecode#egg=airbyte-python-cdk[dev]' --help

# Update a connector to use the CDK from this branch ref:
cd airbyte-integrations/connectors/source-example
poe use-cdk-branch devin/1791497733-sdm-precompile-bytecode

PR Slash Commands

Airbyte Maintainers can execute the following slash commands on your PR:

  • /autofix - Fixes most formatting and linting issues
  • /poetry-lock - Updates poetry.lock file
  • /test - Runs connector tests with the updated CDK
  • /prerelease - Triggers a prerelease publish with default arguments
  • /poe build - Regenerate git-committed build artifacts, such as the pydantic models which are generated from the manifest JSON schema in YAML.
  • /poe <command> - Runs any poe command in the CDK environment
📚 Show Repo Guidance

Helpful Resources

📝 Edit this welcome message.

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

PyTest Results (Fast)

5 839 tests  ±0   5 808 ✅ ±0   12m 3s ⏱️ + 3m 51s
    1 suites ±0      31 💤 ±0 
    1 files   ±0       0 ❌ ±0 

Results for commit 7364108. ± Comparison against base commit b8ccd9d.

♻️ This comment has been updated with latest results.

@pnilan
Patrick Nilan (pnilan) marked this pull request as ready for review October 21, 2026 20:29
@pnilan
Patrick Nilan (pnilan) requested a review from a team as a code owner October 21, 2026 20:29
@pnilan

Patrick Nilan (pnilan) commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

/prerelease

Prerelease Job Info

This job triggers the publish workflow with default arguments to create a prerelease.

Prerelease job started... Check job output.

✅ Prerelease workflow triggered successfully.

View the publish workflow run: https://github.com/airbytehq/airbyte-python-cdk/actions/runs/39677594442

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Checked the prerelease image published by the /prerelease run (CDK Publish run), airbyte/source-declarative-manifest:7.34.0.post2.dev39677594442, by running the same bytecode check as CI on both platforms:

linux/amd64: /usr/local/lib/python3.13/site-packages: 6606 .py files, 0 without .pyc, 0 with stale .pyc
linux/arm64: /usr/local/lib/python3.13/site-packages: 6606 .py files, 0 without .pyc, 0 with stale .pyc

This means the publish pipeline keeps source mtimes and the timestamp-based .pyc files are still valid after publishing. On arm64 this is the first runtime check; CI only checks amd64.

I also timed import source_declarative_manifest.run (linux/amd64, --cpus=1, 3 runs, as the default airbyte user):

  • 7.33.0: 4339, 1963, 1984 ms. The first run was a cold start.
  • prerelease: 912, 861, 890 ms

Written by Devin

@pnilan Patrick Nilan (pnilan) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This review was generated by an AI Agent.

Three changes to make before this merges. The bytecode itself checks out: all 6,596 precompiled code objects match what compiling from source produces, and spec, check, discover and read output is unchanged.

1. Don't fail the SDM build when a file doesn't compile (Dockerfile:26)

compileall exits 1 when any file fails to compile, so this line fails the SDM image build on any uncompilable .py in site-packages. Nothing fails today, but a dependency update could add such a file. Then:

  • SDM Docker Image Build isn't a required check on main, so the update can still merge.
  • At the next release, publish_cdk and publish_sdm each need only build. PyPI would get the new version, the SDM image build would fail, and Builder sources and manifest-only connectors would stay on the previous SDM version. Nothing alerts on a failed publish_sdm today; that is tracked separately in airbytehq/airbyte-internal-issues#18062.

Make the release build warn instead of fail, and drop "Fails the build if any file fails to compile." from the comment above it:

RUN python -m compileall -q -j 0 /usr/local/lib/python3.13/site-packages \
    || echo "WARNING: some files failed to compile (see errors above); continuing without their bytecode"

The CI check still reports any such file as missing a .pyc, so a PR that adds one still goes red.

2. Reject .pyc files the check can't validate (.github/workflows/docker-build-check.yml:76-78)

The check compares mtime and size only when flags == 0, and passes any other .pyc without comparing. Reproduced against a test image:

  • A checked-hash .pyc with an edited source passes, while Python recompiles that module on every start.
  • An unchecked-hash .pyc with an edited source passes, while stale code runs.
  • A 12-byte header passes, and a 10-byte header crashes the check with struct.error.

The Dockerfile only produces timestamp-based files, so treat anything else as stale:

if len(header) < 16 or header[:4] != importlib.util.MAGIC_NUMBER or flags != 0:
    stale.append(src)
else:
    mtime, size = struct.unpack("<II", header[8:16])

3. Skip test directories (Dockerfile:26 and the check)

1,372 of the 4,261 new .pyc files are under */test/ or */tests/, mostly in pandas and numpy, and nothing imports them at runtime. Skipping them cuts the added bytecode from 22.7 MB to 13.8 MB compressed, and from 64.9 MB to 37.8 MB on disk, per platform.

Change both places together, or the check fails with 1,372 files missing a .pyc. With fix 1, the Dockerfile step becomes:

# Precompile bytecode for installed packages, skipping test directories. `poetry install` does not
# compile it, and the runtime `airbyte` user cannot write to the root-owned site-packages, so without
# this every connector process recompiles its dependencies on start.
RUN python -m compileall -q -j 0 -x '/tests?/' /usr/local/lib/python3.13/site-packages \
    || echo "WARNING: some files failed to compile (see errors above); continuing without their bytecode"

And the check, with fix 2:

import importlib.util
import pathlib
import re
import struct
import sys
import sysconfig

SKIP = re.compile(r"/tests?/")  # keep in sync with compileall -x in the Dockerfile

root = pathlib.Path(sysconfig.get_paths()["purelib"])
missing, stale = [], []
total = 0
for src in root.rglob("*.py"):
    if SKIP.search(str(src)):
        continue
    total += 1
    pyc = pathlib.Path(importlib.util.cache_from_source(str(src)))
    if not pyc.is_file():
        missing.append(src)
        continue
    header = pyc.read_bytes()[:16]
    flags = int.from_bytes(header[4:8], "little")
    if len(header) < 16 or header[:4] != importlib.util.MAGIC_NUMBER or flags != 0:
        stale.append(src)
    else:
        mtime, size = struct.unpack("<II", header[8:16])
        st = src.stat()
        if (mtime, size) != (int(st.st_mtime) & 0xFFFFFFFF, st.st_size & 0xFFFFFFFF):
            stale.append(src)
print(f"{root}: {total} .py files, {len(missing)} without .pyc, {len(stale)} with stale .pyc")
for path in (missing + stale)[:20]:
    print(f"  {path}")
sys.exit(1 if total == 0 or missing or stale else 0)

Tested

Images built from airbyte/source-declarative-manifest:7.33.0 with the two snippets above:

Image Check result
With the changes 5,014 files, 0 missing, 0 stale; exit 0
Plus a deliberately broken .py Build completes and prints the warning; the check reports 1 missing; exit 1
Compiled as checked-hash 5,014 stale; exit 1
One .pyc truncated to 10 bytes 1 stale, no crash; exit 1
Published 7.33.0, not precompiled 2,889 missing; exit 1

Importing the SDM entrypoint on the changed image loads 1,477 modules, none from a test directory.

…est dirs

Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Re: review. I made all three changes in 6574c47, using your snippets as written.

  1. Warn instead of fail: compileall ... || echo "WARNING: ...", and I removed the "Fails the build" sentence from the comment.
  2. Stricter check: a .pyc is now marked stale if its header is shorter than 16 bytes, its magic number is wrong, or flags != 0. Only timestamp-based files are validated against their source.
  3. Skip test dirs: -x '/tests?/' in the Dockerfile, and a matching SKIP regex in the check.

Local results (linux/amd64; the check run is identical to the CI heredoc):

Image Check result
This PR 5019 files, 0 missing, 0 stale; exit 0
main 2894 missing; exit 1
This PR plus a broken broken_mod.py, then the same compileall command Build completes and prints the WARNING; 1 missing; exit 1
pydantic recompiled with --invalidation-mode checked-hash 105 stale; exit 1
One .pyc truncated to 10 bytes 1 stale, no traceback; exit 1
  • Image size: 317.1 MB, down from 326.0 MB with test dirs compiled (main is 303.1 MB).
  • Import time: import source_declarative_manifest.run as airbyte at --cpus=1 has a median of 1054 ms, against 1085 ms for the previous revision in the same run.
  • Test-dir modules: the import loads 1489 modules, none of them from a test directory.

I updated the PR description to match.

Written by Devin

@pnilan

Copy link
Copy Markdown
Member

Resolve conflicts

@pnilan
Patrick Nilan (pnilan) added this pull request to stack #1245 October 21, 2026 21:57
…precompile-bytecode

Co-Authored-By: bot_apk <apk@cognition.ai>

# Conflicts:
#	Dockerfile
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I resolved the conflicts in 7364108, which merges main into this branch. The only conflict was in Dockerfile: the RUN python -c "import pandas" check from #1221 and this PR's compileall step were added at the same spot. I kept both, with the pandas check first.

GitHub now shows the PR as mergeable and no new commits have landed on main since. CI is re-running on the merge commit.

Written by Devin

@pnilan
Patrick Nilan (pnilan) merged commit 8844b50 into main Oct 22, 2026
21 checks passed
@pnilan
Patrick Nilan (pnilan) deleted the devin/1791497733-sdm-precompile-bytecode branch October 22, 2026 02:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant