features: - | Added a new `threading` backend to `oslo.service`, using standard Python threads instead of `eventlet`. This includes a full implementation of: - `Service`, `Launcher`, `ServiceLauncher`, and `ProcessLauncher` using `cotyledon` - `LoopingCall` variants (`FixedIntervalLoopingCall`, `DynamicLoopingCall`, etc.) using `futurist.ThreadPoolExecutor` - A new `ThreadGroup` and `Thread` abstraction to mimic `eventlet`-like behavior - A native `SignalHandler` implementation using standard Python signals This backend provides a standard-thread-compatible alternative that avoids monkey-patching, making it suitable for environments where `eventlet` is problematic or undesirable. Additionally: - `ProcessLauncher` now supports a `no_fork=True` mode, allowing services to run in the main process without forking. This is useful when `fork()` is unsafe — for example, in threaded environments or with Python 3.12+ where the default multiprocessing start method has changed to `spawn`. - A new `register_backend_default_hook()` API has been added. It allows users to define a fallback backend type in case `init_backend()` was not called early enough. This is helpful in environments where import order or initialization timing cannot be guaranteed. Example: ```python from oslo_service import backend backend.register_backend_default_hook(lambda: backend.BackendType.THREADING) ``` This hook will only be used if `init_backend()` has not already been called. upgrade: - | While Python 3.14 defaults to ``spawn`` as the multiprocessing start method, `oslo.service` continues to rely on ``fork`` as the only supported method for creating worker processes. Many parts of OpenStack depend on objects that cannot be safely pickled (e.g. argparse parsers, thread locks, lambdas in config defaults), which makes ``spawn`` currently impractical. In the long term, process scaling should be handled externally (e.g. via Kubernetes or systemd), rather than by the service itself. issues: - | When using the `threading` backend with multiple workers (i.e. `ProcessLauncher` with `fork()`), **starting threads before the fork occurs can lead to corrupted state** due to how `os.fork()` behaves in multi-threaded processes. This is a known limitation in Python. See: https://gibizer.github.io/posts/Eventlet-Removal-The-First-Threading-Bug/#threads--osfork--confused-threadpools To avoid this issue, you can: - Ensure that no threads are started before `oslo.service` forks the workers. - Use `ProcessLauncher(no_fork=True)` to disable forking entirely. - Explicitly manage thread lifecycle — for example, stop all threads before forking, as currently done in Nova.