Skip to content

Bump protobuf 4.25.8 → 5.29.6 - #349

Open
reitblatt wants to merge 4 commits into
roostorg:mainfrom
reitblatt:update-protobuf
Open

Bump protobuf 4.25.8 → 5.29.6#349
reitblatt wants to merge 4 commits into
roostorg:mainfrom
reitblatt:update-protobuf

Conversation

@reitblatt

@reitblatt reitblatt commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Description

Depends on #415 (branch rebased on top of it) — grpcio is bumped there separately. Until #415 merges into main, this PR's diff/commit list will include its two commits (Bump grpcio to 1.82.1, Add CHANGELOG entry for grpcio bump); they'll drop out automatically once #415 lands.

On top of that, this PR upgrades protobuf and grpcio-tools, and as a downstream consequence, google-cloud-pubsub and tink, to versions compatible with protobuf 5.x. Relaxes google-cloud-kms, grpcio-health-checking, grpcio-reflection, and grpcio-status from exact pins to floor constraints.

Regenerate all *_pb2.py files via ./gen-protos.sh.

Resolves #316, #317

Test plan:

./run-tests.sh

Checklist

  • Tests pass locally
  • uv run ruff check . passes (no unused imports or other lint errors)
  • uv tool run fawltydeps --check-unused --pyenv .venv passes (no unused dependencies)
  • Updated CHANGELOG.md with my changes, if applicable

Summary by CodeRabbit

Summary by CodeRabbit

  • Chores
    • Upgraded protobuf to 7.35.1 and refreshed related protobuf/crypto/tooling libraries.
    • Modernized the gRPC dependency set (grpcio/ grpcio-tools to 1.82.1, plus updated Google/protobuf dependencies) and regenerated protobuf/gRPC bindings.
    • Updated cloud library constraints (including KMS and Pub/Sub).
  • Behavior Changes
    • Added import-time protobuf and gRPC runtime compatibility checks with clear upgrade/downgrade guidance when versions don’t match.

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b36adc8b-3c12-49df-82a2-84586fc2ea0b

📥 Commits

Reviewing files that changed from the base of the PR and between b923855 and 018d9c2.

⛔ Files ignored due to path filters (37)
  • osprey_rpc/src/osprey/rpc/actions/v1/action_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/action_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/action_types_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/action_types_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/application_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/application_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/captcha_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/captcha_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/channel_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/channel_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/guild_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/guild_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/invite_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/invite_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/metadata_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/metadata_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/user_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/user_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/execution_result_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/execution_result_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/take_data_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/take_data_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/verdicts_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/verdicts_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/etcd_watcherd/v1/etcd_watcherd_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/etcd_watcherd/v1/etcd_watcherd_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/osprey_coordinator/bidirectional_stream/v1/service_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/osprey_coordinator/bidirectional_stream/v1/service_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/osprey_coordinator/sync_action/v1/service_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/osprey_coordinator/sync_action/v1/service_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/pigeon/tests/v1/tests_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/pigeon/tests/v1/tests_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/pigeon/v1/options_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/pigeon/v1/options_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/request_caching/v1/service_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/request_caching/v1/service_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • CHANGELOG.md
  • pyproject.toml

📝 Walkthrough

Walkthrough

Dependency constraints are updated for protobuf, gRPC, Tink, and related Google Cloud packages. Protobuf and gRPC bindings are regenerated with runtime compatibility checks, and generated service wiring now uses registered-method APIs.

Changes

Protobuf and gRPC upgrade

Layer / File(s) Summary
Dependency constraints and release documentation
pyproject.toml, CHANGELOG.md
Updates protobuf, gRPC, Tink, and Google Cloud constraints and documents the regenerated bindings.
Protobuf runtime and descriptor regeneration
osprey_rpc/src/osprey/rpc/**/*.py
Adds protobuf runtime validation and updates descriptor construction and non-C-descriptor handling.
Generated gRPC compatibility guards
osprey_rpc/src/osprey/rpc/**/*_pb2_grpc.py
Adds import-time checks requiring compatible installed gRPC versions.
Registered RPC method wiring
osprey_rpc/src/osprey/rpc/**/*_pb2_grpc.py
Marks generated RPC methods as registered and uses registered-method server handlers.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

  • #317 — Updates protobuf and regenerates protobuf/gRPC bindings, overlapping this PR’s superseding dependency changes.

