Skip to content

Fix TypeError in os.fork() for garbage-collected processors#5453

Open
pranaysb wants to merge 11 commits into
open-telemetry:mainfrom
pranaysb:fix/os-fork-gc-bug
Open

Fix TypeError in os.fork() for garbage-collected processors#5453
pranaysb wants to merge 11 commits into
open-telemetry:mainfrom
pranaysb:fix/os-fork-gc-bug

Conversation

@pranaysb

Copy link
Copy Markdown
Contributor

Description

Fixes TypeError: 'NoneType' object is not callable in os.fork() when a BatchProcessor or PeriodicExportingMetricReader has been garbage collected.

The after_in_child callbacks registered via os.register_at_fork during initialization stored weak references to reinit methods, but unconditionally invoked them without checking if the reference had resolved to None. This PR adds a safe _after_in_child method that checks for None before invoking the callback, matching the existing behavior of ConcurrentMultiSpanProcessor.

Fixes #5452

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Verified via the existing test suite and added specific unit tests verifying that garbage-collected processors do not crash the child process upon fork.

  • test_garbage_collected_processor_does_not_crash_on_fork in tests/shared_internal/test_batch_processor.py
  • test_garbage_collected_processor_does_not_crash_on_fork in tests/metrics/test_periodic_exporting_metric_reader.py

Does This PR Require a Contrib Repo Change?

  • Yes. - Link to PR:
  • No.

Checklist:

  • Followed the style guidelines of this project
  • Changelogs have been updated
  • Unit tests have been added
  • Documentation has been updated

@pranaysb
pranaysb requested a review from a team as a code owner July 23, 2026 01:42
Comment thread opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/__init__.py Outdated
Comment thread opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py Outdated

# string msg with no args - getMessage bypasses formatting and sets the string directly
logger.warning("a string with a percent-s: %s")
logger.warning("a string with a percent-s: %s") # pylint: disable=logging-too-few-args

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.

Why are we touching this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah, sorry about that! When I ran tox -e lint-opentelemetry-sdk locally to verify my changes, it failed on this exact line with E1206: Not enough arguments for logging format string (logging-too-few-args).

I added the inline suppression just to get the linting checks to pass, but I realize now I shouldn't be touching unrelated files. I have reverted this to keep the PR strictly focused on the os.fork() bug! (If the lint CI check ends up failing on this line again, just let me know how you'd prefer I handle it).

@github-project-automation github-project-automation Bot moved this to Reviewed PRs that need fixes in Python PR digest Jul 23, 2026
@herin049
herin049 requested a review from Copilot July 23, 2026 04:05

Copilot AI 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.

Pull request overview

This PR fixes a TypeError: 'NoneType' object is not callable that can occur during os.fork() when at-fork callbacks are registered via os.register_at_fork using weakref.WeakMethod, and the underlying processor/reader instance has been garbage collected. The change updates the at-fork callback implementations to safely no-op when the weak reference has expired, aligning behavior with existing fork-handling patterns in the SDK.

Changes:

  • Guard os.register_at_fork(after_in_child=...) callbacks so weakly-referenced bound methods are only invoked when still alive.
  • Add fork-focused regression tests for BatchProcessor and PeriodicExportingMetricReader to ensure no unraisable exception is emitted to stderr after GC.
  • Add a targeted pylint suppression in a log export test; add changelog entry.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/init.py Makes BatchProcessor’s at-fork callback safe when the weak method has expired.
opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/init.py Makes PeriodicExportingMetricReader’s at-fork callback safe when the weak method has expired.
opentelemetry-sdk/tests/shared_internal/test_batch_processor.py Adds a regression test that forks after GC and asserts no TypeError is printed to stderr.
opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py Adds a regression test that forks after GC and asserts no TypeError is printed to stderr.
opentelemetry-sdk/tests/logs/test_export.py Adds an inline pylint suppression for an intentional “too few args” logging call in a test.
.changelog/5453.fixed Documents the bugfix in the changelog system.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +226 to +231
exporter = Mock()
processor = batch_processor_class(exporter)
processor.shutdown()
del processor
gc.collect()

Comment on lines +219 to +222
@unittest.skipUnless(
hasattr(os, "fork"),
"needs *nix",
)
Comment on lines +239 to +254
old_fd = os.dup(sys.stderr.fileno())
os.dup2(w_fd, sys.stderr.fileno())
old_hook = sys.unraisablehook
sys.unraisablehook = sys.__unraisablehook__

pid = os.fork()
if pid == 0:
os.close(r_fd)
# os.fork() has already run the at_fork hooks.
os._exit(0)

# Restore original stderr and hook in parent
sys.unraisablehook = old_hook
os.dup2(old_fd, sys.stderr.fileno())
os.close(old_fd)
os.close(w_fd)
Comment on lines +319 to +323
processor = PeriodicExportingMetricReader(exporter)
processor.shutdown()
del processor
gc.collect()

Comment on lines +312 to +313
@pytest.mark.skipif(not hasattr(os, "fork"), reason="needs *nix")
def test_garbage_collected_processor_does_not_crash_on_fork(self):
Comment on lines +331 to +346
old_fd = os.dup(sys.stderr.fileno())
os.dup2(w_fd, sys.stderr.fileno())
old_hook = sys.unraisablehook
sys.unraisablehook = sys.__unraisablehook__

pid = os.fork()
if pid == 0:
os.close(r_fd)
# os.fork() has already run the at_fork hooks.
os._exit(0)

# Restore original stderr and hook in parent
sys.unraisablehook = old_hook
os.dup2(old_fd, sys.stderr.fileno())
os.close(old_fd)
os.close(w_fd)
pranaysb and others added 5 commits July 23, 2026 13:12
…t__.py

Co-authored-by: Lukas Hering <40302054+herin049@users.noreply.github.com>
…rt/__init__.py

Co-authored-by: Lukas Hering <40302054+herin049@users.noreply.github.com>
Assisted-by: Google Antigravity
Assisted-by: Google Antigravity
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Reviewed PRs that need fixes

Development

Successfully merging this pull request may close these issues.

BatchProcessor and PeriodicExportingMetricReader cause TypeError: 'NoneType' object is not callable on os.fork() after being garbage collected

3 participants