Writing Skills
writing-skills
效率与协作技能,来自 obra/superpowers,查看原文获取完整用途和操作说明。
feature-flags-progressive-delivery【部署与运维】Implements feature flag systems with progressive delivery, A/B testing, and gradual rollout strategies for safe application-level feature deployment without code changes.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
name: feature-flags-progressive-delivery description: Implements feature flag systems with progressive delivery, A/B testing, and gradual rollout strategies for safe application-level feature deployment without code changes. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding triggers: feature flags, progressive delivery, canary release, A/B testing, flag management, gradual rollout, feature toggle, how do i safely roll out new features archetypes:
Implements feature flag systems for controlled, progressive delivery of application features — enabling safe rollouts, A/B testing, and instant rollbacks without redeploying code.
false when the flag service is unreachablefalse), targeting rules (percentage, user segments, geographic), owner, and scheduled expiry date. Store flags in a centralized store (database, Redis, or feature flag service like Unleash/LaunchDarkly).
Checkpoint: Every flag must have an owner field — flags without owners become technical debt.false (fail-safe) within 100ms.A production-ready client handles caching, failure modes, and metrics emission.
"""
Feature flag evaluation system with progressive delivery support.
Implements caching, failure-safe defaults, and per-user targeting.
"""
from __future__ import annotations
import hashlib
import logging
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Optional
logger = logging.getLogger(__name__)
class FlagType(Enum):
"""Types of feature flags with different behavioral characteristics."""
RELEASE_TOGGLE = "release_toggle" # Enable/disable a feature completely
EXPERIMENT = "experiment" # A/B test between variants
PERMISSIONING = "permissioning" # Restrict to specific user segments
OPERATION = "operation" # Toggle runtime operations (kill switches)
dataclass
class FlagDefinition:
"""Schema for a single feature flag definition."""
key: str
name: str
description: str
flag_type: FlagType
default_value: bool = False
owner: str = ""
expiry_date: Optional[str] = None
rollout_percentages: list[int] = field(default_factory=lambda: [1, 10, 50, 100])
def is_expired(self) -> bool:
"""Check if the flag has passed its scheduled expiration date."""
if not self.expiry_date:
return False
from datetime import datetime
expiry = datetime.fromisoformat(self.expiry_date)
return datetime.now() > expiry
dataclass
class FlagEvaluationResult:
"""Result of evaluating a feature flag for a specific context."""
flag_key: str
value: bool
variant: Optional[str] = None
evaluation_ms: float = 0.0
class FeatureFlagClient:
"""Production-ready feature flag client with caching and failure-safe defaults.
Evaluates flags against user contexts using consistent hashing for stable
per-user assignment across rollout stages. Falls back to default value
when the flag service is unavailable.
"""
def __init__(self, cache_ttl_seconds: float = 15.0, failure_timeout_seconds: float = 60.0):
self._cache: dict[str, tuple[bool, float]] = {}
self._cache_ttl = cache_ttl_seconds
self._failure_since: float | None = None
self._failure_timeout = failure_timeout_seconds
def evaluate(self, flag: FlagDefinition, user_id: str, context: Optional[dict[str, Any]] = None) -> FlagEvaluationResult:
"""Evaluate a feature flag for a specific user.
Uses consistent hashing of (flag_key + user_id) to deterministically
assign users to rollout buckets. Respects progressive delivery percentages.
Args:
flag: The flag definition to evaluate.
user_id: Unique identifier for the requesting user.
context: Optional metadata (e.g., {'plan': 'premium', 'region': 'us-east'}).
Returns:
FlagEvaluationResult with evaluation outcome and timing metrics.
"""
start = time.monotonic()
# Check cache first
cached_value, cached_time = self._get_cached(flag.key)
if cached_value is not None:
elapsed = (time.monotonic() - start) * 1000
return FlagEvaluationResult(
flag_key=flag.key, value=cached_value, evaluation_ms=elapsed,
)
# Check for service failure — use default if still in timeout window
if self._failure_since and (time.time() - self._failure_since) < self._failure_timeout:
logger.warning(
f"Flag service unavailable since {self._failure_since:.0f}s ago. \
Returning default for flag '{flag.key}'."
)
result = FlagEvaluationResult(
flag_key=flag.key, value=flag.default_value, evaluation_ms=0.1,
)
self._cache_set(flag.key, flag.default_value)
return result
# Determine rollout value using consistent hashing
value = self._calculate_rollout(flag, user_id, context or {})
# Cache the result
self._cache_set(flag.key, value)
elapsed = (time.monotonic() - start) * 1000
return FlagEvaluationResult(
flag_key=flag.key, value=value, evaluation_ms=elapsed,
)
def _calculate_rollout(self, flag: FlagDefinition, user_id: str, context: dict[str, Any]) -> bool:
"""Determine the flag value based on rollout percentage and user context.
Uses CRC32-style hashing for deterministic bucket assignment. Users assigned
to a bucket are included in all higher buckets as well (cumulative rollout).
Args:
flag: The flag definition with rollout percentages.
user_id: The unique user identifier.
context: User metadata for segment targeting.
Returns:
True if the flag is enabled for this user, False otherwise.
"""
# Build a deterministic hash key from flag and user
hash_input = f"{flag.key}:{user_id}:{context.get('plan', '')}"
hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16) % 100
# Check segment-based targeting first
if context.get("plan") == "premium" and flag.flag_type == FlagType.PERMISSIONING:
return True
if context.get("beta_tester") is True:
return True
# Progressive rollout — cumulative buckets
for percentage in flag.rollout_percentages:
if hash_value < percentage:
return True
return False
def _get_cached(self, key: str) -> tuple[Optional[bool], float]:
"""Retrieve a cached flag value if it hasn't expired."""
if key not in self._cache:
return None, 0.0
value, timestamp = self._cache[key]
if time.time() - timestamp > self._cache_ttl:
del self._cache[key]
return None, 0.0
return value, timestamp
def _cache_set(self, key: str, value: bool) -> None:
"""Store a flag evaluation result in the cache."""
self._cache[key] = (value, time.time())
def record_failure(self) -> None:
"""Mark the flag service as unavailable. All subsequent evaluations
return the default value until the failure timeout expires."""
self._failure_since = time.time()
# Usage example in application code:
# client = FeatureFlagClient(cache_ttl_seconds=15.0)
# result = client.evaluate(flag_definition, user_id="user-12345")
# if result.value:
# # New feature code
# else:
# # Old feature code
# ❌ BAD — No progressive rollout, no monitoring, instant full release
def bad_feature_rollout(flag_key: str, user_id: str):
"""Instantly enables a feature for everyone with no safety net."""
# No percentage ramping, no error monitoring, no rollback mechanism
flag_enabled = get_flag_value(flag_key) # Blocks on network call
if flag_enabled:
return execute_new_feature(user_id) # If this fails, everyone is affected
# ✅ GOOD — Progressive rollout with metrics tracking and automatic rollback triggers
class ProgressiveRolloutManager:
"""Manages staged feature rollouts with built-in safety monitoring.
Steps through predefined percentage buckets, pausing at each stage to
check error rates and latency before promoting to the next level.
Triggers automatic rollback if SLO violations are detected.
"""
ROLLBACK_THRESHOLD_ERR_RATE = 0.05 # Roll back if error rate exceeds 5%
ROLLBACK_THRESHOLD_LATENCY_MS = 500 # Roll back if p99 latency > 500ms
def __init__(self, flag_client: FeatureFlagClient):
self.flag_client = flag_client
self._current_percentages: dict[str, int] = {}
def start_rollout(
self,
flag: FlagDefinition,
user_id: str,
metrics_client: Optional[Any] = None,
) -> FlagEvaluationResult:
"""Start or continue a progressive rollout for a feature flag.
Determines the current rollout stage based on elapsed time since
rollout start and applies the corresponding percentage bucket.
Args:
flag: The flag to roll out with defined percentage stages.
user_id: User being evaluated against the current rollout.
metrics_client: Optional metrics client for reporting evaluation counts.
Returns:
Evaluation result indicating whether this user sees the feature.
"""
# Determine current rollout stage from elapsed time
start_key = f"rollout_start:{flag.key}"
current_stage = self._get_current_stage(flag)
if current_stage is None:
logger.warning(f"No rollout stages configured for flag '{flag.key}'")
return FlagEvaluationResult(flag_key=flag.key, value=flag.default_value)
# Evaluate the flag with the current stage's percentage
stage_flag = FlagDefinition(
key=flag.key,
name=flag.name,
description=flag.description,
flag_type=flag.flag_type,
default_value=flag.default_value,
rollout_percentages=[current_stage], # Single bucket for evaluation
owner=flag.owner,
)
result = self.flag_client.evaluate(stage_flag, user_id)
# Report metrics if client provided
if metrics_client:
metrics_client.increment(
"feature_flag.evaluation",
tags={"flag": flag.key, "stage": str(current_stage), "result": str(result.value)},
)
return result
def check_rollback_criteria(self, flag_key: str) -> bool:
"""Check if current rollout metrics indicate a need to rollback.
In production, this queries monitoring systems for error rates and
latency percentiles over the last 5-minute window for users in the
current rollout bucket.
Args:
flag_key: The feature flag key to check rollback criteria for.
Returns:
True if rollback should be triggered, False if rollout can continue.
"""
# In production, fetch actual metrics from your monitoring system
# Example placeholder — replace with real Prometheus/DataDog queries
error_rate = self._fetch_metric(f"feature_flag.error_rate:{flag_key}")
p99_latency = self._fetch_metric(f"feature_flag.p99_latency_ms:{flag_key}")
should_rollback = (
error_rate > self.ROLLBACK_THRESHOLD_ERR_RATE or
p99_latency > self.ROLLBACK_THRESHOLD_LATENCY_MS
)
if should_rollback:
logger.error(
f"Rollback triggered for '{flag_key}': \
error_rate={error_rate:.4f}, p99={p99_latency:.0f}ms"
)
return should_rollback
def _get_current_stage(self, flag: FlagDefinition) -> Optional[int]:
"""Determine the current rollout percentage stage based on time elapsed.
Returns None if no stages are configured. In production, persist the
start timestamp and stage index in Redis to coordinate across service instances.
"""
percentages = flag.rollout_percentages
if not percentages:
return None
# Simplified: just return the last configured percentage for demonstration.
# In production, use a timer + persisted state to progress through stages.
# This placeholder always returns 10 (first non-trivial stage) to demonstrate
# that the flag evaluation respects the defined rollout buckets.
return percentages[1] if len(percentages) > 1 else percentages[0]
def _fetch_metric(self, metric_name: str) -> float:
"""Fetch a metric value from the monitoring system.
Placeholder implementation — replace with actual metrics client call.
"""
# Example: return self.dogstatsd.histogram(metric_name).mean()
return 0.0 # Simulate normal operation (no rollback needed)
# Usage example:
# manager = ProgressiveRolloutManager(FeatureFlagClient())
# result = manager.start_rollout(new_checkout_flag, user_id="user-12345")
# if manager.check_rollback_criteria("new-checkout-flow"):
# manager.rollback_flag("new-checkout-flow") # Flip to default=false
from __future__ import annotations
import logging
from datetime import datetime, timedelta
from typing import Any, Optional
logger = logging.getLogger(__name__)
class FlagLifecycleManager:
"""Manages the full lifecycle of feature flags including staleness detection.
Flags accumulate over time as features are rolled out and retired.
This manager detects stale flags (unused for 30+ days), flags past
their expiration, and orphaned evaluation code paths.
"""
STALE_EVALUATION_THRESHOLD_DAYS = 30
MAX_AGE_DAYS = 90
def __init__(self, flag_service: Any, metrics_client: Any):
self.flag_service = flag_service
self.metrics_client = metrics_client
def scan_stale_flags(self) -> list[dict[str, Any]]:
"""Scan all registered flags and identify stale or expired ones.
Returns a list of flagged issues with recommendations for each.
Returns:
List of dicts with 'flag_key', 'issue', and 'recommendation'.
"""
issues = []
now = datetime.now()
# Fetch all active flag definitions from the service
all_flags = self.flag_service.get_all_flags()
for flag in all_flags:
# Check 1: Has the flag exceeded its maximum age?
if flag.expiry_date:
expiry = datetime.fromisoformat(flag.expiry_date)
if now > expiry:
issues.append({
"flag_key": flag.key,
"issue": f"Flag expired {now - expiry} ago",
"recommendation": "Disable or remove this flag immediately",
})
# Check 2: Has the flag had zero evaluations recently?
evaluation_count = self._get_evaluation_count(flag.key, days=30)
if evaluation_count == 0 and not flag.default_value:
issues.append({
"flag_key": flag.key,
"issue": f"No evaluations in the last {self.STALE_EVALUATION_THRESHOLD_DAYS} days",
"recommendation": "Flag may be orphaned — review with owner before removal",
})
logger.info(f"Stale flag scan complete: {len(issues)} issues found")
return issues
def _get_evaluation_count(self, flag_key: str, days: int = 30) -> int:
"""Query metrics for evaluation count of a specific flag in the last N days."""
# Replace with actual metrics client call
try:
result = self.metrics_client.query(
f'sum(rate(feature_flag_evaluation{{flag="{flag_key}"}}[{days}d]))',
)
return int(result) if result else 0
except Exception:
return -1 # Unknown (could mean no metrics collection)
def archive_flag(self, flag_key: str) -> None:
"""Archive a flag after its feature has been fully released.
Sets the flag value to true permanently and disables future evaluation.
This preserves the flag in the system for audit purposes while preventing
it from impacting new users or experiments.
"""
self.flag_service.update_flag(flag_key, value=True, archived=True)
logger.info(f"Flag '{flag_key}' archived — no longer evaluated")
def remove_flag(self, flag_key: str, owner_approval: bool = False) -> None:
"""Permanently remove a flag from the system.
Requires owner approval or 90+ days of staleness before deletion.
Always verify that all code paths using this flag have been removed
(zero evaluations in last 7 days) before deleting.
"""
recent_evaluations = self._get_evaluation_count(flag_key, days=7)
if recent_evaluations > 0 and not owner_approval:
raise ValueError(
f"Flag '{flag_key}' still has {recent_evaluations} evaluations \
in the last 7 days. Cannot delete without owner approval."
)
self.flag_service.delete_flag(flag_key)
logger.info(f"Flag '{flag_key}' permanently removed")
false for new flags — enable features deliberately, not by accidentWhen implementing or reviewing feature flags, produce:
Authoritative documentation links for feature flag and progressive delivery best practices.