Suggested reviewers: exbreder, vinaysrao1, haileyok, dependabot[bot], ayubun

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and matches the protobuf dependency bump, which is part of the PR.
Linked Issues check ✅ Passed pyproject bumps grpcio well past 1.53.2 and updates the grpc stack required by #316.
Out of Scope Changes check ✅ Passed The protobuf regeneration and dependency updates appear consistent with the grpc/protobuf upgrade work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@reitblatt
reitblatt marked this pull request as ready for review June 15, 2026 16:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 11: Replace the placeholder PR link in the CHANGELOG.md file where the
protobuf upgrade is documented. Find the text
`[`#XXX`](https://github.com/roostorg/osprey/pull/XXX)` and replace both the link
text (`#XXX`) and the URL with the actual PR number of this change to create a
valid changelog reference.

In `@pyproject.toml`:
- Around line 26-28: Multiple dependency packages are being upgraded across
pyproject.toml (google-cloud-logging at lines 26-28, google-cloud-pubsub at
lines 26-28, protobuf/grpcio/tink/grpcio-tools at lines 36-41, and additional
packages at lines 56 and 77), but the PR lacks documented evidence of the
required CVE and license review approvals mandated by AGENTS.md. Add a checklist
or link in the PR description or as a comment documenting that you have verified
each upgraded package for license compatibility with LICENSE.md and confirmed
there are no known CVEs, covering all affected dependency upgrades across the
entire file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c9680a2c-49f2-4ca1-a5d4-6873e6b4beb7

📥 Commits

Reviewing files that changed from the base of the PR and between 219528a and 3ee059e.

⛔ Files ignored due to path filters (37)
  • osprey_rpc/src/osprey/rpc/actions/v1/action_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/action_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/action_types_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/action_types_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/application_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/application_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/captcha_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/captcha_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/channel_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/channel_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/guild_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/guild_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/invite_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/invite_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/metadata_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/metadata_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/user_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/user_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/execution_result_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/execution_result_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/take_data_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/take_data_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/verdicts_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/verdicts_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/etcd_watcherd/v1/etcd_watcherd_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/etcd_watcherd/v1/etcd_watcherd_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/osprey_coordinator/bidirectional_stream/v1/service_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/osprey_coordinator/bidirectional_stream/v1/service_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/osprey_coordinator/sync_action/v1/service_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/osprey_coordinator/sync_action/v1/service_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/pigeon/tests/v1/tests_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/pigeon/tests/v1/tests_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/pigeon/v1/options_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/pigeon/v1/options_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/request_caching/v1/service_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/request_caching/v1/service_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • CHANGELOG.md
  • pyproject.toml

Comment thread CHANGELOG.md Outdated
Comment thread pyproject.toml
@julietshen

Copy link
Copy Markdown
Member

tagging in @haileyok @cmttt @EXBreder for their thoughts! I forget if this upgrade is the one @ayubun suggested we have more robust testing in place beforehand or not.

@haileyok

Copy link
Copy Markdown
Member

grpcio/grpcio-tools to 1.71.2/1.74.0

yea this has been problematic in the past (both for me when deploying at bluesky and at discord). there are some documented issues here grpc/grpc#38327

@reitblatt

Copy link
Copy Markdown
Contributor Author

