Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
46
src/Framework/SlidingWindow/Aggregator/BooleanAggregator.php
Normal file
46
src/Framework/SlidingWindow/Aggregator/BooleanAggregator.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow\Aggregator;
|
||||
|
||||
use App\Framework\SlidingWindow\WindowEntry;
|
||||
|
||||
/**
|
||||
* Aggregator for boolean values (success/failure) - useful for CircuitBreaker
|
||||
*
|
||||
* @implements SlidingWindowAggregator<BooleanResult>
|
||||
*/
|
||||
final readonly class BooleanAggregator implements SlidingWindowAggregator
|
||||
{
|
||||
public function aggregate(WindowEntry ...$entries): BooleanResult
|
||||
{
|
||||
$successes = 0;
|
||||
$failures = 0;
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry->value === true) {
|
||||
$successes++;
|
||||
} elseif ($entry->value === false) {
|
||||
$failures++;
|
||||
}
|
||||
}
|
||||
|
||||
$total = $successes + $failures;
|
||||
$failureRate = $total > 0 ? $failures / $total : 0.0;
|
||||
$successRate = $total > 0 ? $successes / $total : 0.0;
|
||||
|
||||
return new BooleanResult(
|
||||
total: $total,
|
||||
successes: $successes,
|
||||
failures: $failures,
|
||||
successRate: $successRate,
|
||||
failureRate: $failureRate
|
||||
);
|
||||
}
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return 'boolean';
|
||||
}
|
||||
}
|
||||
51
src/Framework/SlidingWindow/Aggregator/BooleanResult.php
Normal file
51
src/Framework/SlidingWindow/Aggregator/BooleanResult.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow\Aggregator;
|
||||
|
||||
/**
|
||||
* Result of boolean aggregation
|
||||
*/
|
||||
final readonly class BooleanResult
|
||||
{
|
||||
public function __construct(
|
||||
public int $total,
|
||||
public int $successes,
|
||||
public int $failures,
|
||||
public float $successRate,
|
||||
public float $failureRate,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->total === 0;
|
||||
}
|
||||
|
||||
public function hasMinimumRequests(int $minimum): bool
|
||||
{
|
||||
return $this->total >= $minimum;
|
||||
}
|
||||
|
||||
public function isFailureRateExceeded(float $threshold): bool
|
||||
{
|
||||
return $this->failureRate > $threshold;
|
||||
}
|
||||
|
||||
public function isSuccessRateBelow(float $threshold): bool
|
||||
{
|
||||
return $this->successRate < $threshold;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'total' => $this->total,
|
||||
'successes' => $this->successes,
|
||||
'failures' => $this->failures,
|
||||
'success_rate' => $this->successRate,
|
||||
'failure_rate' => $this->failureRate,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow\Aggregator;
|
||||
|
||||
use App\Framework\SlidingWindow\WindowEntry;
|
||||
|
||||
/**
|
||||
* Aggregator for counting entries - useful for RateLimiter
|
||||
*
|
||||
* @implements SlidingWindowAggregator<CountingResult>
|
||||
*/
|
||||
final readonly class CountingAggregator implements SlidingWindowAggregator
|
||||
{
|
||||
public function aggregate(WindowEntry ...$entries): CountingResult
|
||||
{
|
||||
return new CountingResult(
|
||||
count: count($entries),
|
||||
values: array_map(fn (WindowEntry $entry) => $entry->value, $entries)
|
||||
);
|
||||
}
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return 'counting';
|
||||
}
|
||||
}
|
||||
35
src/Framework/SlidingWindow/Aggregator/CountingResult.php
Normal file
35
src/Framework/SlidingWindow/Aggregator/CountingResult.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow\Aggregator;
|
||||
|
||||
/**
|
||||
* Result of counting aggregation
|
||||
*/
|
||||
final readonly class CountingResult
|
||||
{
|
||||
public function __construct(
|
||||
public int $count,
|
||||
public array $values = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->count === 0;
|
||||
}
|
||||
|
||||
public function hasMinimum(int $minimum): bool
|
||||
{
|
||||
return $this->count >= $minimum;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'count' => $this->count,
|
||||
'values' => $this->values,
|
||||
];
|
||||
}
|
||||
}
|
||||
65
src/Framework/SlidingWindow/Aggregator/NumericAggregator.php
Normal file
65
src/Framework/SlidingWindow/Aggregator/NumericAggregator.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow\Aggregator;
|
||||
|
||||
use App\Framework\SlidingWindow\WindowEntry;
|
||||
|
||||
/**
|
||||
* Aggregator for numeric values - useful for Performance Metrics
|
||||
*
|
||||
* @implements SlidingWindowAggregator<NumericResult>
|
||||
*/
|
||||
final readonly class NumericAggregator implements SlidingWindowAggregator
|
||||
{
|
||||
public function aggregate(WindowEntry ...$entries): NumericResult
|
||||
{
|
||||
if (empty($entries)) {
|
||||
return new NumericResult(
|
||||
count: 0,
|
||||
sum: 0,
|
||||
average: 0,
|
||||
min: null,
|
||||
max: null,
|
||||
median: null,
|
||||
percentile95: null
|
||||
);
|
||||
}
|
||||
|
||||
$values = array_map(fn (WindowEntry $entry) => (float) $entry->value, $entries);
|
||||
sort($values);
|
||||
|
||||
$count = count($values);
|
||||
$sum = array_sum($values);
|
||||
$average = $sum / $count;
|
||||
$min = min($values);
|
||||
$max = max($values);
|
||||
|
||||
// Calculate median
|
||||
$middle = intval($count / 2);
|
||||
$median = $count % 2 === 0
|
||||
? ($values[$middle - 1] + $values[$middle]) / 2
|
||||
: $values[$middle];
|
||||
|
||||
// Calculate 95th percentile
|
||||
$p95Index = intval($count * 0.95);
|
||||
$p95Index = min($p95Index, $count - 1);
|
||||
$percentile95 = $values[$p95Index];
|
||||
|
||||
return new NumericResult(
|
||||
count: $count,
|
||||
sum: $sum,
|
||||
average: $average,
|
||||
min: $min,
|
||||
max: $max,
|
||||
median: $median,
|
||||
percentile95: $percentile95
|
||||
);
|
||||
}
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return 'numeric';
|
||||
}
|
||||
}
|
||||
55
src/Framework/SlidingWindow/Aggregator/NumericResult.php
Normal file
55
src/Framework/SlidingWindow/Aggregator/NumericResult.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow\Aggregator;
|
||||
|
||||
/**
|
||||
* Result of numeric aggregation
|
||||
*/
|
||||
final readonly class NumericResult
|
||||
{
|
||||
public function __construct(
|
||||
public int $count,
|
||||
public float $sum,
|
||||
public float $average,
|
||||
public ?float $min,
|
||||
public ?float $max,
|
||||
public ?float $median,
|
||||
public ?float $percentile95,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->count === 0;
|
||||
}
|
||||
|
||||
public function hasMinimumSamples(int $minimum): bool
|
||||
{
|
||||
return $this->count >= $minimum;
|
||||
}
|
||||
|
||||
public function isAverageAbove(float $threshold): bool
|
||||
{
|
||||
return $this->average > $threshold;
|
||||
}
|
||||
|
||||
public function isPercentile95Above(float $threshold): bool
|
||||
{
|
||||
return $this->percentile95 !== null && $this->percentile95 > $threshold;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'count' => $this->count,
|
||||
'sum' => $this->sum,
|
||||
'average' => $this->average,
|
||||
'min' => $this->min,
|
||||
'max' => $this->max,
|
||||
'median' => $this->median,
|
||||
'percentile_95' => $this->percentile95,
|
||||
];
|
||||
}
|
||||
}
|
||||
198
src/Framework/SlidingWindow/Aggregator/RateLimitAggregator.php
Normal file
198
src/Framework/SlidingWindow/Aggregator/RateLimitAggregator.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow\Aggregator;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\SlidingWindow\WindowEntry;
|
||||
|
||||
/**
|
||||
* Advanced aggregator for rate limiting with burst detection and threat analysis
|
||||
*
|
||||
* @implements SlidingWindowAggregator<RateLimitResult>
|
||||
*/
|
||||
final readonly class RateLimitAggregator implements SlidingWindowAggregator
|
||||
{
|
||||
public function __construct(
|
||||
private float $burstThreshold = 0.8,
|
||||
private float $minIntervalThreshold = 2.0
|
||||
) {
|
||||
}
|
||||
|
||||
public function aggregate(WindowEntry ...$entries): RateLimitResult
|
||||
{
|
||||
$timestamps = array_map(fn (WindowEntry $entry) => $entry->timestamp, $entries);
|
||||
$count = count($entries);
|
||||
|
||||
if ($count < 2) {
|
||||
return new RateLimitResult(
|
||||
count: $count,
|
||||
timestamps: $timestamps
|
||||
);
|
||||
}
|
||||
|
||||
// Sort timestamps for analysis
|
||||
$sortedTimestamps = $timestamps;
|
||||
usort($sortedTimestamps, fn (Timestamp $a, Timestamp $b) => $a->toFloat() <=> $b->toFloat());
|
||||
|
||||
// Calculate intervals between requests
|
||||
$intervals = [];
|
||||
for ($i = 1; $i < count($sortedTimestamps); $i++) {
|
||||
$intervals[] = $sortedTimestamps[$i]->toFloat() - $sortedTimestamps[$i - 1]->toFloat();
|
||||
}
|
||||
|
||||
// Burst analysis
|
||||
$avgInterval = array_sum($intervals) / count($intervals);
|
||||
$minInterval = min($intervals);
|
||||
|
||||
// Calculate burst intensity (lower intervals = higher intensity)
|
||||
$burstIntensity = $avgInterval > 0 ? ($avgInterval - $minInterval) / $avgInterval : 1.0;
|
||||
$burstDetected = $burstIntensity > $this->burstThreshold && $minInterval < $this->minIntervalThreshold;
|
||||
|
||||
// Threat analysis based on patterns
|
||||
$threatAnalysis = $this->analyzeThreatPatterns($intervals, $count, $burstIntensity);
|
||||
|
||||
return new RateLimitResult(
|
||||
count: $count,
|
||||
timestamps: $timestamps,
|
||||
burstIntensity: $burstIntensity,
|
||||
minInterval: $minInterval,
|
||||
avgInterval: $avgInterval,
|
||||
burstDetected: $burstDetected,
|
||||
threatAnalysis: $threatAnalysis
|
||||
);
|
||||
}
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return 'rate_limit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze threat patterns from request intervals
|
||||
*/
|
||||
private function analyzeThreatPatterns(array $intervals, int $count, float $burstIntensity): array
|
||||
{
|
||||
$patterns = [];
|
||||
|
||||
// High frequency attack
|
||||
if ($count > 100) {
|
||||
$patterns[] = 'high_frequency_attack';
|
||||
}
|
||||
|
||||
// Burst attack pattern
|
||||
if ($burstIntensity > $this->burstThreshold) {
|
||||
$patterns[] = 'burst_attack';
|
||||
}
|
||||
|
||||
// Sustained attack pattern (many requests with consistent timing)
|
||||
$intervalVariance = $this->calculateVariance($intervals);
|
||||
if ($intervalVariance < 0.1 && count($intervals) > 10) {
|
||||
$patterns[] = 'sustained_attack';
|
||||
}
|
||||
|
||||
// Gradual ramp-up attack
|
||||
if ($this->detectRampUpPattern($intervals)) {
|
||||
$patterns[] = 'ramp_up_attack';
|
||||
}
|
||||
|
||||
return [
|
||||
'patterns' => $patterns,
|
||||
'confidence' => $this->calculateConfidence($patterns, $count, $burstIntensity),
|
||||
'threat_score' => $this->calculateThreatScore($patterns, $count, $burstIntensity),
|
||||
'interval_variance' => $intervalVariance ?? 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate variance of intervals
|
||||
*/
|
||||
private function calculateVariance(array $intervals): float
|
||||
{
|
||||
if (count($intervals) < 2) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$mean = array_sum($intervals) / count($intervals);
|
||||
$variance = array_sum(array_map(fn ($x) => pow($x - $mean, 2), $intervals)) / count($intervals);
|
||||
|
||||
return $variance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect ramp-up attack pattern (increasing frequency over time)
|
||||
*/
|
||||
private function detectRampUpPattern(array $intervals): bool
|
||||
{
|
||||
if (count($intervals) < 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if intervals are generally decreasing (frequency increasing)
|
||||
$decreasing = 0;
|
||||
for ($i = 1; $i < count($intervals); $i++) {
|
||||
if ($intervals[$i] < $intervals[$i - 1]) {
|
||||
$decreasing++;
|
||||
}
|
||||
}
|
||||
|
||||
// If more than 70% of intervals are decreasing, it's likely a ramp-up
|
||||
return ($decreasing / (count($intervals) - 1)) > 0.7;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate confidence score for threat detection
|
||||
*/
|
||||
private function calculateConfidence(array $patterns, int $count, float $burstIntensity): float
|
||||
{
|
||||
$confidence = 0.0;
|
||||
|
||||
// Base confidence on number of patterns detected
|
||||
$confidence += count($patterns) * 0.2;
|
||||
|
||||
// High request count increases confidence
|
||||
if ($count > 50) {
|
||||
$confidence += 0.3;
|
||||
}
|
||||
|
||||
// High burst intensity increases confidence
|
||||
if ($burstIntensity > 0.8) {
|
||||
$confidence += 0.4;
|
||||
}
|
||||
|
||||
return min(1.0, $confidence);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate threat score (0-100)
|
||||
*/
|
||||
private function calculateThreatScore(array $patterns, int $count, float $burstIntensity): float
|
||||
{
|
||||
$score = 0.0;
|
||||
|
||||
// Pattern-based scoring
|
||||
$patternScores = [
|
||||
'high_frequency_attack' => 30,
|
||||
'burst_attack' => 25,
|
||||
'sustained_attack' => 20,
|
||||
'ramp_up_attack' => 15,
|
||||
];
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
$score += $patternScores[$pattern] ?? 0;
|
||||
}
|
||||
|
||||
// Count-based scoring
|
||||
if ($count > 100) {
|
||||
$score += 20;
|
||||
} elseif ($count > 50) {
|
||||
$score += 10;
|
||||
}
|
||||
|
||||
// Burst intensity scoring
|
||||
$score += $burstIntensity * 25;
|
||||
|
||||
return min(100.0, $score);
|
||||
}
|
||||
}
|
||||
70
src/Framework/SlidingWindow/Aggregator/RateLimitResult.php
Normal file
70
src/Framework/SlidingWindow/Aggregator/RateLimitResult.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow\Aggregator;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
|
||||
/**
|
||||
* Result object for rate limit aggregations with threat analysis
|
||||
*/
|
||||
final readonly class RateLimitResult
|
||||
{
|
||||
public function __construct(
|
||||
public int $count,
|
||||
public array $timestamps,
|
||||
public ?float $burstIntensity = null,
|
||||
public ?float $minInterval = null,
|
||||
public ?float $avgInterval = null,
|
||||
public bool $burstDetected = false,
|
||||
public array $threatAnalysis = [],
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all timestamps as array of floats
|
||||
*/
|
||||
public function getTimestampsAsFloats(): array
|
||||
{
|
||||
return array_map(
|
||||
fn (Timestamp $timestamp) => $timestamp->toFloat(),
|
||||
$this->timestamps
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if burst pattern is detected
|
||||
*/
|
||||
public function hasBurstPattern(): bool
|
||||
{
|
||||
return $this->burstDetected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get burst analysis data
|
||||
*/
|
||||
public function getBurstAnalysis(): array
|
||||
{
|
||||
return [
|
||||
'burst_detected' => $this->burstDetected,
|
||||
'burst_intensity' => $this->burstIntensity,
|
||||
'min_interval' => $this->minInterval,
|
||||
'avg_interval' => $this->avgInterval,
|
||||
'request_count' => $this->count,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array for storage/serialization
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'count' => $this->count,
|
||||
'timestamps' => $this->getTimestampsAsFloats(),
|
||||
'burst_analysis' => $this->getBurstAnalysis(),
|
||||
'threat_analysis' => $this->threatAnalysis,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow\Aggregator;
|
||||
|
||||
use App\Framework\SlidingWindow\WindowEntry;
|
||||
|
||||
/**
|
||||
* Interface for aggregating sliding window data
|
||||
*
|
||||
* @template TResult
|
||||
*/
|
||||
interface SlidingWindowAggregator
|
||||
{
|
||||
/**
|
||||
* Aggregate window entries into summary data
|
||||
*
|
||||
* @return TResult
|
||||
*/
|
||||
public function aggregate(WindowEntry ...$entries): mixed;
|
||||
|
||||
/**
|
||||
* Get aggregator identifier
|
||||
*/
|
||||
public function getIdentifier(): string;
|
||||
}
|
||||
159
src/Framework/SlidingWindow/CacheBasedSlidingWindow.php
Normal file
159
src/Framework/SlidingWindow/CacheBasedSlidingWindow.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheKey;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\SlidingWindow\Aggregator\SlidingWindowAggregator;
|
||||
|
||||
/**
|
||||
* Cache-based sliding window implementation for persistence
|
||||
*/
|
||||
final readonly class CacheBasedSlidingWindow implements SlidingWindow
|
||||
{
|
||||
private const string CACHE_PREFIX = 'sliding_window:';
|
||||
|
||||
public function __construct(
|
||||
private Cache $cache,
|
||||
private Duration $windowSize,
|
||||
private string $identifier,
|
||||
private SlidingWindowAggregator $aggregator,
|
||||
private ?Duration $cacheTtl = null,
|
||||
) {
|
||||
$this->cacheTtl ??= Duration::fromHours(1);
|
||||
}
|
||||
|
||||
public function record(mixed $value, Timestamp $timestamp): void
|
||||
{
|
||||
$entries = $this->getEntriesFromCache();
|
||||
$entries[] = new WindowEntry($value, $timestamp);
|
||||
|
||||
$this->storeEntriesToCache($entries);
|
||||
$this->cleanup();
|
||||
}
|
||||
|
||||
public function getValues(?Timestamp $now = null): array
|
||||
{
|
||||
$now ??= Timestamp::now();
|
||||
$this->cleanup($now);
|
||||
|
||||
return array_map(
|
||||
fn (WindowEntry $entry) => $entry->value,
|
||||
$this->getValidEntries($now)
|
||||
);
|
||||
}
|
||||
|
||||
public function getStats(?Timestamp $now = null): SlidingWindowStats
|
||||
{
|
||||
$now ??= Timestamp::now();
|
||||
$this->cleanup($now);
|
||||
|
||||
$validEntries = $this->getValidEntries($now);
|
||||
$windowStart = $now->diff($this->windowSize) > Duration::zero()
|
||||
? Timestamp::fromFloat($now->toFloat() - $this->windowSize->toSeconds())
|
||||
: $now;
|
||||
|
||||
$aggregatedData = [];
|
||||
if (! empty($validEntries)) {
|
||||
$result = $this->aggregator->aggregate(...$validEntries);
|
||||
$aggregatedData = method_exists($result, 'toArray') ? $result->toArray() : [$this->aggregator->getIdentifier() => $result];
|
||||
}
|
||||
|
||||
return new SlidingWindowStats(
|
||||
totalCount: count($validEntries),
|
||||
windowSize: $this->windowSize,
|
||||
windowStart: $windowStart,
|
||||
windowEnd: $now,
|
||||
aggregatedData: $aggregatedData
|
||||
);
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$cacheKey = $this->getCacheKey();
|
||||
$this->cache->forget($cacheKey);
|
||||
}
|
||||
|
||||
public function getWindowSize(): Duration
|
||||
{
|
||||
return $this->windowSize;
|
||||
}
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<WindowEntry>
|
||||
*/
|
||||
private function getEntriesFromCache(): array
|
||||
{
|
||||
$cacheKey = $this->getCacheKey();
|
||||
$cacheItem = $this->cache->get($cacheKey);
|
||||
|
||||
if (! $cacheItem->isHit) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$serializedEntries = $cacheItem->value;
|
||||
if (! is_array($serializedEntries)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(
|
||||
fn (array $data) => new WindowEntry(
|
||||
$data['value'],
|
||||
Timestamp::fromFloat($data['timestamp'])
|
||||
),
|
||||
$serializedEntries
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<WindowEntry> $entries
|
||||
*/
|
||||
private function storeEntriesToCache(array $entries): void
|
||||
{
|
||||
$cacheKey = $this->getCacheKey();
|
||||
$serializedEntries = array_map(
|
||||
fn (WindowEntry $entry) => [
|
||||
'value' => $entry->value,
|
||||
'timestamp' => $entry->timestamp->toFloat(),
|
||||
],
|
||||
$entries
|
||||
);
|
||||
|
||||
$this->cache->set($cacheKey, $serializedEntries, $this->cacheTtl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<WindowEntry>
|
||||
*/
|
||||
private function getValidEntries(Timestamp $now): array
|
||||
{
|
||||
$entries = $this->getEntriesFromCache();
|
||||
$cutoffTime = Timestamp::fromFloat($now->toFloat() - $this->windowSize->toSeconds());
|
||||
|
||||
return array_filter(
|
||||
$entries,
|
||||
fn (WindowEntry $entry) => $entry->isWithinWindow($cutoffTime)
|
||||
);
|
||||
}
|
||||
|
||||
private function cleanup(?Timestamp $now = null): void
|
||||
{
|
||||
$now ??= Timestamp::now();
|
||||
$validEntries = $this->getValidEntries($now);
|
||||
$this->storeEntriesToCache($validEntries);
|
||||
}
|
||||
|
||||
private function getCacheKey(): CacheKey
|
||||
{
|
||||
return CacheKey::fromString(self::CACHE_PREFIX . $this->identifier);
|
||||
}
|
||||
}
|
||||
108
src/Framework/SlidingWindow/InMemorySlidingWindow.php
Normal file
108
src/Framework/SlidingWindow/InMemorySlidingWindow.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\SlidingWindow\Aggregator\SlidingWindowAggregator;
|
||||
|
||||
/**
|
||||
* In-memory sliding window implementation
|
||||
*/
|
||||
final class InMemorySlidingWindow implements SlidingWindow
|
||||
{
|
||||
/**
|
||||
* @var array<WindowEntry>
|
||||
*/
|
||||
private array $entries = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly Duration $windowSize,
|
||||
private readonly string $identifier,
|
||||
private readonly SlidingWindowAggregator $aggregator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function record(mixed $value, Timestamp $timestamp): void
|
||||
{
|
||||
$this->entries[] = new WindowEntry($value, $timestamp);
|
||||
$this->cleanup();
|
||||
}
|
||||
|
||||
public function getValues(?Timestamp $now = null): array
|
||||
{
|
||||
$now ??= Timestamp::now();
|
||||
$this->cleanup($now);
|
||||
|
||||
return array_map(
|
||||
fn (WindowEntry $entry) => $entry->value,
|
||||
$this->getValidEntries($now)
|
||||
);
|
||||
}
|
||||
|
||||
public function getStats(?Timestamp $now = null): SlidingWindowStats
|
||||
{
|
||||
$now ??= Timestamp::now();
|
||||
$this->cleanup($now);
|
||||
|
||||
$validEntries = $this->getValidEntries($now);
|
||||
$windowStart = $now->diff($this->windowSize) > Duration::zero()
|
||||
? Timestamp::fromFloat($now->toFloat() - $this->windowSize->toSeconds())
|
||||
: $now;
|
||||
|
||||
$aggregatedData = [];
|
||||
if (! empty($validEntries)) {
|
||||
$result = $this->aggregator->aggregate(...$validEntries);
|
||||
$aggregatedData = method_exists($result, 'toArray') ? $result->toArray() : [$this->aggregator->getIdentifier() => $result];
|
||||
}
|
||||
|
||||
return new SlidingWindowStats(
|
||||
totalCount: count($validEntries),
|
||||
windowSize: $this->windowSize,
|
||||
windowStart: $windowStart,
|
||||
windowEnd: $now,
|
||||
aggregatedData: $aggregatedData
|
||||
);
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->entries = [];
|
||||
}
|
||||
|
||||
public function getWindowSize(): Duration
|
||||
{
|
||||
return $this->windowSize;
|
||||
}
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all valid entries within the window
|
||||
*
|
||||
* @return array<WindowEntry>
|
||||
*/
|
||||
private function getValidEntries(Timestamp $now): array
|
||||
{
|
||||
$cutoffTime = Timestamp::fromFloat($now->toFloat() - $this->windowSize->toSeconds());
|
||||
|
||||
return array_filter(
|
||||
$this->entries,
|
||||
fn (WindowEntry $entry) => $entry->isWithinWindow($cutoffTime)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entries outside the window
|
||||
*/
|
||||
private function cleanup(?Timestamp $now = null): void
|
||||
{
|
||||
$now ??= Timestamp::now();
|
||||
$this->entries = $this->getValidEntries($now);
|
||||
}
|
||||
}
|
||||
50
src/Framework/SlidingWindow/SlidingWindow.php
Normal file
50
src/Framework/SlidingWindow/SlidingWindow.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
|
||||
/**
|
||||
* Generic sliding window interface for time-based data collection
|
||||
*
|
||||
* @template T
|
||||
*/
|
||||
interface SlidingWindow
|
||||
{
|
||||
/**
|
||||
* Record a value with timestamp
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function record(mixed $value, Timestamp $timestamp): void;
|
||||
|
||||
/**
|
||||
* Get all values within the window
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function getValues(?Timestamp $now = null): array;
|
||||
|
||||
/**
|
||||
* Get aggregated statistics for the window
|
||||
*/
|
||||
public function getStats(?Timestamp $now = null): SlidingWindowStats;
|
||||
|
||||
/**
|
||||
* Clear all data from the window
|
||||
*/
|
||||
public function clear(): void;
|
||||
|
||||
/**
|
||||
* Get window configuration
|
||||
*/
|
||||
public function getWindowSize(): Duration;
|
||||
|
||||
/**
|
||||
* Get window identifier
|
||||
*/
|
||||
public function getIdentifier(): string;
|
||||
}
|
||||
100
src/Framework/SlidingWindow/SlidingWindowFactory.php
Normal file
100
src/Framework/SlidingWindow/SlidingWindowFactory.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\SlidingWindow\Aggregator\BooleanAggregator;
|
||||
use App\Framework\SlidingWindow\Aggregator\CountingAggregator;
|
||||
use App\Framework\SlidingWindow\Aggregator\NumericAggregator;
|
||||
use App\Framework\SlidingWindow\Aggregator\RateLimitAggregator;
|
||||
use App\Framework\SlidingWindow\Aggregator\SlidingWindowAggregator;
|
||||
|
||||
/**
|
||||
* Factory for creating sliding windows
|
||||
*/
|
||||
final readonly class SlidingWindowFactory
|
||||
{
|
||||
public function __construct(
|
||||
private ?Cache $cache = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a counting sliding window (for rate limiting)
|
||||
*/
|
||||
public function createCountingWindow(
|
||||
string $identifier,
|
||||
Duration $windowSize,
|
||||
bool $persistent = false
|
||||
): SlidingWindow {
|
||||
$aggregator = new CountingAggregator();
|
||||
|
||||
return $persistent && $this->cache !== null
|
||||
? new CacheBasedSlidingWindow($this->cache, $windowSize, $identifier, $aggregator)
|
||||
: new InMemorySlidingWindow($windowSize, $identifier, $aggregator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a boolean sliding window (for circuit breakers)
|
||||
*/
|
||||
public function createBooleanWindow(
|
||||
string $identifier,
|
||||
Duration $windowSize,
|
||||
bool $persistent = false
|
||||
): SlidingWindow {
|
||||
$aggregator = new BooleanAggregator();
|
||||
|
||||
return $persistent && $this->cache !== null
|
||||
? new CacheBasedSlidingWindow($this->cache, $windowSize, $identifier, $aggregator)
|
||||
: new InMemorySlidingWindow($windowSize, $identifier, $aggregator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a numeric sliding window (for performance metrics)
|
||||
*/
|
||||
public function createNumericWindow(
|
||||
string $identifier,
|
||||
Duration $windowSize,
|
||||
bool $persistent = false
|
||||
): SlidingWindow {
|
||||
$aggregator = new NumericAggregator();
|
||||
|
||||
return $persistent && $this->cache !== null
|
||||
? new CacheBasedSlidingWindow($this->cache, $windowSize, $identifier, $aggregator)
|
||||
: new InMemorySlidingWindow($windowSize, $identifier, $aggregator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a rate limit sliding window with advanced analytics
|
||||
*/
|
||||
public function createRateLimitWindow(
|
||||
string $identifier,
|
||||
Duration $windowSize,
|
||||
bool $persistent = false,
|
||||
float $burstThreshold = 0.8,
|
||||
float $minIntervalThreshold = 2.0
|
||||
): SlidingWindow {
|
||||
$aggregator = new RateLimitAggregator($burstThreshold, $minIntervalThreshold);
|
||||
|
||||
return $persistent && $this->cache !== null
|
||||
? new CacheBasedSlidingWindow($this->cache, $windowSize, $identifier, $aggregator)
|
||||
: new InMemorySlidingWindow($windowSize, $identifier, $aggregator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a custom sliding window with provided aggregator
|
||||
*/
|
||||
public function createCustomWindow(
|
||||
string $identifier,
|
||||
Duration $windowSize,
|
||||
SlidingWindowAggregator $aggregator,
|
||||
bool $persistent = false
|
||||
): SlidingWindow {
|
||||
return $persistent && $this->cache !== null
|
||||
? new CacheBasedSlidingWindow($this->cache, $windowSize, $identifier, $aggregator)
|
||||
: new InMemorySlidingWindow($windowSize, $identifier, $aggregator);
|
||||
}
|
||||
}
|
||||
81
src/Framework/SlidingWindow/SlidingWindowStats.php
Normal file
81
src/Framework/SlidingWindow/SlidingWindowStats.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
|
||||
/**
|
||||
* Statistics aggregated from a sliding window
|
||||
*/
|
||||
final readonly class SlidingWindowStats
|
||||
{
|
||||
public function __construct(
|
||||
public int $totalCount,
|
||||
public Duration $windowSize,
|
||||
public Timestamp $windowStart,
|
||||
public Timestamp $windowEnd,
|
||||
public array $aggregatedData = [],
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if minimum count threshold is met
|
||||
*/
|
||||
public function hasMinimumCount(int $minimum): bool
|
||||
{
|
||||
return $this->totalCount >= $minimum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rate per second
|
||||
*/
|
||||
public function getRatePerSecond(): float
|
||||
{
|
||||
$seconds = $this->windowSize->toSeconds();
|
||||
|
||||
return $seconds > 0 ? $this->totalCount / $seconds : 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rate per minute
|
||||
*/
|
||||
public function getRatePerMinute(): float
|
||||
{
|
||||
return $this->getRatePerSecond() * 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated value by key
|
||||
*/
|
||||
public function getAggregatedValue(string $key): mixed
|
||||
{
|
||||
return $this->aggregatedData[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if window is empty
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->totalCount === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array for serialization
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'total_count' => $this->totalCount,
|
||||
'window_size' => $this->windowSize->toHumanReadable(),
|
||||
'window_start' => $this->windowStart->toIsoString(),
|
||||
'window_end' => $this->windowEnd->toIsoString(),
|
||||
'rate_per_second' => $this->getRatePerSecond(),
|
||||
'rate_per_minute' => $this->getRatePerMinute(),
|
||||
'aggregated_data' => $this->aggregatedData,
|
||||
];
|
||||
}
|
||||
}
|
||||
38
src/Framework/SlidingWindow/WindowEntry.php
Normal file
38
src/Framework/SlidingWindow/WindowEntry.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SlidingWindow;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
|
||||
/**
|
||||
* Single entry in a sliding window
|
||||
*/
|
||||
final readonly class WindowEntry
|
||||
{
|
||||
public function __construct(
|
||||
public mixed $value,
|
||||
public Timestamp $timestamp,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if entry is within window from given timestamp
|
||||
*/
|
||||
public function isWithinWindow(Timestamp $fromTimestamp): bool
|
||||
{
|
||||
return $this->timestamp->isAfter($fromTimestamp) || $this->timestamp->equals($fromTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array for serialization
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'value' => $this->value,
|
||||
'timestamp' => $this->timestamp->toIsoString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user