- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
458 lines
15 KiB
PHP
458 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\Percentage;
|
|
use App\Framework\Database\ConnectionInterface;
|
|
use App\Framework\Database\Migration\Exception\MemoryThresholdExceededException;
|
|
use App\Framework\Database\Migration\Migration;
|
|
use App\Framework\Database\Migration\MigrationCollection;
|
|
use App\Framework\Database\Migration\MigrationRunner;
|
|
use App\Framework\Database\Migration\MigrationVersion;
|
|
use App\Framework\Database\Migration\ValueObjects\MemoryThresholds;
|
|
use App\Framework\Database\Platform\MySQLPlatform;
|
|
use App\Framework\Database\ResultInterface;
|
|
use App\Framework\Database\ValueObjects\SqlQuery;
|
|
use App\Framework\DateTime\SystemClock;
|
|
use App\Framework\Performance\MemoryMonitor;
|
|
use App\Framework\Performance\OperationTracker;
|
|
use App\Framework\Performance\PerformanceReporter;
|
|
|
|
describe('Migration Performance Integration', function () {
|
|
beforeEach(function () {
|
|
$this->connection = new MigrationPerformanceMockConnection();
|
|
$this->platform = new MySQLPlatform();
|
|
$this->clock = new SystemClock();
|
|
$this->memoryMonitor = new MemoryMonitor();
|
|
$this->operationTracker = new OperationTracker($this->clock, $this->memoryMonitor);
|
|
|
|
$this->migrationRunner = new MigrationRunner(
|
|
$this->connection,
|
|
$this->platform,
|
|
$this->clock,
|
|
null, // tableConfig
|
|
null, // logger
|
|
$this->operationTracker,
|
|
$this->memoryMonitor
|
|
);
|
|
});
|
|
|
|
test('migration runner integrates with performance monitoring', function () {
|
|
$migration = new MigrationPerformanceTestMigration();
|
|
$migrations = new MigrationCollection($migration);
|
|
|
|
// Set no applied migrations initially
|
|
$this->connection->setAppliedMigrations([]);
|
|
|
|
$result = $this->migrationRunner->migrate($migrations);
|
|
|
|
expect($result)->toContain('2024_01_01_000000');
|
|
expect($migration->wasExecuted())->toBeTrue();
|
|
|
|
// Verify performance tracking was used
|
|
expect($this->operationTracker)->toBeInstanceOf(OperationTracker::class);
|
|
expect($this->memoryMonitor)->toBeInstanceOf(MemoryMonitor::class);
|
|
});
|
|
|
|
test('memory monitor provides memory summary', function () {
|
|
$memorySummary = $this->memoryMonitor->getSummary();
|
|
|
|
expect($memorySummary)->toHaveProperties([
|
|
'current',
|
|
'peak',
|
|
'limit',
|
|
'usagePercentage',
|
|
'isApproachingLimit',
|
|
]);
|
|
|
|
expect($memorySummary->getCurrentHumanReadable())->toBeString();
|
|
expect($memorySummary->getPeakHumanReadable())->toBeString();
|
|
expect($memorySummary->getUsagePercentageFormatted())->toBeString();
|
|
});
|
|
|
|
test('operation tracker can track operations', function () {
|
|
$operationId = 'test_migration_batch_' . uniqid();
|
|
|
|
$snapshot = $this->operationTracker->startOperation(
|
|
$operationId,
|
|
\App\Framework\Performance\PerformanceCategory::DATABASE,
|
|
['operation_type' => 'migration_test']
|
|
);
|
|
|
|
expect($snapshot)->toBeInstanceOf(\App\Framework\Performance\ValueObjects\PerformanceSnapshot::class);
|
|
|
|
// Complete the operation
|
|
$finalSnapshot = $this->operationTracker->completeOperation($operationId);
|
|
|
|
expect($finalSnapshot)->toBeInstanceOf(\App\Framework\Performance\ValueObjects\PerformanceSnapshot::class);
|
|
expect($finalSnapshot->duration)->toBeInstanceOf(\App\Framework\Core\ValueObjects\Duration::class);
|
|
});
|
|
|
|
test('memory thresholds can be configured with custom values', function () {
|
|
$customThresholds = new MemoryThresholds(
|
|
warning: Percentage::from(60.0),
|
|
critical: Percentage::from(75.0),
|
|
abort: Percentage::from(90.0)
|
|
);
|
|
|
|
expect($customThresholds->warning->getValue())->toBe(60.0);
|
|
expect($customThresholds->critical->getValue())->toBe(75.0);
|
|
expect($customThresholds->abort->getValue())->toBe(90.0);
|
|
});
|
|
|
|
test('memory thresholds default configuration is valid', function () {
|
|
$defaultThresholds = MemoryThresholds::default();
|
|
|
|
expect($defaultThresholds->warning->getValue())->toBe(75.0);
|
|
expect($defaultThresholds->critical->getValue())->toBe(85.0);
|
|
expect($defaultThresholds->abort->getValue())->toBe(95.0);
|
|
});
|
|
|
|
test('memory thresholds conservative configuration is valid', function () {
|
|
$conservativeThresholds = MemoryThresholds::conservative();
|
|
|
|
expect($conservativeThresholds->warning->getValue())->toBe(60.0);
|
|
expect($conservativeThresholds->critical->getValue())->toBe(70.0);
|
|
expect($conservativeThresholds->abort->getValue())->toBe(80.0);
|
|
});
|
|
|
|
test('memory thresholds relaxed configuration is valid', function () {
|
|
$relaxedThresholds = MemoryThresholds::relaxed();
|
|
|
|
expect($relaxedThresholds->warning->getValue())->toBe(80.0);
|
|
expect($relaxedThresholds->critical->getValue())->toBe(90.0);
|
|
expect($relaxedThresholds->abort->getValue())->toBe(98.0);
|
|
});
|
|
|
|
test('memory threshold exception can be created for migration', function () {
|
|
$currentUsage = Percentage::from(96.0);
|
|
$threshold = Percentage::from(95.0);
|
|
$currentMemory = \App\Framework\Core\ValueObjects\Byte::fromBytes(256 * 1024 * 1024);
|
|
$memoryLimit = \App\Framework\Core\ValueObjects\Byte::fromBytes(256 * 1024 * 1024);
|
|
|
|
$exception = MemoryThresholdExceededException::forMigration(
|
|
'2024_01_01_000001',
|
|
$currentUsage,
|
|
$threshold,
|
|
$currentMemory,
|
|
$memoryLimit
|
|
);
|
|
|
|
expect($exception->getMessage())->toContain('Memory threshold exceeded during migration 2024_01_01_000001');
|
|
expect($exception->getMessage())->toContain('96.0%');
|
|
expect($exception->getMessage())->toContain('95.0%');
|
|
});
|
|
|
|
test('memory threshold exception can be created for batch abort', function () {
|
|
$currentUsage = Percentage::from(97.0);
|
|
$abortThreshold = Percentage::from(95.0);
|
|
$currentMemory = \App\Framework\Core\ValueObjects\Byte::fromBytes(248 * 1024 * 1024);
|
|
$memoryLimit = \App\Framework\Core\ValueObjects\Byte::fromBytes(256 * 1024 * 1024);
|
|
|
|
$exception = MemoryThresholdExceededException::batchAborted(
|
|
$currentUsage,
|
|
$abortThreshold,
|
|
$currentMemory,
|
|
$memoryLimit,
|
|
3,
|
|
10
|
|
);
|
|
|
|
expect($exception->getMessage())->toContain('Migration batch aborted due to critical memory usage');
|
|
expect($exception->getMessage())->toContain('97.0%');
|
|
expect($exception->getMessage())->toContain('95.0%');
|
|
});
|
|
|
|
test('migration runner with memory thresholds can handle normal operation', function () {
|
|
$memoryThresholds = MemoryThresholds::conservative();
|
|
|
|
$migrationRunner = new MigrationRunner(
|
|
$this->connection,
|
|
$this->platform,
|
|
$this->clock,
|
|
null, // tableConfig
|
|
null, // logger
|
|
$this->operationTracker,
|
|
$this->memoryMonitor,
|
|
null, // performanceReporter
|
|
$memoryThresholds
|
|
);
|
|
|
|
$migration = new MigrationPerformanceTestMigration();
|
|
$migrations = new MigrationCollection($migration);
|
|
|
|
// Set no applied migrations initially
|
|
$this->connection->setAppliedMigrations([]);
|
|
|
|
$result = $migrationRunner->migrate($migrations);
|
|
|
|
expect($result)->toContain('2024_01_01_000000');
|
|
expect($migration->wasExecuted())->toBeTrue();
|
|
});
|
|
|
|
test('rollback operation includes performance tracking', function () {
|
|
$migration = new MigrationPerformanceTestMigration();
|
|
$migrations = new MigrationCollection($migration);
|
|
|
|
// Set migration as already applied
|
|
$this->connection->setAppliedMigrations([
|
|
['version' => '2024_01_01_000000', 'description' => 'Performance Test Migration'],
|
|
]);
|
|
|
|
$result = $this->migrationRunner->rollback($migrations, 1);
|
|
|
|
expect($result)->toContain($migration);
|
|
|
|
// Verify performance tracking was used during rollback
|
|
expect($this->operationTracker)->toBeInstanceOf(OperationTracker::class);
|
|
expect($this->memoryMonitor)->toBeInstanceOf(MemoryMonitor::class);
|
|
});
|
|
|
|
test('operation tracker can track multiple operations', function () {
|
|
$operationId1 = 'test_migration_1';
|
|
$operationId2 = 'test_migration_2';
|
|
|
|
// Start first operation
|
|
$snapshot1 = $this->operationTracker->startOperation(
|
|
$operationId1,
|
|
\App\Framework\Performance\PerformanceCategory::DATABASE,
|
|
['operation_type' => 'migration_execution']
|
|
);
|
|
|
|
// Start second operation
|
|
$snapshot2 = $this->operationTracker->startOperation(
|
|
$operationId2,
|
|
\App\Framework\Performance\PerformanceCategory::DATABASE,
|
|
['operation_type' => 'migration_execution']
|
|
);
|
|
|
|
usleep(1000); // Small delay to ensure duration > 0
|
|
|
|
// Complete both operations
|
|
$finalSnapshot1 = $this->operationTracker->completeOperation($operationId1);
|
|
$finalSnapshot2 = $this->operationTracker->completeOperation($operationId2);
|
|
|
|
expect($finalSnapshot1)->toBeInstanceOf(\App\Framework\Performance\ValueObjects\PerformanceSnapshot::class);
|
|
expect($finalSnapshot2)->toBeInstanceOf(\App\Framework\Performance\ValueObjects\PerformanceSnapshot::class);
|
|
expect($finalSnapshot1->operationId)->toBe($operationId1);
|
|
expect($finalSnapshot2->operationId)->toBe($operationId2);
|
|
});
|
|
|
|
test('migration runner validates memory threshold order', function () {
|
|
expect(function () {
|
|
new MemoryThresholds(
|
|
warning: Percentage::from(90.0), // Warning higher than critical
|
|
critical: Percentage::from(80.0),
|
|
abort: Percentage::from(95.0)
|
|
);
|
|
})->toThrow(\InvalidArgumentException::class, 'Warning threshold cannot be higher than critical threshold');
|
|
|
|
expect(function () {
|
|
new MemoryThresholds(
|
|
warning: Percentage::from(70.0),
|
|
critical: Percentage::from(90.0), // Critical higher than abort
|
|
abort: Percentage::from(80.0)
|
|
);
|
|
})->toThrow(\InvalidArgumentException::class, 'Critical threshold cannot be higher than abort threshold');
|
|
});
|
|
|
|
test('migration runner can handle pre-flight checks', function () {
|
|
$migration = new MigrationPerformanceTestMigration();
|
|
$migrations = new MigrationCollection($migration);
|
|
|
|
// Set no applied migrations initially
|
|
$this->connection->setAppliedMigrations([]);
|
|
|
|
// Should pass pre-flight checks for mock connection
|
|
$result = $this->migrationRunner->migrate($migrations);
|
|
|
|
expect($result)->toContain('2024_01_01_000000');
|
|
expect($migration->wasExecuted())->toBeTrue();
|
|
});
|
|
});
|
|
|
|
// Test fixtures
|
|
class MigrationPerformanceTestMigration implements Migration
|
|
{
|
|
private bool $executed = false;
|
|
|
|
public function up(ConnectionInterface $connection): void
|
|
{
|
|
$this->executed = true;
|
|
// Simulate migration execution
|
|
$connection->execute(SqlQuery::create('CREATE TABLE performance_test (id INT)'));
|
|
}
|
|
|
|
public function down(ConnectionInterface $connection): void
|
|
{
|
|
$connection->execute(SqlQuery::create('DROP TABLE performance_test'));
|
|
}
|
|
|
|
public function getDescription(): string
|
|
{
|
|
return 'Performance Test Migration';
|
|
}
|
|
|
|
public function getVersion(): MigrationVersion
|
|
{
|
|
return MigrationVersion::fromTimestamp('2024_01_01_000000');
|
|
}
|
|
|
|
public function wasExecuted(): bool
|
|
{
|
|
return $this->executed;
|
|
}
|
|
}
|
|
|
|
class MigrationPerformanceMockConnection implements ConnectionInterface
|
|
{
|
|
private array $queries = [];
|
|
|
|
private array $appliedMigrations = [];
|
|
|
|
private bool $inTransaction = false;
|
|
|
|
public function setAppliedMigrations(array $migrations): void
|
|
{
|
|
$this->appliedMigrations = $migrations;
|
|
}
|
|
|
|
public function execute(SqlQuery $query): int
|
|
{
|
|
$this->queries[] = ['type' => 'execute', 'sql' => $query->sql, 'params' => $query->parameters->toArray()];
|
|
|
|
return 1;
|
|
}
|
|
|
|
public function query(SqlQuery $query): ResultInterface
|
|
{
|
|
$this->queries[] = ['type' => 'query', 'sql' => $query->sql, 'params' => $query->parameters->toArray()];
|
|
|
|
return new MigrationPerformanceMockResult($this->appliedMigrations);
|
|
}
|
|
|
|
public function queryOne(SqlQuery $query): ?array
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public function queryColumn(SqlQuery $query): array
|
|
{
|
|
$this->queries[] = ['type' => 'queryColumn', 'sql' => $query->sql, 'params' => $query->parameters->toArray()];
|
|
|
|
return array_column($this->appliedMigrations, 'version');
|
|
}
|
|
|
|
public function queryScalar(SqlQuery $query): mixed
|
|
{
|
|
$this->queries[] = ['type' => 'queryScalar', 'sql' => $query->sql, 'params' => $query->parameters->toArray()];
|
|
|
|
// Return '1' for connectivity test (SELECT 1)
|
|
if (str_contains($query->sql, 'SELECT 1')) {
|
|
return '1';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function beginTransaction(): void
|
|
{
|
|
$this->inTransaction = true;
|
|
}
|
|
|
|
public function commit(): void
|
|
{
|
|
$this->inTransaction = false;
|
|
}
|
|
|
|
public function rollback(): void
|
|
{
|
|
$this->inTransaction = false;
|
|
}
|
|
|
|
public function inTransaction(): bool
|
|
{
|
|
return $this->inTransaction;
|
|
}
|
|
|
|
public function lastInsertId(): string
|
|
{
|
|
return '1';
|
|
}
|
|
|
|
public function getPdo(): \PDO
|
|
{
|
|
return new class () extends \PDO {
|
|
public function __construct()
|
|
{
|
|
// Skip parent constructor to avoid actual DB connection
|
|
}
|
|
|
|
public function getAttribute(int $attribute): mixed
|
|
{
|
|
return match($attribute) {
|
|
\PDO::ATTR_DRIVER_NAME => 'mysql',
|
|
default => null
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
public function getQueries(): array
|
|
{
|
|
return $this->queries;
|
|
}
|
|
}
|
|
|
|
class MigrationPerformanceMockResult implements ResultInterface
|
|
{
|
|
private array $data;
|
|
|
|
public function __construct(array $data)
|
|
{
|
|
$this->data = $data;
|
|
}
|
|
|
|
public function fetch(): ?array
|
|
{
|
|
return $this->data[0] ?? null;
|
|
}
|
|
|
|
public function fetchAll(): array
|
|
{
|
|
return $this->data;
|
|
}
|
|
|
|
public function fetchOne(): ?array
|
|
{
|
|
return $this->data[0] ?? null;
|
|
}
|
|
|
|
public function fetchColumn(int $column = 0): array
|
|
{
|
|
return array_column($this->data, $column);
|
|
}
|
|
|
|
public function fetchScalar(): mixed
|
|
{
|
|
$row = $this->fetchOne();
|
|
|
|
return $row ? array_values($row)[0] : null;
|
|
}
|
|
|
|
public function rowCount(): int
|
|
{
|
|
return count($this->data);
|
|
}
|
|
|
|
public function getIterator(): \Traversable
|
|
{
|
|
return new \ArrayIterator($this->data);
|
|
}
|
|
|
|
public function count(): int
|
|
{
|
|
return count($this->data);
|
|
}
|
|
}
|