As discussed on Discord, we should block until stress testing is landed (#324).

@haileyok

haileyok commented Jun 17, 2026

Copy link
Copy Markdown
Member

Sharing some Claude findings re: the bump of grpcio. these are current unverified in the osprey repo, but were verified via testing the library outside of osprey

grpcio aio channel-churn memory leak (#38327): root cause, the fix, and how it was found

Summary

  • Symptom: On grpcio 1.71.0, repeatedly creating and closing a Python grpc.aio channel
    grows process RSS without bound. Reported in grpc/grpc#38327
    (and #36986, #36117), e.g. Dagster webserver/daemon OOMs.
  • Root cause: Not a glibc/arena artifact and not the proto_buffer_reader Cord allocation the
    issue speculates about. It is an unbounded std::vector in the EventEngine's fork-handler registry
    (ObjectGroupForkHandler::forkables_). Every time the EventEngine is rebuilt — which the aio stack
    does on every channel teardown — its sub-objects re-register a weak_ptr into that global
    vector, and the vector is only ever pruned during an actual fork(). A long-running, never-forking
    process accumulates entries (and the control blocks they pin) forever.
  • The fix: grpc/grpc#38980 "[event_engine] Event engine fork support",
    commit cad1d0eef74f1d8c85a5eed07f433e84db1a9f52 (Eugene Ostroukhov, 2025-06-03). It deletes the
    ObjectGroupForkHandler registry (forkable.cc/forkable.h) and all RegisterForkable call sites.
    First released in grpcio 1.74.0.
  • Action: Upgrade to ≥ 1.74 (latest 1.81.x is cleanest). 1.71.0 is not a safe upgrade target —
    it still leaks. There is no source fix to upstream; it is already fixed.

How it was found

1. Reproduce, and separate the two codepaths

The issue's reproducer creates/closes a channel in a loop. Testing carefully:

  • Sync grpc.insecure_channel create/close → RSS plateaus (~37 MB). The small excess over
    1.57.0 is glibc per-thread arena overhead; MALLOC_ARENA_MAX=1 flattens it. Not a real leak.
  • aio grpc.aio.insecure_channel create/close → RSS grows linearly and never plateaus, and
    MALLOC_ARENA_MAX=1 does not stop it. This is genuine un-freed memory.

The aio path is the real bug. This matches the strongest reports in the thread: a macOS aio-server
report (macOS has no glibc arenas, so it can't be arena fragmentation) and memray traces showing
growing live allocation blocks.

2. Identify the trigger

The leak only fires when the aio refcount drops to 0 — i.e. when the last channel is deallocated.
That is what rebuilds the whole aio stack (poller completion queue + Core + EventEngine) on the next
channel. Consequences, all confirmed empirically:

  • The issue's run()-helper pattern (channel closed and dropped each iteration) triggers it.
  • Holding the channel in a variable, or sustained concurrency (several channels always open), keeps
    the refcount > 0 and hides the leak. (This is why production reports were so inconsistent, and
    it briefly fooled this investigation via a harness bug.)
  • Sync doesn't leak because the sync stack holds a process-lifetime grpc_init reference
    (fork_handlers_and_grpc_init), so Core/EventEngine never fully shut down and never get rebuilt.

3. Bisect to the exact release, then the exact commit

  • Released wheels (worst-case serial prompt-dealloc repro): 1.71.0 leaks → 1.74.0 bounded →
    1.76.0 flat → 1.81.1 flat. So the fix shipped in 1.74.0.
  • git bisect over the 193-commit master range between the 1.73 branch point and a known-good
    1.74-era commit, 8 from-source builds, each running the repro with a LEAK/FIXED verdict
    (peak − mid RSS > 1.5 MB over 24k cycles). Parent 717c0fae4b LEAKs; cad1d0eef7 is the first
    build that plateaus → cad1d0eef7 = PR #38980 is the fix.

4. Pin down the exact mechanism (no guessing)

Reading the #38980 diff, the lone lifecycle change that stood out was the removal of a global
fork-handler registry that every EventEngine sub-object registered into. That registry is gated on
IsForkEnabled(). Decisive runtime test on the leaking 1.73.1 wheel (no rebuild needed):

Run Verdict
default LEAK
GRPC_ENABLE_FORK_SUPPORT=1 LEAK
GRPC_ENABLE_FORK_SUPPORT=0 FIXED (flat)

Disabling fork support makes the leak vanish → the leak is the fork-handler registry. (grpcio's
prebuilt wheels compile with fork support on: the core enable_fork_support config defaults to
GRPC_ENABLE_FORK_SUPPORT_DEFAULT, which the wheels define to true.)


Root cause in detail

Before #38980, src/core/lib/event_engine/forkable.{h,cc} defined a process-global registry used to
run pthread_atfork-style callbacks on the EventEngine's internals:

class ObjectGroupForkHandler {
  // ...
  std::vector<std::weak_ptr<Forkable>> forkables_;   // grows here
};

void ObjectGroupForkHandler::RegisterForkable(
    std::shared_ptr<Forkable> forkable, ...) {
  if (IsForkEnabled()) {
    CHECK(!is_forking_);
    forkables_.emplace_back(forkable);   // append on every creation
    // (pthread_atfork registered once)
  }
}

// The ONLY place expired entries are removed is inside the fork hooks:
void ObjectGroupForkHandler::Prefork() {
  if (IsForkEnabled()) {
    for (auto it = forkables_.begin(); it != forkables_.end();) {
      auto shared = it->lock();
      if (shared) { shared->PrepareFork(); ++it; }
      else        { it = forkables_.erase(it); }   // pruning only on fork
    }
  }
}

These handlers are static per translation unit (e.g. g_thread_pool_fork_manager in
thread_pool/thread_pool_factory.cc), so they live for the whole process. The leak chain:

  1. grpcio Python builds run with fork support enabled → IsForkEnabled() is true.
  2. The aio stack tears down and rebuilds the EventEngine every time the last channel closes
    (Core refcount → 0). Sync does not (it pins Core for the process).
  3. Each EventEngine rebuild creates a new thread pool, poller, timer manager, etc. — each of which
    calls RegisterForkable, appending a weak_ptr to its global forkables_ vector.
  4. Those vectors are pruned only during an actual fork(). A long-running server that never
    forks never prunes them, so they grow without bound. Worse, each lingering weak_ptr keeps the
    destroyed object's shared-pointer control block alive, so it is not just 16 bytes/entry.
  5. Net: ~0.25 MB leaked per 1,000 channel create/close cycles, unbounded over time.

This explains every observation: aio-only (sync pins Core), hidden by concurrency (refcount stays

0), unaffected by MALLOC_ARENA_MAX (it is genuinely live heap, a growing std::vector + control
blocks), present on macOS (allocator-independent), and visible as growing live blocks in memray.


What in PR #38980 actually fixes it

The fix is the removal of the ObjectGroupForkHandler registry and all RegisterForkable calls,
replacing managed-fork support with a different mechanism (a per-engine FileDescriptorCollection /
PosixInterface that closes/repairs fds across fork without a process-global accumulator).

Concretely, commit cad1d0eef7:

  • Deletes src/core/lib/event_engine/forkable.cc and src/core/lib/event_engine/forkable.h
    (and test/core/event_engine/forkable_test.cc) — the vector that grew.
  • Removes the RegisterForkable call sites that ran on every EventEngine rebuild:
    • src/core/lib/event_engine/thread_pool/thread_pool_factory.ccMakeThreadPool no longer
      registers each pool with g_thread_pool_fork_manager (the whole global handler is gone):

      -#include "src/core/lib/event_engine/forkable.h"
      -#include "src/core/util/no_destruct.h"
      -namespace {
      -grpc_core::NoDestruct<ObjectGroupForkHandler> g_thread_pool_fork_manager;
      -class ThreadPoolForkCallbackMethods { ... };
      -}  // namespace
      
       std::shared_ptr<ThreadPool> MakeThreadPool(size_t reserve_threads) {
         auto thread_pool = std::make_shared<WorkStealingThreadPool>(reserve_threads);
      -  g_thread_pool_fork_manager->RegisterForkable(thread_pool, ...);
         return thread_pool;
       }
    • src/core/lib/event_engine/posix_engine/posix_engine.cc

    • src/core/lib/event_engine/posix_engine/event_poller_posix_default.cc

With no global registry, repeatedly creating and destroying EventEngines no longer accumulates
anything, so the aio create/close loop stops leaking.

Note: the leak fix is incidental to #38980, whose stated purpose is a POSIX EventEngine fork
rewrite. Nothing in the PR references #38327, which is almost certainly why the issue is still open
despite being fixed since 1.74.0.


Recommendation

  • Upgrade grpcio to ≥ 1.74.0 (ideally latest 1.81.x: ~37 MB flat vs 1.74's ~40 MB plateau,
    a minor follow-on improvement). Do not upgrade to 1.71.0 — it still leaks.
  • Interim mitigations if pinned to an affected version:
    • Reuse a long-lived aio channel instead of one-per-request (keeps Core refcount > 0 → no rebuild,
      no leak). This is the best application-level fix regardless of version.
    • GRPC_ENABLE_FORK_SUPPORT=0 removes the leak too, but only do this if the process does not rely
      on grpc fork support.

Reproduction / verification artifacts

All scripts, per-version and per-commit logs, and bisect logs are in /home/discord/claude-debug-grpcio/:
aio_client.py / conc_client.py (repro), aio_server.py, test_wheel*.sh (release bisect),
bisect_run.sh + bisect-full3.log (commit bisect), test_forksupport.sh (fork-support proof),
FINDINGS.md (running log).

@reitblatt

Copy link
Copy Markdown
Contributor Author

Sharing some Claude findings re: the bump of grpcio. these are current unverified in the osprey repo, but were verified via testing the library outside of osprey

Recommendation

  • Upgrade grpcio to ≥ 1.74.0 (ideally latest 1.81.x: ~37 MB flat vs 1.74's ~40 MB plateau,
    a minor follow-on improvement). Do not upgrade to 1.71.0 — it still leaks.

The comments + changelog were out of date after the latest iterations. This PR actually pins grpcio to 1.74.0. I'll fix the changelog.

@reitblatt

reitblatt commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

As for the request in #400 to separate protobufs and grpcio upgrades, I can send out a separate PR to just upgrade grpcio. It has no dependencies itself, so should be pretty easy and clean.

Edit: done in #415

reitblatt and others added 2 commits July 11, 2026 04:20
Collapses the platform_machine split (grpcio 1.49.1 on x86_64, 1.53.x
elsewhere) into a single pin now that upstream ships wheels for both
platforms again. Also bumps typing-extensions to 4.12.2, which grpcio
1.82.1 requires (>=4.12,<5).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@julietshen

Copy link
Copy Markdown
Member

@reitblatt that would be great, if you can help separate it! Thanks so much!

Update grpcio/grpcio-tools to 1.74.0/1.71.2, google-cloud-pubsub, and
tink to versions compatible with protobuf 5.x. Relax google-cloud-kms,
grpcio-health-checking, grpcio-reflection, and grpcio-status from exact
pins to floor constraints. Unify the grpcio platform split into a
single pin.

google-cloud-logging is not bumped here: it (along with
google-cloud-secret-manager, google-cloud-appengine-logging, and
google-cloud-audit-log) was dropped as an unused Discord-era dependency
by roostorg#391 while this branch was in flight, so there's nothing left to
upgrade.

Regenerate all *_pb2.py files via ./gen-protos.sh.

Test plan:
./run-tests.sh

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pyproject.toml (1)

83-83: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

types-protobuf is pinned two major versions behind the protobuf runtime.

types-protobuf==4.24.0.1 (line 83, unchanged) is now mismatched with protobuf==5.29.6 (line 47). The 4.x type stubs won't cover protobuf 5.x API changes, which could cause mypy to miss type errors or flag false positives in code that uses protobuf 5.x features. Consider bumping types-protobuf to a 5.x-compatible release.

Note: mypy excludes *_pb2*.py files (lines 265-267), so generated code is unaffected, but hand-written code that imports protobuf types directly would be impacted.

As per path instructions, this dependency upgrade requires human approval for license and CVE review.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` at line 83, Update the types-protobuf dependency entry to a
5.x-compatible release matching the protobuf==5.29.6 runtime, then request human
approval for the required license and CVE review.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pyproject.toml`:
- Line 83: Update the types-protobuf dependency entry to a 5.x-compatible
release matching the protobuf==5.29.6 runtime, then request human approval for
the required license and CVE review.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fd1b7f4b-1e99-4871-b92c-c18110151ee6

📥 Commits

Reviewing files that changed from the base of the PR and between 0004923 and b923855.

⛔ Files ignored due to path filters (37)
  • osprey_rpc/src/osprey/rpc/actions/v1/action_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/action_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/action_types_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/action_types_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/application_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/application_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/captcha_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/captcha_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/channel_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/channel_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/guild_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/guild_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/invite_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/invite_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/metadata_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/metadata_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/user_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/actions/v1/user_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/execution_result_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/execution_result_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/take_data_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/take_data_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/verdicts_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/common/v1/verdicts_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/etcd_watcherd/v1/etcd_watcherd_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/etcd_watcherd/v1/etcd_watcherd_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/osprey_coordinator/bidirectional_stream/v1/service_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/osprey_coordinator/bidirectional_stream/v1/service_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/osprey_coordinator/sync_action/v1/service_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/osprey_coordinator/sync_action/v1/service_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/pigeon/tests/v1/tests_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/pigeon/tests/v1/tests_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/pigeon/v1/options_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/pigeon/v1/options_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/request_caching/v1/service_pb2.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • osprey_rpc/src/osprey/rpc/request_caching/v1/service_pb2_grpc.py is excluded by !osprey_rpc/src/osprey/rpc/**/*_pb2*.py
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • CHANGELOG.md
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Update grpcio-tools to 1.82.1 (to match grpcio, and required by
grpcio-tools itself for protobuf 7.x support), google-api-core to
2.31.0, googleapis-common-protos to 1.75.0, and grpc-google-iam-v1 to
0.14.4 — all of which capped protobuf below 7.0 at their prior pins.
Also bump types-protobuf to match the new protobuf major version.

google-cloud-pubsub, tink, and google-cloud-kms already tolerate
protobuf 7.x, so no changes needed there.

Regenerate all *_pb2.py files via ./gen-protos.sh.

Test plan:
./run-tests.sh (1176 passed, 0 failed, 0 errors)
uv run mypy osprey_worker osprey_rpc osprey_async_worker example_plugins
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bump grpcio to 1.53.x (CVE-2023-32731)

3 participants