The on_start and on_stop hooks in Locust aren’t just for setup and teardown; they’re the primary mechanism for managing shared state across your entire distributed test session, allowing you to coordinate actions and data between user instances.
Let’s see this in action. Imagine you need to ensure a specific configuration setting is applied before any users start hitting your system, and then clean it up afterward.
from locust import HttpUser, task, between, events
import requests
# Global dictionary to hold shared state
shared_state = {
"config_applied": False,
"user_count": 0
}
class WebsiteUser(HttpUser):
wait_time = between(1, 5)
host = "http://localhost:8080"
@task
def index(self):
self.client.get("/")
@events.test_start.add_listener
def _(ch, **kw):
print("\n--- Test starting ---")
# Apply a global configuration setting (simulated)
try:
response = requests.post(f"{self.host}/admin/config", json={"setting": "new_value"})
if response.status_code == 200:
shared_state["config_applied"] = True
print("Global configuration applied successfully.")
else:
print(f"Failed to apply global configuration: {response.status_code}")
except requests.exceptions.ConnectionError:
print("Could not connect to host for config application.")
@events.test_stop.add_listener
def _(ch, **kw):
print("\n--- Test stopping ---")
# Clean up the global configuration setting (simulated)
if shared_state["config_applied"]:
try:
response = requests.delete(f"{self.host}/admin/config")
if response.status_code == 200:
print("Global configuration cleaned up successfully.")
shared_state["config_applied"] = False
else:
print(f"Failed to clean up global configuration: {response.status_code}")
except requests.exceptions.ConnectionError:
print("Could not connect to host for config cleanup.")
else:
print("No global configuration was applied, skipping cleanup.")
@events.user_start.add_listener
def on_user_start(user, **kw):
shared_state["user_count"] += 1
print(f"User {user.id} started. Total users: {shared_state['user_count']}")
@events.user_stop.add_listener
def on_user_stop(user, **kw):
shared_state["user_count"] -= 1
print(f"User {user.id} stopped. Total users: {shared_state['user_count']}")
The test_start and test_stop events fire once per Locust worker process. This is crucial: if you’re running Locust in distributed mode, these events will execute on each worker. The shared_state dictionary, being a global variable, is accessible and modifiable by all these event handlers. The user_start and user_stop events, conversely, fire for each user instance as it’s spawned or stopped, allowing you to track individual user lifecycles and aggregate information.
The core problem on_start and on_stop solve is the need for persistent, session-wide state that survives the creation and destruction of individual user classes. Without them, any setup or teardown logic would have to be duplicated within each user’s __init__ and __del__ (or similar), which is impractical and error-prone for managing global resources or configurations. These events provide a single point of control.
Internally, Locust’s event system is a publish-subscribe mechanism. When test_start occurs, Locust publishes an event. Any function that has registered itself as a listener for test_start (using @events.test_start.add_listener) will then be called by the Locust master or worker. The same applies to test_stop, user_start, and user_stop. The arguments passed to these listeners (ch for "context handler" and **kw for keyword arguments) provide access to the current test state and other relevant information, though they are often not needed for simple state management.
The shared_state dictionary is your primary tool. You can store anything in it: connection pools, configuration objects, counters, lists of IDs, or even results from initial setup calls that subsequent user tasks might need. Accessing self.host within the event listeners is a bit of a shortcut; in a more robust setup, you might pass the host as an argument or access it via the environment object if it were available in that scope.
A common pitfall is assuming test_start and test_stop run only once globally. Remember they run per worker. If you need truly global state across all workers (e.g., updating a central database that all workers should see), you’d typically implement that logic in the test_start handler on the Locust master process. However, for resource cleanup, running it on each worker is often the desired behavior.
The real power comes when you combine these events with other Locust features. For instance, you might use test_start to pre-populate a cache or provision resources, and then use task logic within your HttpUser to consume those resources. Conversely, test_stop could be used to trigger reporting or send final metrics to an external system.
When you run the example above, you’ll see the "Test starting" and "Global configuration applied successfully" messages appear, followed by individual user start messages. When the test finishes, you’ll see the "Test stopping" and "Global configuration cleaned up successfully" messages, along with user stop messages.
The next logical step after mastering state management is understanding how to use the data collected in shared_state to dynamically adjust test behavior, perhaps by modifying user spawn rates or task weights based on the state of the system under test.