This guide explains how to create custom plugins for Cyclone Cache.
Plugins allow you to customize cache behavior in several ways:
- Key Generation: Control how cache keys are computed from requests
- Variant Selection: Choose the best cached variant for a request
- Freshness Checking: Determine if a cached entry is still valid
- Eviction Priority: Influence which entries are evicted first
#include "cyclone/plugin/plugin.hpp"
class MyPlugin : public cyclone::CachePlugin {
public:
cyclone::PluginInfo info() const override {
return {
"my-plugin", // name
"1.0.0", // version
1000 // unique plugin_id
};
}
std::optional<size_t> select_variant(
const cyclone::VariantCollection& variants,
const cyclone::LookupContext& ctx) override
{
// Required: implement variant selection
if (variants.empty()) {
return std::nullopt;
}
return 0; // Return first variant
}
};auto plugin = std::make_shared<MyPlugin>();
cache->plugin_manager().register_plugin(plugin);
// If this plugin handles variant selection:
cache->plugin_manager().set_alternate_selector(plugin);Required. Returns plugin identification.
PluginInfo info() const override {
return {"plugin-name", "1.0.0", unique_id};
}name: Human-readable name for loggingversion: Version stringplugin_id: Unique uint32_t identifier (use values > 100 for custom plugins)
Optional. Customizes cache key generation.
CacheKey generate_key(const KeyContext& ctx) override {
// Example: Include query parameters in key
std::string key_string = std::string(ctx.hostname);
key_string += ctx.url;
return CacheKey(key_string);
}KeyContext fields:
url: Request URLhostname: Request hostnamerequest_headers: Raw request headersuser_data: Custom data pointer
Required. Chooses the best cached variant for a request.
std::optional<size_t> select_variant(
const VariantCollection& variants,
const LookupContext& ctx) override
{
// Return index of best variant, or nullopt if none match
for (size_t i = 0; i < variants.size(); ++i) {
if (is_match(variants[i], ctx)) {
return i;
}
}
return std::nullopt;
}LookupContext fields:
request_headers: Raw request headersuser_data: Custom data pointer
CacheVariant fields:
key: Cache keymetadata: Plugin-specific metadatacontent_length: Size of cached contentcreated: Creation timestamplast_accessed: Last access timestamphit_count: Number of times accessed
Optional. Determines if a cached entry is still valid.
FreshnessResult check_freshness(
const CacheVariant& variant,
const FreshnessContext& ctx) override
{
if (ctx.force_revalidate) {
return FreshnessResult::MustRevalidate;
}
auto age = ctx.now - variant.created;
if (age > std::chrono::hours(24)) {
return FreshnessResult::Stale;
}
return FreshnessResult::Fresh;
}FreshnessResult values:
Fresh: Entry is valid, serve directlyStale: Entry is stale but can be served (while revalidating)MustRevalidate: Must check with origin before servingError: Error checking freshness
Optional. Returns a priority value for eviction decisions.
double eviction_priority(const CacheVariant& variant) override {
// Higher values = more likely to evict
double priority = 1.0;
// Older entries get higher priority
auto age = std::chrono::system_clock::now() - variant.last_accessed;
auto age_hours = std::chrono::duration_cast<std::chrono::hours>(age).count();
priority += age_hours * 0.1;
// Frequently accessed entries get lower priority
if (variant.hit_count > 10) {
priority *= 0.5;
}
return priority;
}Plugins can store custom metadata with cached entries.
struct MyMetadata {
uint32_t priority;
std::string content_type;
std::time_t expires;
std::vector<std::byte> serialize() const {
std::vector<std::byte> data;
// Serialize fields...
return data;
}
static MyMetadata deserialize(std::span<const std::byte> data) {
MyMetadata meta;
// Deserialize fields...
return meta;
}
};// When writing to cache
MyMetadata meta;
meta.priority = 5;
meta.content_type = "text/html";
meta.expires = std::time(nullptr) + 3600;
auto serialized = meta.serialize();
write_handle.set_header(std::span<const std::byte>(serialized));std::optional<size_t> select_variant(
const VariantCollection& variants,
const LookupContext& ctx) override
{
for (size_t i = 0; i < variants.size(); ++i) {
auto& variant = variants[i];
// Deserialize our metadata
auto meta = MyMetadata::deserialize(variant.metadata.data());
if (meta.content_type == get_accept_type(ctx)) {
return i;
}
}
return std::nullopt;
}class ExpiryPlugin : public CachePlugin {
public:
static constexpr uint32_t kPluginId = 1001;
PluginInfo info() const override {
return {"expiry-plugin", "1.0.0", kPluginId};
}
std::optional<size_t> select_variant(
const VariantCollection& variants,
const LookupContext& ctx) override
{
(void)ctx;
if (variants.empty()) {
return std::nullopt;
}
// Select first non-expired variant
auto now = std::chrono::system_clock::now();
for (size_t i = 0; i < variants.size(); ++i) {
if (!is_expired(variants[i], now)) {
return i;
}
}
return std::nullopt;
}
FreshnessResult check_freshness(
const CacheVariant& variant,
const FreshnessContext& ctx) override
{
if (ctx.force_revalidate) {
return FreshnessResult::MustRevalidate;
}
if (is_expired(variant, ctx.now)) {
return FreshnessResult::Stale;
}
return FreshnessResult::Fresh;
}
private:
bool is_expired(const CacheVariant& variant,
std::chrono::system_clock::time_point now) const
{
auto age = now - variant.created;
return age > _default_ttl;
}
std::chrono::seconds _default_ttl{3600}; // 1 hour
};class ContentTypePlugin : public CachePlugin {
public:
static constexpr uint32_t kPluginId = 1002;
PluginInfo info() const override {
return {"content-type-plugin", "1.0.0", kPluginId};
}
std::optional<size_t> select_variant(
const VariantCollection& variants,
const LookupContext& ctx) override
{
auto accept = parse_accept_header(ctx.request_headers);
size_t best_index = 0;
float best_quality = 0.0f;
for (size_t i = 0; i < variants.size(); ++i) {
auto content_type = get_content_type(variants[i]);
float quality = calculate_quality(accept, content_type);
if (quality > best_quality) {
best_quality = quality;
best_index = i;
}
}
if (best_quality > 0.0f) {
return best_index;
}
return std::nullopt;
}
private:
std::string_view parse_accept_header(std::span<const std::byte> headers) {
// Parse Accept header from request...
return {};
}
std::string_view get_content_type(const CacheVariant& variant) {
// Extract Content-Type from variant metadata...
return {};
}
float calculate_quality(std::string_view accept,
std::string_view content_type) {
// RFC 7231 quality value calculation...
return 1.0f;
}
};Reserve plugin IDs to avoid conflicts:
- 0: Core cache (reserved)
- 1: HTTP plugin (built-in)
- 2-99: Reserved for future built-in plugins
- 100+: Custom plugins
Always check for empty variant collections:
if (variants.empty()) {
return std::nullopt;
}Use compact binary formats for metadata:
// Good: Fixed-size binary format
struct CompactMeta {
uint32_t flags;
int64_t timestamp;
};
// Avoid: String-based formats for frequently accessed dataPlugin methods may be called from multiple threads. Ensure thread-safe access to shared state:
class ThreadSafePlugin : public CachePlugin {
mutable std::shared_mutex _mutex;
std::map<std::string, int> _stats;
void record_hit(const std::string& key) {
std::unique_lock lock(_mutex);
_stats[key]++;
}
};Handle errors without throwing exceptions:
std::optional<size_t> select_variant(...) override {
try {
// Plugin logic...
} catch (...) {
// Log error, return safe default
return variants.empty() ? std::nullopt : std::optional{0};
}
}#include <catch2/catch_test_macros.hpp>
TEST_CASE("MyPlugin selects correct variant") {
MyPlugin plugin;
VariantCollection variants;
// Add test variants...
LookupContext ctx;
// Set up context...
auto result = plugin.select_variant(variants, ctx);
REQUIRE(result.has_value());
REQUIRE(*result == expected_index);
}TEST_CASE("Plugin integration with cache") {
CacheConfig config;
auto cache = Cache::create(config).value();
auto plugin = std::make_shared<MyPlugin>();
cache->plugin_manager().set_alternate_selector(plugin);
// Write and read with plugin active...
}