Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## [1.2.2] - 2026-03-18

- Reset inherited singleton state after `fork()` by registering a child-only `os.register_at_fork()` handler [#26]
- Add POSIX fork tests covering inherited lock state and fresh singleton reinitialization [#26]

## [1.2.1] - 2026-02-25

- Fix SDK version header: use correct header name `X-Reforge-SDK-Version` and value format `sdk-python-{version}` to match all other SDKs [#25]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "sdk-reforge"
version = "1.2.1"
version = "1.2.2"
description = "Python sdk for Reforge Feature Flags and Config as a Service: https://www.reforge.com"
license = "MIT"
authors = ["Michael Berkowitz <michael.berkowitz@gmail.com>", "James Kebinger <james.kebinger@reforge.com>"]
Expand Down
2 changes: 1 addition & 1 deletion sdk_reforge/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.2.1
1.2.2
12 changes: 12 additions & 0 deletions sdk_reforge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ def _get_version() -> str:
__lock = _ReadWriteLock()


def _reset_singleton_after_fork() -> None:
"""Drop inherited singleton state in forked children."""
global __base_sdk
global __lock
__base_sdk = None
__lock = _ReadWriteLock()


if hasattr(os, "register_at_fork"):
os.register_at_fork(after_in_child=_reset_singleton_after_fork)


def set_options(options: Options) -> None:
"""Configure the SDK. SDK will be instantiated lazily with these options. Setting them again will have no effect unless reset_instance is called"""
global __options
Expand Down
98 changes: 98 additions & 0 deletions tests/test_forking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import os
import threading

import pytest

import sdk_reforge
from sdk_reforge import Options


def _read_exact(fd: int, size: int) -> bytes:
chunks = []
remaining = size
while remaining > 0:
chunk = os.read(fd, remaining)
if not chunk:
break
chunks.append(chunk)
remaining -= len(chunk)
return b"".join(chunks)


@pytest.mark.skipif(not hasattr(os, "fork"), reason="requires os.fork")
def test_child_process_replaces_inherited_singleton_lock():
sdk_reforge.reset_instance()

lock = getattr(sdk_reforge, "__lock")
ready = threading.Event()
release = threading.Event()

def hold_lock():
with lock.write_locked():
ready.set()
release.wait(timeout=5)

holder = threading.Thread(target=hold_lock)
holder.start()
ready.wait(timeout=2)

read_fd, write_fd = os.pipe()
pid = os.fork()
if pid == 0:
try:
os.close(read_fd)
sdk_reforge.set_options(
Options(
reforge_datasources="LOCAL_ONLY",
collect_sync_interval=None,
)
)
os.write(write_fd, b"ok")
finally:
os.close(write_fd)
os._exit(0)

os.close(write_fd)
try:
os.waitpid(pid, 0)
assert _read_exact(read_fd, 2) == b"ok"
finally:
os.close(read_fd)
release.set()
holder.join(timeout=2)
sdk_reforge.reset_instance()


@pytest.mark.skipif(not hasattr(os, "fork"), reason="requires os.fork")
def test_child_process_creates_fresh_singleton_sdk_instance():
sdk_reforge.reset_instance()
sdk_reforge.set_options(
Options(
reforge_datasources="LOCAL_ONLY",
collect_sync_interval=None,
)
)
parent_sdk = sdk_reforge.get_sdk()
parent_hash = parent_sdk.instance_hash

read_fd, write_fd = os.pipe()
pid = os.fork()
if pid == 0:
try:
os.close(read_fd)
child_sdk = sdk_reforge.get_sdk()
payload = child_sdk.instance_hash.encode("utf-8")
os.write(write_fd, payload)
finally:
os.close(write_fd)
os._exit(0)

os.close(write_fd)
try:
os.waitpid(pid, 0)
child_hash = _read_exact(read_fd, len(parent_hash)).decode("utf-8")
assert child_hash
assert child_hash != parent_hash
finally:
os.close(read_fd)
sdk_reforge.reset_instance()
Loading