Cotyledon
Cotyledon is a Python framework (version 2.2.0, actively maintained) designed for defining and managing long-running services. It provides robust handling of Unix signals, efficient spawning and supervision of worker processes, daemon reloading capabilities, `sd-notify` integration, and rate limiting for worker restarts. It sees significant use in OpenStack Telemetry projects as a lightweight replacement for `oslo.service`, which carried heavy `eventlet` dependencies. The library aims for a consistent code path for single and multiple worker configurations and offers advanced reload and termination APIs.
Common errors
-
TypeError: __init__() missing 1 required positional argument: 'worker_id'
cause Your custom service class (inheriting from `cotyledon.Service`) must define an `__init__` method that accepts `worker_id` as its first argument after `self` and passes it to `super().__init__(worker_id)`.fixEnsure your service class's `__init__` method signature is `def __init__(self, worker_id, *args, **kwargs):` and calls `super().__init__(worker_id, *args, **kwargs)`. -
Service process exited unexpectedly (exit code N)
cause A worker process terminated prematurely. This can be due to unhandled exceptions within the worker's `run()` method, memory issues, or incorrect termination logic.fixInspect worker logs for unhandled exceptions or error messages. Ensure your `run()` method properly handles its work loop and that the `terminate()` method gracefully shuts down resources. Increase logging level for the specific service to debug. -
SIGHUP received but service does not reload
cause Your custom service class has not implemented the `reload()` method, or the implementation does not include the desired reload logic.fixOverride the `reload()` method in your `cotyledon.Service` subclass. This method is called when `SIGHUP` is received, allowing you to implement logic like reloading configuration files or re-initializing state without fully stopping and restarting the worker process. If no `reload()` method is defined, `cotyledon` will still restart the worker for a full reload.
Warnings
- breaking When migrating from `oslo.service`, be aware that Cotyledon does not rely on `eventlet` for greenlet-based concurrency or monkey-patching the standard library. This means applications depending on `eventlet`'s behavior will need significant refactoring.
- gotcha Cotyledon does not provide built-in facilities for WSGI application creation or socket sharing between parent and child processes. If your `oslo.service` based application relied on these features for HTTP services, Cotyledon is not a direct drop-in replacement.
- gotcha The `ServiceManager` includes a 'seatbelt' mechanism to prevent multiple service managers from running concurrently, which can lead to unexpected behavior if multiple instances of your application are launched in the same environment.
Install
-
pip install cotyledon
Imports
- Service
from cotyledon import Service
- ServiceManager
from cotyledon import ServiceManager
Quickstart
import cotyledon
import logging
import threading
import time
import os
LOG = logging.getLogger(__name__)
class MyService(cotyledon.Service):
name = "my_example_service"
def __init__(self, worker_id):
super(MyService, self).__init__(worker_id)
self._shutdown = threading.Event()
LOG.info(f"[{os.getpid()}] {self.name} worker {self.worker_id} init")
def run(self):
LOG.info(f"[{os.getpid()}] {self.name} worker {self.worker_id} running...")
# In a real service, this loop would perform work, e.g., consume from a queue
# and call _shutdown.set() when it needs to stop processing.
while not self._shutdown.is_set():
LOG.debug(f"[{os.getpid()}] {self.name} worker {self.worker_id} working...")
time.sleep(1)
def terminate(self):
LOG.info(f"[{os.getpid()}] {self.name} worker {self.worker_id} terminating...")
self._shutdown.set()
def reload(self):
LOG.info(f"[{os.getpid()}] {self.name} worker {self.worker_id} reloading...")
# Implement logic to reload configuration or re-initialize components
# without stopping the worker if possible.
def main():
# Basic setup for logging to console
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(process)d - %(levelname)s - %(message)s')
LOG.info("Starting Cotyledon Service Manager")
manager = cotyledon.ServiceManager()
# Add MyService with 2 worker processes
manager.add(MyService, workers=2)
# Run the service manager, which will spawn workers and handle signals
manager.run()
LOG.info("Cotyledon Service Manager stopped.")
if __name__ == "__main__":
main()