State Management Guide
When to Use State Management
State management is essential for:
- Tracking plugin progress and counters
- Caching expensive computations or API responses
- Sharing data between plugin executions
- Coordinating between multiple plugins (global_state)
- Implementing rate limiting and throttling
- Managing distributed locks and flags
Key Difference: state
is private to each plugin, while global_state
is shared across all plugins.
Quick Start
Basic Operations
// MVEL - Store and retrieve plugin state
state.put("last_processed_item", itemId);
state.put("request_count", 1000);
lastItem = state.get("last_processed_item");
if (state.containsKey("request_count")) {
count = state.get("request_count");
}
Atomic Counter
// JavaScript - Thread-safe counter
const requestCount = state.increment("api_requests");
if (requestCount > 1000) {
console.log("Milestone: 1000 API requests processed!");
}
Core Concepts
state vs global_state
Feature | state | global_state |
---|---|---|
Scope | Plugin-specific | Shared across all plugins |
Isolation | Complete - other plugins can't access | None - all plugins can read/write |
Use Cases | Plugin config, progress tracking | Cross-plugin coordination, shared cache |
Namespace | Automatic (plugin name) | Global namespace |
Operation Categories
- Basic Operations - Simple key-value storage
- Atomic Counters - Thread-safe numeric operations
- Set Operations - Manage collections atomically
- Compute Operations - Update values based on current state
- Compare-and-Set - Conditional updates for coordination
API Reference
Basic Operations
// Store values
state.put("key", "value");
state.put("counter", 42);
state.put("data", complexObject);
// Retrieve values
value = state.get("key");
counter = state.get("counter", 0); // With default
// Check existence
if (state.containsKey("key")) {
// Key exists
}
// Remove values
state.remove("key");
// Clear all state (use with caution!)
state.clear();
Atomic Counter Operations
These operations are thread-safe and perfect for concurrent environments:
// Increment operations
count = state.increment("counter"); // +1, returns new value
count = state.increment("counter", 5); // +5, returns new value
// Decrement operations
count = state.decrement("counter"); // -1, returns new value
count = state.decrement("counter", 3); // -3, returns new value
// Add and get variations
newVal = state.addAndGet("counter", 10); // Add 10, return new value
oldVal = state.getAndAdd("counter", 10); // Return old value, then add 10
Set Operations
Manage collections of unique elements atomically:
// Add elements to set
added = state.addToSet("processed_items", itemId); // Returns true if new
// Remove elements
removed = state.removeFromSet("processed_items", itemId);
// Check membership
exists = state.containsInSet("processed_items", itemId);
// Get entire set (immutable view)
allItems = state.getSet("processed_items");
// Get set size
size = state.setSize("processed_items");
Advanced Compute Operations
Update values based on their current state:
// Compute - update based on current value
state.compute("max_amount", (key, current) -> {
return Math.max(current ?: 0, newAmount);
});
// Merge - combine new value with existing
state.merge("total_volume", amount, (existing, new) -> {
return (existing ?: 0) + new;
});
// Compare and set - atomic conditional update
success = state.compareAndSet("status", "pending", "processing");
// Get and set - return old value, set new
oldValue = state.getAndSet("flag", true);
Common Patterns
1. Progress Tracking
// JavaScript - Track processing progress
function processBlocks(blocks) {
// Get last processed block
let lastProcessed = state.get("last_block") || 0;
blocks.forEach(block => {
if (block.number <= lastProcessed) {
return; // Skip already processed
}
// Process block
processBlock(block);
// Update progress atomically
state.put("last_block", block.number);
state.increment("blocks_processed");
});
// Log statistics
const total = state.get("blocks_processed");
console.log(`Total blocks processed: ${total}`);
}
2. Rate Limiting
// MVEL - Implement rate limiting
function checkRateLimit(clientId) {
key = "rate_limit:" + clientId;
windowKey = "rate_window:" + clientId;
currentWindow = System.currentTimeMillis() / 60000; // 1-minute windows
lastWindow = state.get(windowKey, 0);
if (currentWindow != lastWindow) {
// New time window, reset counter
state.put(windowKey, currentWindow);
state.put(key, 1);
return true;
}
// Same window, check limit
requests = state.increment(key);
return requests <= 100; // 100 requests per minute
}
3. Deduplication
// JavaScript - Prevent duplicate processing
function processTransaction(tx) {
const txHash = tx.getTxHash();
// Atomic check-and-add
if (!state.addToSet("processed_transactions", txHash)) {
console.log(`Transaction ${txHash} already processed, skipping`);
return false;
}
try {
// Process the transaction
const result = doProcessing(tx);
// Track successful processing
state.increment("successful_txs");
return true;
} catch (error) {
// Remove from set on failure to allow retry
state.removeFromSet("processed_transactions", txHash);
state.increment("failed_txs");
throw error;
}
}
4. Distributed Locking
// MVEL - Simple distributed lock using compare-and-set
function acquireLock(lockName, timeout) {
lockKey = "lock:" + lockName;
lockHolder = Thread.currentThread().getName() + ":" + System.currentTimeMillis();
// Try to acquire lock
if (global_state.compareAndSet(lockKey, null, lockHolder)) {
return lockHolder; // Lock acquired
}
// Check if existing lock is expired
existing = global_state.get(lockKey);
if (existing != null) {
parts = existing.split(":");
lockTime = Long.parseLong(parts[1]);
if (System.currentTimeMillis() - lockTime > timeout) {
// Lock expired, try to take over
if (global_state.compareAndSet(lockKey, existing, lockHolder)) {
return lockHolder;
}
}
}
return null; // Failed to acquire
}
function releaseLock(lockName, lockHolder) {
lockKey = "lock:" + lockName;
return global_state.compareAndSet(lockKey, lockHolder, null);
}
5. Caching with Expiry
# Python - Cache with TTL
def get_cached_data(key, fetch_function, ttl_seconds=300):
cache_key = f"cache:{key}"
timestamp_key = f"cache_time:{key}"
# Check cache validity
cached_time = state.get(timestamp_key, 0)
current_time = System.currentTimeMillis() / 1000
if current_time - cached_time < ttl_seconds:
# Cache is valid
cached_data = state.get(cache_key)
if cached_data:
return cached_data
# Fetch fresh data
fresh_data = fetch_function()
# Update cache atomically
state.put(cache_key, fresh_data)
state.put(timestamp_key, current_time)
return fresh_data
6. Metrics Aggregation
// JavaScript - Aggregate metrics across events
function updateMetrics(event) {
const amount = event.getAmount();
// Update various metrics atomically
state.increment("event_count");
state.merge("total_amount", amount, (old, val) => (old || 0) + val);
// Track maximum
state.compute("max_amount", (key, current) => {
return Math.max(current || 0, amount);
});
// Track unique addresses
state.addToSet("unique_addresses", event.getAddress());
// Update hourly buckets
const hour = Math.floor(Date.now() / 3600000);
state.increment(`hourly_count:${hour}`);
}
function getMetricsSummary() {
return {
total_events: state.get("event_count", 0),
total_amount: state.get("total_amount", 0),
max_amount: state.get("max_amount", 0),
unique_addresses: state.setSize("unique_addresses")
};
}
Best Practices
1. Use Atomic Operations for Counters
⚠️
Always use atomic operations (increment
, decrement
) for counters instead of get/put to avoid race conditions.
// ❌ BAD - Race condition risk
let count = state.get("counter") || 0;
state.put("counter", count + 1);
// ✅ GOOD - Atomic operation
const count = state.increment("counter");
2. Namespace Your Keys
// Good key naming prevents collisions
const keys = {
lastBlock: "processor:last_block",
apiCache: "cache:api:response",
rateLimit: `rate:${clientId}:${window}`
};
3. Handle Missing Keys Gracefully
// Always provide defaults or check existence
const config = state.get("config") || getDefaultConfig();
const retries = state.get("retry_count", 0);
if (state.containsKey("important_flag")) {
// Process only if flag exists
}
4. Clean Up Stale Data
// Periodically clean up old data
function cleanupOldData() {
const cutoff = Date.now() - (24 * 60 * 60 * 1000); // 24 hours ago
// Remove old cache entries
const cacheKeys = state.getKeys().filter(k => k.startsWith("cache:"));
cacheKeys.forEach(key => {
const timestamp = state.get(key + ":time");
if (timestamp && timestamp < cutoff) {
state.remove(key);
state.remove(key + ":time");
}
});
}
Performance Considerations
- Atomic operations are fast and lock-free
- Set operations maintain uniqueness automatically
- Large objects in state can impact memory - consider storing IDs and fetching from database