The enhanced hot-reload system provides zero-downtime plugin updates with automatic state preservation, dependency management, atomic operations, and comprehensive rollback capabilities.
Plugins can implement the IStatefulPlugin interface to preserve their internal state during hot-reload:
class IStatefulPlugin {
public:
virtual ~IStatefulPlugin() = default;
/**
* @brief Save plugin state before reload
* @return Serialized state as JSON string
*/
virtual std::string saveState() = 0;
/**
* @brief Restore plugin state after reload
* @param state Previously saved state
* @return true if restored successfully
*/
virtual bool restoreState(const std::string& state) = 0;
};class MyStatefulPlugin : public IThemisPlugin, public IStatefulPlugin {
private:
int request_count = 0;
std::map<std::string, std::string> cache;
public:
// IStatefulPlugin implementation
std::string saveState() override {
nlohmann::json state;
state["request_count"] = request_count;
state["cache"] = cache;
return state.dump();
}
bool restoreState(const std::string& state_json) override {
try {
auto state = nlohmann::json::parse(state_json);
request_count = state["request_count"];
cache = state["cache"].get<std::map<std::string, std::string>>();
return true;
} catch (const std::exception& e) {
THEMIS_ERROR("Failed to restore state: {}", e.what());
return false;
}
}
// ... other IThemisPlugin methods
};Register listeners to be notified during different phases of plugin reload:
enum class PluginReloadPhase {
BEFORE_UNLOAD, // About to unload old plugin
AFTER_UNLOAD, // Old plugin unloaded
AFTER_LOAD // New plugin loaded and initialized
};
using PluginReloadListener = std::function<void(
const std::string& plugin_name,
PluginReloadPhase phase
)>;auto& pm = PluginManager::instance();
// Register a reload listener
pm.registerReloadListener([](const std::string& name, PluginReloadPhase phase) {
switch (phase) {
case PluginReloadPhase::BEFORE_UNLOAD:
THEMIS_INFO("Plugin {} is about to be reloaded", name);
// Pause incoming requests to this plugin
break;
case PluginReloadPhase::AFTER_UNLOAD:
THEMIS_INFO("Plugin {} has been unloaded", name);
break;
case PluginReloadPhase::AFTER_LOAD:
THEMIS_INFO("Plugin {} has been reloaded", name);
// Resume incoming requests
break;
}
});
// Trigger reload
pm.reloadPlugin("my_plugin");The system automatically checks for dependent plugins before allowing reload:
// If plugin B depends on plugin A, attempting to reload A will fail:
auto& pm = PluginManager::instance();
bool success = pm.reloadPlugin("plugin_a");
if (!success) {
// Check logs: "Cannot reload plugin plugin_a - 1 plugins depend on it: plugin_b"
}Workaround: Unload dependent plugins first, or reload them together.
The reload process is atomic - if any step fails, the system automatically rolls back to the old plugin version:
- Check Dependencies - Fail if other plugins depend on this one
- Save State - Call
saveState()if plugin is stateful - Notify BEFORE_UNLOAD - Alert registered listeners
- Unload Old Plugin - Shutdown and unload old binary
- Notify AFTER_UNLOAD - Alert registered listeners
- Verify New Binary - Check signatures and security
- Load New Binary - Load plugin library
- Create Instance - Call
createPlugin() - Initialize - Call
initialize()with restored state - Restore State - Call
restoreState()if available - Notify AFTER_LOAD - Alert registered listeners
If any step 6-10 fails, the system rolls back to the old plugin instance.
// Scenario: New plugin binary is corrupted
bool success = pm.reloadPlugin("my_plugin");
// Logs show:
// ERROR: Plugin verification failed after reload for my_plugin: Invalid signature
// WARN: Attempting to rollback plugin my_plugin to previous version
// Result: Old plugin is still loaded and functional
EXPECT_FALSE(success);
EXPECT_NE(pm.getPlugin("my_plugin"), nullptr); // Old instance still worksWhen a plugin is reloaded, the saved state is passed in the initialization config:
bool MyPlugin::initialize(const char* config_json) {
auto config = nlohmann::json::parse(config_json);
// Check for restored state
if (config.contains("restored_state")) {
std::string state = config["restored_state"];
restoreState(state);
}
// Apply new configuration
if (config.contains("new_setting")) {
apply_new_setting(config["new_setting"]);
}
return true;
}/**
* @brief Register a reload event listener
*/
void registerReloadListener(PluginReloadListener listener);
/**
* @brief Clear all reload event listeners
*/
void clearReloadListeners();
/**
* @brief Reload a plugin (enhanced version)
* @param name Plugin name
* @return true if reload successful, false otherwise
*
* Features:
* - Checks for dependent plugins
* - Saves state (if IStatefulPlugin)
* - Atomic operation with rollback
* - Event notifications
* - Thread-safe
*/
bool reloadPlugin(const std::string& name);auto& pm = PluginManager::instance();
// Load plugin
pm.loadPlugin("my_plugin");
// ... plugin is in use ...
// Hot-reload (e.g., after deploying new binary)
if (pm.reloadPlugin("my_plugin")) {
THEMIS_INFO("Plugin reloaded successfully");
} else {
THEMIS_ERROR("Plugin reload failed - old version still running");
}class DatabaseConnectionPlugin : public IThemisPlugin, public IStatefulPlugin {
private:
std::map<std::string, Connection> active_connections;
public:
std::string saveState() override {
nlohmann::json state;
for (const auto& [id, conn] : active_connections) {
state["connections"][id] = {
{"host", conn.host},
{"port", conn.port},
{"database", conn.database}
};
}
return state.dump();
}
bool restoreState(const std::string& state_json) override {
try {
auto state = nlohmann::json::parse(state_json);
for (const auto& [id, conn_data] : state["connections"].items()) {
// Reconnect to database
Connection conn;
conn.host = conn_data["host"];
conn.port = conn_data["port"];
conn.database = conn_data["database"];
conn.reconnect();
active_connections[id] = std::move(conn);
}
return true;
} catch (...) {
return false;
}
}
};
// Usage:
auto& pm = PluginManager::instance();
pm.reloadPlugin("database_connection");
// All active connections are automatically restored!class RequestRouter {
private:
std::atomic<bool> paused_{false};
public:
void setup() {
auto& pm = PluginManager::instance();
pm.registerReloadListener([this](const std::string& name, PluginReloadPhase phase) {
if (name == "critical_plugin") {
switch (phase) {
case PluginReloadPhase::BEFORE_UNLOAD:
// Pause routing to this plugin
paused_ = true;
THEMIS_INFO("Paused requests to {}", name);
break;
case PluginReloadPhase::AFTER_LOAD:
// Resume routing
paused_ = false;
THEMIS_INFO("Resumed requests to {}", name);
break;
}
}
});
}
void routeRequest(const Request& req) {
if (paused_) {
// Queue or reject request
return;
}
// Process normally
}
};While state preservation is available, design plugins to be as stateless as possible:
// Good: State can be reconstructed from external source
class CachePlugin : public IThemisPlugin, public IStatefulPlugin {
std::string saveState() override {
// Save cache keys only, not values
nlohmann::json state;
for (const auto& [key, _] : cache) {
state["keys"].push_back(key);
}
return state.dump();
}
bool restoreState(const std::string& state_json) override {
auto state = nlohmann::json::parse(state_json);
for (const std::string& key : state["keys"]) {
// Reload value from database
cache[key] = database.get(key);
}
return true;
}
};bool MyPlugin::initialize(const char* config_json) {
auto config = nlohmann::json::parse(config_json);
if (config.contains("restored_state")) {
if (!restoreState(config["restored_state"])) {
THEMIS_WARN("Failed to restore state, starting fresh");
// Continue with default state
}
}
return true;
}// Notify external systems about reload
pm.registerReloadListener([](const std::string& name, PluginReloadPhase phase) {
if (phase == PluginReloadPhase::AFTER_LOAD) {
// Update service discovery
service_registry.update_plugin_version(name);
// Warm up caches
plugin_cache.invalidate(name);
}
});TEST(PluginTest, ReloadPreservesState) {
auto plugin = std::make_unique<MyPlugin>();
plugin->initialize("{}");
// Set state
plugin->setCounter(42);
// Save state
std::string state = plugin->saveState();
// Simulate reload
plugin.reset();
plugin = std::make_unique<MyPlugin>();
plugin->initialize("{}");
plugin->restoreState(state);
// Verify state preserved
EXPECT_EQ(plugin->getCounter(), 42);
}- Binary Compatibility - New plugin version must be binary compatible with saved state format
- Dependency Blocking - Cannot reload if other plugins depend on it (must unload dependents first)
- No Parallel Reload - Only one plugin can be reloaded at a time (thread-safe serialization)
- State Size - Very large state may cause brief pause during serialization
Solution: Unload dependent plugins first:
// Get dependent plugins (hypothetical helper)
auto dependents = getDependents("plugin_a");
for (const auto& dep : dependents) {
pm.unloadPlugin(dep);
}
// Now reload works
pm.reloadPlugin("plugin_a");
// Reload dependents
for (const auto& dep : dependents) {
pm.loadPlugin(dep);
}Checklist:
- Plugin implements
IStatefulPlugin saveState()returns valid JSONrestoreState()is called ininitialize()- State format is compatible between versions
Solutions:
- Reduce state size (save only essential data)
- Move long-running cleanup to background thread
- Use differential state updates