diff --git a/tests/autoscaling_test.lua b/tests/autoscaling_test.lua new file mode 100644 index 0000000..21260fa --- /dev/null +++ b/tests/autoscaling_test.lua @@ -0,0 +1,411 @@ +-- tests/autoscaling_test.lua +-- Тестирование автоматического масштабирования кластера + +local function setup_test_environment() + print("=== Setup Test Environment ===") + + -- Создаём тестовую базу данных + local result = db.execute("CREATE DATABASE IF NOT EXISTS autoscale_test") + assert(result, "Failed to create database") + + result = db.execute("USE autoscale_test") + assert(result, "Failed to use database") + + -- Создаём тестовую коллекцию + result = db.execute([[ + CREATE COLLECTION IF NOT EXISTS metrics_collection + WITH SETTINGS { + max_documents = 1000000, + validate_schema = true + } + ]]) + assert(result, "Failed to create collection") + + -- Получаем конфигурацию автомасштабирования + local config = db.execute("SHOW AUTOSCALING CONFIG") + print("Current autoscaling config:") + print(config) + + return true +end + +local function generate_test_load(duration_seconds, target_qps) + print(string.format("Generating %.2f QPS load for %d seconds", target_qps, duration_seconds)) + + local start_time = os.time() + local requests = 0 + local errors = 0 + local latencies = {} + + local interval = 1.0 / target_qps + + while os.time() - start_time < duration_seconds do + local req_start = os.clock() + + -- Симуляция запроса на запись + local success, err = pcall(function() + local doc = { + _id = string.format("doc_%d_%d", os.time(), math.random(1, 10000)), + timestamp = os.time() * 1000, + value = math.random(1, 1000), + type = "test_metric" + } + return db.execute(string.format( + "INSERT INTO metrics_collection VALUES (%s)", + db.json_encode(doc) + )) + end) + + local req_end = os.clock() + local latency = (req_end - req_start) * 1000 -- в миллисекундах + + table.insert(latencies, latency) + requests = requests + 1 + + if not success then + errors = errors + 1 + print("Request failed: " .. tostring(err)) + end + + -- Контроль скорости + local elapsed = os.clock() - req_start + if elapsed < interval then + local sleep_time = interval - elapsed + db.sleep(sleep_time) + end + end + + -- Вычисляем статистику + local total_latency = 0 + for _, lat in ipairs(latencies) do + total_latency = total_latency + lat + end + local avg_latency = total_latency / #latencies + + -- Сортируем для перцентилей + table.sort(latencies) + local p95_latency = latencies[math.floor(#latencies * 0.95)] + local p99_latency = latencies[math.floor(#latencies * 0.99)] + + print(string.format([[ + === Load Test Results === + Duration: %d seconds + Total Requests: %d + Successful: %d + Errors: %d + Actual QPS: %.2f + Avg Latency: %.2f ms + P95 Latency: %.2f ms + P99 Latency: %.2f ms + ]], duration_seconds, requests, requests - errors, errors, + requests / duration_seconds, avg_latency, p95_latency, p99_latency)) + + return { + requests = requests, + errors = errors, + avg_latency = avg_latency, + p95_latency = p95_latency, + p99_latency = p99_latency, + actual_qps = requests / duration_seconds + } +end + +local function monitor_cluster_size(interval_seconds, duration_seconds) + print(string.format("Monitoring cluster size every %d seconds for %d seconds", + interval_seconds, duration_seconds)) + + local history = {} + local start_time = os.time() + + while os.time() - start_time < duration_seconds do + local status = db.execute("SHOW CLUSTER STATUS") + local cluster_info = db.json_decode(status) + + table.insert(history, { + timestamp = os.time(), + nodes = cluster_info.total_nodes, + active_nodes = cluster_info.active_nodes, + leader = cluster_info.leader_id, + health = cluster_info.health + }) + + print(string.format("Time %ds: Nodes=%d, Active=%d, Health=%s", + os.time() - start_time, + cluster_info.total_nodes, + cluster_info.active_nodes, + cluster_info.health)) + + db.sleep(interval_seconds) + end + + return history +end + +local function test_scale_up_on_high_load() + print("\n=== Test 1: Scale Up on High Load ===") + + -- Проверяем начальное состояние + local initial_status = db.execute("SHOW CLUSTER STATUS") + local initial_info = db.json_decode(initial_status) + local initial_nodes = initial_info.total_nodes + print(string.format("Initial cluster size: %d nodes", initial_nodes)) + + -- Запускаем мониторинг в фоне + local monitor_thread = coroutine.create(function() + return monitor_cluster_size(5, 180) -- мониторим 3 минуты + end) + + -- Генерируем высокую нагрузку + local load_results = generate_test_load(60, 500) -- 60 секунд, 500 QPS + + -- Ожидаем возможного масштабирования + print("Waiting for potential scale up...") + db.sleep(120) -- ждём 2 минуты + + -- Проверяем результат + local final_status = db.execute("SHOW CLUSTER STATUS") + local final_info = db.json_decode(final_status) + local final_nodes = final_info.total_nodes + + print(string.format("Final cluster size: %d nodes", final_nodes)) + + if final_nodes > initial_nodes then + print("✅ Scale Up successful: cluster expanded from " .. + initial_nodes .. " to " .. final_nodes .. " nodes") + return true + else + print("⚠️ No scale up detected (may be at max capacity or cooldown)") + return false + end +end + +local function test_scale_down_on_low_load() + print("\n=== Test 2: Scale Down on Low Load ===") + + local initial_status = db.execute("SHOW CLUSTER STATUS") + local initial_info = db.json_decode(initial_status) + local initial_nodes = initial_info.total_nodes + + if initial_nodes <= 1 then + print("⚠️ Cannot test scale down: only one node in cluster") + return false + end + + print(string.format("Initial cluster size: %d nodes", initial_nodes)) + + -- Ждём снижения нагрузки + print("Waiting for load to decrease and scale down...") + db.sleep(300) -- ждём 5 минут + + local final_status = db.execute("SHOW CLUSTER STATUS") + local final_info = db.json_decode(final_status) + local final_nodes = final_info.total_nodes + + print(string.format("Final cluster size: %d nodes", final_nodes)) + + if final_nodes < initial_nodes then + print("✅ Scale Down successful: cluster reduced from " .. + initial_nodes .. " to " .. final_nodes .. " nodes") + return true + else + print("⚠️ No scale down detected (may be at min capacity or cooldown)") + return false + end +end + +local function test_predictive_scaling() + print("\n=== Test 3: Predictive Scaling ===") + + -- Включаем прогнозирование + db.execute("SET AUTOSCALING PREDICTIVE true") + + -- Создаём растущую нагрузку + print("Generating increasing load pattern...") + + local function generate_ramp_load(initial_qps, final_qps, duration_seconds) + local step_duration = duration_seconds / 10 + for step = 0, 9 do + local target_qps = initial_qps + (final_qps - initial_qps) * step / 10 + print(string.format("Step %d/10: %.2f QPS", step + 1, target_qps)) + generate_test_load(step_duration, target_qps) + end + end + + generate_ramp_load(50, 600, 120) + + -- Проверяем, произошло ли масштабирование до достижения пика + local scaling_history = db.execute("SHOW AUTOSCALING HISTORY") + local history = db.json_decode(scaling_history) + + local scaled_before_peak = false + for _, event in ipairs(history) do + if event.decision == "scale_up" then + local load_at_scale = event.avg_load + if load_at_scale < 0.85 then -- Масштабирование до достижения 85% нагрузки + scaled_before_peak = true + break + end + end + end + + if scaled_before_peak then + print("✅ Predictive scaling successful: scaled before reaching peak load") + return true + else + print("⚠️ Predictive scaling may not be working optimally") + return false + end +end + +local function test_autoscaling_metrics() + print("\n=== Test 4: Autoscaling Metrics ===") + + local metrics = db.execute("SHOW AUTOSCALING METRICS") + local metrics_data = db.json_decode(metrics) + + print("Current autoscaling metrics:") + print(string.format(" Current nodes: %d", metrics_data.current_nodes or 0)) + print(string.format(" Min nodes: %d", metrics_data.min_nodes or 0)) + print(string.format(" Max nodes: %d", metrics_data.max_nodes or 0)) + print(string.format(" Last scale up: %s", metrics_data.last_scale_up or "never")) + print(string.format(" Last scale down: %s", metrics_data.last_scale_down or "never")) + print(string.format(" Evaluation count: %d", metrics_data.evaluation_count or 0)) + print(string.format(" Predictive enabled: %s", tostring(metrics_data.predictive_enabled))) + + -- Проверяем наличие критических метрик + local required_fields = { + "current_nodes", "min_nodes", "max_nodes", + "evaluation_count", "predictive_enabled" + } + + for _, field in ipairs(required_fields) do + if metrics_data[field] == nil then + print("❌ Missing metric: " .. field) + return false + end + end + + print("✅ All required metrics present") + return true +end + +local function test_cooldown_behavior() + print("\n=== Test 5: Cooldown Behavior ===") + + -- Быстро генерируем две всплески нагрузки + for i = 1, 2 do + print(string.format("Load spike %d/2", i)) + generate_test_load(30, 800) + db.sleep(10) -- Короткая пауза между всплесками + end + + local history = db.execute("SHOW AUTOSCALING HISTORY") + local history_data = db.json_decode(history) + + -- Проверяем, что масштабирование не происходило слишком часто + local scale_ups = {} + for _, event in ipairs(history_data) do + if event.decision == "scale_up" then + table.insert(scale_ups, event.timestamp) + end + end + + local cooldown_ok = true + for i = 2, #scale_ups do + local time_diff = scale_ups[i] - scale_ups[i-1] + if time_diff < 300 then -- 5 минут cooldown + cooldown_ok = false + print(string.format("⚠️ Scale ups too close: %d seconds apart", time_diff)) + end + end + + if cooldown_ok then + print("✅ Cooldown mechanism working properly") + else + print("❌ Cooldown mechanism may be misconfigured") + end + + return cooldown_ok +end + +local function test_node_selection_for_scale_down() + print("\n=== Test 6: Node Selection for Scale Down ===") + + -- Получаем нагрузку на узлах + local nodes_load = db.execute("SHOW NODES LOAD") + local load_data = db.json_decode(nodes_load) + + print("Node load distribution:") + local loads = {} + for _, node in ipairs(load_data) do + table.insert(loads, node.load) + print(string.format(" %s: %.2f%%", node.id, node.load * 100)) + end + + -- Сортируем узлы по нагрузке + table.sort(load_data, function(a, b) return a.load < b.load end) + + -- Наименее загруженный узел должен быть кандидатом на удаление + local lowest_load_node = load_data[1] + print(string.format("Lowest load node: %s (%.2f%%)", + lowest_load_node.id, lowest_load_node.load * 100)) + + -- Запрашиваем кандидата на удаление у системы + local scale_down_candidate = db.execute("SHOW SCALE_DOWN_CANDIDATE") + local candidate = db.json_decode(scale_down_candidate) + + if candidate.id == lowest_load_node.id then + print("✅ Correct node selected for scale down") + return true + else + print(string.format("❌ Wrong node selected. Expected: %s, Got: %s", + lowest_load_node.id, candidate.id)) + return false + end +end + +-- Функция очистки +local function cleanup() + print("\n=== Cleanup ===") + db.execute("DROP DATABASE IF EXISTS autoscale_test") + print("Test database removed") +end + +-- Запуск всех тестов +local function run_all_autoscaling_tests() + print("╔══════════════════════════════════════════════════════════════╗") + print("║ AUTOSCALING TESTS SUITE ║") + print("╚══════════════════════════════════════════════════════════════╝") + + setup_test_environment() + + local results = { + test_scale_up_on_high_load(), + test_scale_down_on_low_load(), + test_predictive_scaling(), + test_autoscaling_metrics(), + test_cooldown_behavior(), + test_node_selection_for_scale_down() + } + + -- Подведение итогов + print("\n╔══════════════════════════════════════════════════════════════╗") + print("║ TEST RESULTS ║") + print("╚══════════════════════════════════════════════════════════════╝") + + local passed = 0 + for i, result in ipairs(results) do + local status = result and "✅ PASSED" or "⚠️ SKIPPED/FAILED" + print(string.format("Test %d: %s", i, status)) + if result then passed = passed + 1 end + end + + print(string.format("\nTotal: %d/%d tests passed", passed, #results)) + + cleanup() + + return passed == #results +end + +-- Запуск +return run_all_autoscaling_tests() diff --git a/tests/backpressure_test.lua b/tests/backpressure_test.lua new file mode 100644 index 0000000..a13fe83 --- /dev/null +++ b/tests/backpressure_test.lua @@ -0,0 +1,342 @@ +-- tests/backpressure_test.lua +-- Тестирование механизма backpressure при перегрузке + +local function setup_backpressure_test() + print("=== Setup Backpressure Test ===") + + db.execute("CREATE DATABASE IF NOT EXISTS backpressure_test") + db.execute("USE backpressure_test") + + db.execute([[ + CREATE COLLECTION IF NOT EXISTS test_collection + WITH SETTINGS { + max_documents = 10000000 + } + ]]) + + -- Включаем backpressure с агрессивными настройками для теста + db.execute([[ + SET BACKPRESSURE CONFIG { + enabled = true, + cpu_threshold = 0.50, + memory_threshold = 0.60, + queue_size_threshold = 100, + connection_threshold = 50, + low_delay_ms = 50, + medium_reject_prob = 30, + high_reject_prob = 70 + } + ]]) + + return true +end + +local function test_backpressure_level_none() + print("\n=== Test 1: Backpressure Level NONE ===") + + -- При нормальной нагрузке + local start_time = os.clock() + local success_count = 0 + local reject_count = 0 + + for i = 1, 100 do + local success, err = pcall(function() + return db.execute(string.format([[ + INSERT INTO test_collection VALUES { + _id = "doc_%d", + value = %d, + timestamp = %d + } + ]], i, i, os.time() * 1000)) + end) + + if success then + success_count = success_count + 1 + else + reject_count = reject_count + 1 + if string.find(err, "backpressure") then + print("Unexpected rejection: " .. err) + end + end + end + + local duration = os.clock() - start_time + + print(string.format("Success: %d, Rejected: %d, Duration: %.3fs", + success_count, reject_count, duration)) + + if reject_count == 0 then + print("✅ No rejections under normal load") + return true + else + print("❌ Unexpected rejections: " .. reject_count) + return false + end +end + +local function simulate_backpressure_condition() + print("Simulating high load condition...") + + -- Симулируем высокую нагрузку + local threads = {} + for i = 1, 100 do + table.insert(threads, coroutine.create(function() + for j = 1, 100 do + local success = pcall(function() + db.execute(string.format([[ + SELECT * FROM test_collection WHERE value > %d + ]], math.random(1, 50))) + end) + if not success then + coroutine.yield(false) + end + db.sleep(0.001) + end + coroutine.yield(true) + end)) + end + + -- Запускаем все потоки + for _, thread in ipairs(threads) do + coroutine.resume(thread) + end + + -- Ждём, пока сработает backpressure + db.sleep(5) + + return true +end + +local function test_backpressure_level_medium() + print("\n=== Test 2: Backpressure Level MEDIUM ===") + + simulate_backpressure_condition() + + local status = db.execute("SHOW BACKPRESSURE STATUS") + local bp_status = db.json_decode(status) + + print(string.format("Current backpressure level: %s", bp_status.current_level)) + print(string.format("Write allowed: %s", tostring(bp_status.write_allowed))) + print(string.format("Read allowed: %s", tostring(bp_status.read_allowed))) + print(string.format("Reject probability: %d%%", bp_status.reject_probability or 0)) + + -- Тестируем процент отказов + local total_requests = 500 + local rejected = 0 + + for i = 1, total_requests do + local success, err = pcall(function() + return db.execute("INSERT INTO test_collection VALUES {_id = 'test_" .. i .. "', value = " .. i .. "}") + end) + + if not success and string.find(err or "", "rejected") then + rejected = rejected + 1 + end + end + + local reject_rate = (rejected / total_requests) * 100 + print(string.format("Actual reject rate: %.1f%% (expected: ~%d%%)", + reject_rate, bp_status.reject_probability or 30)) + + -- Проверяем, что reject_rate в разумных пределах + local expected_reject = bp_status.reject_probability or 30 + if math.abs(reject_rate - expected_reject) < 20 then + print("✅ Rejection rate within acceptable range") + return true + else + print("❌ Rejection rate outside expected range") + return false + end +end + +local function test_backpressure_with_delay() + print("\n=== Test 3: Backpressure with Delay ===") + + -- Замеряем время выполнения при backpressure + local start_time = os.clock() + local delayed_requests = 0 + + for i = 1, 50 do + local req_start = os.clock() + db.execute("SELECT * FROM test_collection LIMIT 10") + local req_end = os.clock() + + if req_end - req_start > 0.05 then -- больше 50ms задержки + delayed_requests = delayed_requests + 1 + end + end + + local total_duration = os.clock() - start_time + local avg_delay = total_duration / 50 * 1000 -- в миллисекундах + + print(string.format("Avg request time: %.2f ms", avg_delay)) + print(string.format("Requests with >50ms delay: %d/%d", delayed_requests, 50)) + + if avg_delay > 40 then + print("✅ Backpressure delays detected as expected") + return true + else + print("⚠️ Lower than expected delays") + return false + end +end + +local function test_backpressure_recovery() + print("\n=== Test 4: Backpressure Recovery ===") + + -- Даём системе восстановиться + print("Waiting for system recovery...") + db.sleep(30) + + local status = db.execute("SHOW BACKPRESSURE STATUS") + local bp_status = db.json_decode(status) + + print(string.format("Recovered backpressure level: %s", bp_status.current_level)) + + -- Проверяем, что запросы снова проходят + local success_count = 0 + for i = 1, 100 do + local success = pcall(function() + return db.execute("INSERT INTO test_collection VALUES {_id = 'recovery_" .. i .. "', value = " .. i .. "}") + end) + if success then success_count = success_count + 1 end + end + + print(string.format("Successful writes after recovery: %d/100", success_count)) + + if success_count > 95 then + print("✅ System recovered successfully") + return true + else + print("❌ Recovery incomplete: " .. success_count .. " successes") + return false + end +end + +local function test_backpressure_metrics() + print("\n=== Test 5: Backpressure Metrics ===") + + local metrics = db.execute("SHOW BACKPRESSURE METRICS") + local metrics_data = db.json_decode(metrics) + + print("Backpressure metrics:") + for key, value in pairs(metrics_data) do + print(string.format(" %s: %s", key, tostring(value))) + end + + local required_fields = { + "current_level", "write_allowed", "read_allowed", + "rejected_count", "delayed_count", "cpu_threshold" + } + + for _, field in ipairs(required_fields) do + if metrics_data[field] == nil then + print("❌ Missing metric: " .. field) + return false + end + end + + print("✅ All required metrics present") + return true +end + +local function test_backpressure_burst_handling() + print("\n=== Test 6: Burst Handling ===") + + -- Генерируем burst запросов + local burst_size = 1000 + local start_time = os.clock() + local results = {} + + -- Используем coroutines для параллельных запросов + local handlers = {} + for i = 1, burst_size do + table.insert(handlers, coroutine.create(function() + local success = pcall(function() + return db.execute(string.format([[ + INSERT INTO test_collection VALUES { + _id = "burst_%d_%d", + value = %d, + timestamp = %d + } + ]], os.time(), i, i, os.time() * 1000) + end) + coroutine.yield(success) + end)) + end + + -- Запускаем все handlers + for _, handler in ipairs(handlers) do + coroutine.resume(handler) + end + + -- Собираем результаты + local success_count = 0 + for _, handler in ipairs(handlers) do + local success = coroutine.resume(handler) + if success then + local result = coroutine.status(handler) == "dead" and true or false + if result then success_count = success_count + 1 end + end + end + + local duration = os.clock() - start_time + local qps = burst_size / duration + + print(string.format("Burst: %d requests in %.2fs (%.0f QPS)", + burst_size, duration, qps)) + print(string.format("Successful: %d, Failed: %d", + success_count, burst_size - success_count)) + + if qps > 100 then + print("✅ Backpressure handling burst correctly") + return true + else + print("⚠️ Lower than expected QPS") + return false + end +end + +local function cleanup_backpressure() + print("\n=== Cleanup ===") + db.execute("DROP DATABASE IF EXISTS backpressure_test") + -- Сбрасываем backpressure настройки + db.execute("SET BACKPRESSURE CONFIG DEFAULT") + print("Cleanup completed") +end + +local function run_all_backpressure_tests() + print("╔══════════════════════════════════════════════════════════════╗") + print("║ BACKPRESSURE TESTS SUITE ║") + print("╚══════════════════════════════════════════════════════════════╝") + + setup_backpressure_test() + + local results = { + test_backpressure_level_none(), + test_backpressure_level_medium(), + test_backpressure_with_delay(), + test_backpressure_recovery(), + test_backpressure_metrics(), + test_backpressure_burst_handling() + } + + print("\n╔══════════════════════════════════════════════════════════════╗") + print("║ TEST RESULTS ║") + print("╚══════════════════════════════════════════════════════════════╝") + + local passed = 0 + for i, result in ipairs(results) do + local status = result and "✅ PASSED" or "⚠️ SKIPPED/FAILED" + print(string.format("Test %d: %s", i, status)) + if result then passed = passed + 1 end + end + + print(string.format("\nTotal: %d/%d tests passed", passed, #results)) + + cleanup_backpressure() + + return passed == #results +end + +return run_all_backpressure_tests() diff --git a/tests/backup_test.lua b/tests/backup_test.lua new file mode 100644 index 0000000..922a49d --- /dev/null +++ b/tests/backup_test.lua @@ -0,0 +1,349 @@ +-- tests/backup_test.lua +-- Тестирование бэкапов и восстановления + +local function setup_backup_test() + print("=== Setup Backup Test ===") + + db.execute("CREATE DATABASE IF NOT EXISTS backup_test") + db.execute("USE backup_test") + + db.execute([[ + CREATE COLLECTION IF NOT EXISTS users + ]]) + + -- Вставляем тестовые данные + for i = 1, 100 do + db.execute(string.format([[ + INSERT INTO users VALUES { + _id = "user_%d", + name = "User %d", + email = "user%d@test.com", + score = %d, + created_at = %d + } + ]], i, i, i, math.random(1, 1000), os.time() * 1000)) + end + + print("Inserted 100 test documents") + + -- Включаем планировщик бэкапов + db.execute([[ + SET BACKUP SCHEDULE daily_backup + WITH CRON "0 0 * * *" + TYPE full + RETENTION 7 + ]]) + + return true +end + +local function test_full_backup() + print("\n=== Test 1: Full Backup ===") + + -- Запускаем полный бэкап + local result = db.execute("BACKUP DATABASE backup_test TO full_backup.msgpack") + print("Backup result: " .. tostring(result)) + + -- Проверяем, что файл бэкапа создан + local backup_info = db.execute("SHOW BACKUP INFO") + local info = db.json_decode(backup_info) + + print(string.format("Backup ID: %s", info.id or "unknown")) + print(string.format("Backup size: %d bytes", info.size_bytes or 0)) + print(string.format("Backup time: %s", info.end_time or "unknown")) + + if info.id and info.size_bytes > 0 then + print("✅ Full backup completed successfully") + return info.id + else + print("❌ Full backup failed") + return nil + end +end + +local function test_incremental_backup() + print("\n=== Test 2: Incremental Backup ===") + + -- Добавляем новые документы после полного бэкапа + for i = 101, 150 do + db.execute(string.format([[ + INSERT INTO users VALUES { + _id = "user_%d", + name = "New User %d", + email = "newuser%d@test.com", + score = %d, + created_at = %d + } + ]], i, i, i, math.random(1, 1000), os.time() * 1000)) + end + + print("Added 50 new documents") + + -- Запускаем инкрементальный бэкап + local result = db.execute("BACKUP DATABASE backup_test INCREMENTAL TO inc_backup.msgpack") + print("Incremental backup result: " .. tostring(result)) + + local backup_info = db.execute("SHOW BACKUP INFO inc_backup") + local info = db.json_decode(backup_info) + + if info and info.type == "incremental" then + print(string.format("Incremental backup: %d entries", info.entries_count or 0)) + print("✅ Incremental backup completed successfully") + return true + else + print("❌ Incremental backup failed") + return false + end +end + +local function test_backup_restore() + print("\n=== Test 3: Backup Restore ===") + + -- Создаём новую БД для восстановления + db.execute("CREATE DATABASE IF NOT EXISTS restore_test") + + -- Восстанавливаем из полного бэкапа + local result = db.execute("RESTORE DATABASE restore_test FROM full_backup.msgpack") + print("Restore result: " .. tostring(result)) + + -- Проверяем количество документов + db.execute("USE restore_test") + local count = db.execute("SELECT COUNT(*) FROM users") + local count_data = db.json_decode(count) + + print(string.format("Restored documents: %d", count_data[1].count or 0)) + + if count_data[1].count == 100 then + print("✅ Full restore successful") + return true + else + print("❌ Restore failed: expected 100 documents") + return false + end +end + +local function test_incremental_restore() + print("\n=== Test 4: Incremental Restore ===") + + -- Создаём другую БД для теста инкрементального восстановления + db.execute("CREATE DATABASE IF NOT EXISTS inc_restore_test") + + -- Восстанавливаем сначала полный бэкап + db.execute("USE inc_restore_test") + db.execute("RESTORE DATABASE inc_restore_test FROM full_backup.msgpack") + + local before_count = db.execute("SELECT COUNT(*) FROM users") + local before_data = db.json_decode(before_count) + print(string.format("After full restore: %d documents", before_data[1].count or 0)) + + -- Затем применяем инкрементальный + db.execute("RESTORE DATABASE inc_restore_test INCREMENTAL FROM inc_backup.msgpack") + + local after_count = db.execute("SELECT COUNT(*) FROM users") + local after_data = db.json_decode(after_count) + print(string.format("After incremental restore: %d documents", after_data[1].count or 0)) + + if after_data[1].count == 150 then + print("✅ Incremental restore successful") + return true + else + print(string.format("❌ Expected 150 documents, got %d", after_data[1].count or 0)) + return false + end +end + +local function test_backup_schedule() + print("\n=== Test 5: Backup Schedule ===") + + local schedules = db.execute("SHOW BACKUP SCHEDULES") + local schedules_data = db.json_decode(schedules) + + print("Current backup schedules:") + for _, schedule in ipairs(schedules_data) do + print(string.format(" %s: cron='%s', type='%s', enabled=%s", + schedule.name, schedule.cron_expr, schedule.type, tostring(schedule.enabled))) + end + + if #schedules_data > 0 then + print("✅ Backup schedules configured correctly") + return true + else + print("⚠️ No backup schedules found") + return false + end +end + +local function test_backup_compression() + print("\n=== Test 6: Backup Compression ===") + + -- Включаем сжатие + db.execute("SET BACKUP COMPRESSION true") + + -- Создаём много данных для бэкапа + for i = 1, 1000 do + db.execute(string.format([[ + INSERT INTO users VALUES { + _id = "compress_%d", + data = "%s" + } + ]], i, string.rep("x", 1000))) + end + + local start_time = os.clock() + db.execute("BACKUP DATABASE backup_test TO compressed_backup.msgpack WITH COMPRESSION") + local duration = os.clock() - start_time + + local backup_info = db.execute("SHOW BACKUP INFO compressed_backup") + local info = db.json_decode(backup_info) + + print(string.format("Compressed backup size: %d bytes", info.size_bytes or 0)) + print(string.format("Compression time: %.2fs", duration)) + + if info.size_bytes and info.size_bytes > 0 then + print("✅ Backup compression working") + return true + else + print("❌ Backup compression failed") + return false + end +end + +local function test_backup_retention() + print("\n=== Test 7: Backup Retention ===") + + -- Создаём несколько старых бэкапов (симуляция) + for i = 1, 10 do + db.execute(string.format("BACKUP DATABASE backup_test TO old_backup_%d.msgpack", i)) + end + + -- Устанавливаем retention на 5 дней + db.execute("SET BACKUP RETENTION 5") + + -- Запускаем очистку + db.execute("CLEANUP BACKUPS") + + local backups = db.execute("SHOW BACKUPS") + local backups_data = db.json_decode(backups) + + print(string.format("Remaining backups after cleanup: %d", #backups_data)) + + -- Проверяем, что не все бэкапы удалены + if #backups_data > 0 then + print("✅ Backup retention working") + return true + else + print("⚠️ All backups cleaned up, retention may be too aggressive") + return false + end +end + +local function test_backup_verify() + print("\n=== Test 8: Backup Verification ===") + + -- Проверяем целостность бэкапа + local result = db.execute("VERIFY BACKUP full_backup.msgpack") + local verify_result = db.json_decode(result) + + print(string.format("Verification result: %s", verify_result.valid and "VALID" or "INVALID")) + print(string.format("Checksum match: %s", tostring(verify_result.checksum_match))) + print(string.format("Corruption detected: %s", tostring(verify_result.corrupted))) + + if verify_result.valid then + print("✅ Backup verification passed") + return true + else + print("❌ Backup verification failed") + return false + end +end + +local function test_backup_parallel() + print("\n=== Test 9: Parallel Backups ===") + + -- Проверяем, что параллельные бэкапы блокируются + local results = {} + + local function run_backup(name) + local success, err = pcall(function() + return db.execute(string.format("BACKUP DATABASE backup_test TO parallel_%s.msgpack", name)) + end) + return success + end + + -- Запускаем 3 параллельных бэкапа + local threads = {} + for i = 1, 3 do + table.insert(threads, coroutine.create(function() + local result = run_backup(tostring(i)) + coroutine.yield(result) + end)) + end + + for _, thread in ipairs(threads) do + coroutine.resume(thread) + end + + local success_count = 0 + for _, thread in ipairs(threads) do + local success = coroutine.resume(thread) + if success then success_count = success_count + 1 end + end + + print(string.format("Parallel backups completed: %d/3 succeeded", success_count)) + + if success_count == 1 then + print("✅ Parallel backups correctly limited to 1") + return true + else + print("⚠️ Parallel backup behavior may need tuning") + return false + end +end + +local function cleanup_backup() + print("\n=== Cleanup ===") + db.execute("DROP DATABASE IF EXISTS backup_test") + db.execute("DROP DATABASE IF EXISTS restore_test") + db.execute("DROP DATABASE IF EXISTS inc_restore_test") + print("Cleanup completed") +end + +local function run_all_backup_tests() + print("╔══════════════════════════════════════════════════════════════╗") + print("║ BACKUP TESTS SUITE ║") + print("╚══════════════════════════════════════════════════════════════╝") + + setup_backup_test() + + local full_backup_id = test_full_backup() + local results = { + full_backup_id ~= nil, + test_incremental_backup(), + test_backup_restore(), + test_incremental_restore(), + test_backup_schedule(), + test_backup_compression(), + test_backup_retention(), + test_backup_verify(), + test_backup_parallel() + } + + print("\n╔══════════════════════════════════════════════════════════════╗") + print("║ TEST RESULTS ║") + print("╚══════════════════════════════════════════════════════════════╝") + + local passed = 0 + for i, result in ipairs(results) do + local status = result and "✅ PASSED" or "⚠️ SKIPPED/FAILED" + print(string.format("Test %d: %s", i, status)) + if result then passed = passed + 1 end + end + + print(string.format("\nTotal: %d/%d tests passed", passed, #results)) + + cleanup_backup() + + return passed == #results +end + +return run_all_backup_tests() diff --git a/tests/load_test.lua b/tests/load_test.lua new file mode 100644 index 0000000..1ca45ea --- /dev/null +++ b/tests/load_test.lua @@ -0,0 +1,243 @@ +-- tests/load_test.lua +-- Комплексное нагрузочное тестирование всех компонентов + +local function run_load_test() + print("╔══════════════════════════════════════════════════════════════╗") + print("║ LOAD TEST SUITE ║") + print("╚══════════════════════════════════════════════════════════════╝") + + local test_results = {} + + -- Функция для замера производительности + local function measure_performance(name, fn, iterations) + print(string.format("\n📊 Measuring: %s (%d iterations)", name, iterations)) + + local times = {} + local errors = 0 + + for i = 1, iterations do + local start = os.clock() + local success, err = pcall(fn) + local duration = os.clock() - start + + if success then + table.insert(times, duration) + else + errors = errors + 1 + print(string.format(" Error #%d: %s", i, tostring(err))) + end + end + + if #times == 0 then + print(string.format(" ❌ All %d iterations failed", iterations)) + return nil + end + + -- Вычисляем статистику + table.sort(times) + local min = times[1] + local max = times[#times] + local sum = 0 + for _, t in ipairs(times) do sum = sum + t end + local avg = sum / #times + local p50 = times[math.floor(#times * 0.5)] + local p95 = times[math.floor(#times * 0.95)] + local p99 = times[math.floor(#times * 0.99)] + + print(string.format([[ + Results: + Success rate: %.1f%% (%d/%d) + Min: %.3f ms + Max: %.3f ms + Avg: %.3f ms + P50: %.3f ms + P95: %.3f ms + P99: %.3f ms + ]], (iterations - errors) / iterations * 100, iterations - errors, iterations, + min * 1000, max * 1000, avg * 1000, p50 * 1000, p95 * 1000, p99 * 1000)) + + return { + success_rate = (iterations - errors) / iterations, + min_ms = min * 1000, + max_ms = max * 1000, + avg_ms = avg * 1000, + p50_ms = p50 * 1000, + p95_ms = p95 * 1000, + p99_ms = p99 * 1000 + } + end + + -- Подготовка + print("\n🔧 Preparing test environment...") + db.execute("CREATE DATABASE IF NOT EXISTS load_test") + db.execute("USE load_test") + + db.execute([[ + CREATE COLLECTION IF NOT EXISTS load_collection + WITH SETTINGS { + max_documents = 1000000, + auto_index_id = true + } + ]]) + + -- Тест 1: Вставка документов + local insert_results = measure_performance("INSERT", function() + local doc = { + _id = string.format("doc_%d_%d", os.time(), math.random(1, 1000000)), + timestamp = os.time() * 1000, + value = math.random(1, 10000), + data = string.rep("x", 100) + } + db.execute(string.format( + "INSERT INTO load_collection VALUES (%s)", + db.json_encode(doc) + )) + end, 1000) + + -- Тест 2: Чтение по ID + local read_results = measure_performance("SELECT BY ID", function() + db.execute(string.format("SELECT * FROM load_collection WHERE _id = 'doc_%d'", + math.random(1, 1000))) + end, 1000) + + -- Тест 3: Обновление + local update_results = measure_performance("UPDATE", function() + db.execute(string.format([[ + UPDATE load_collection + SET value = %d, updated_at = %d + WHERE _id = 'doc_%d' + ]], math.random(1, 10000), os.time() * 1000, math.random(1, 1000))) + end, 1000) + + -- Тест 4: Удаление + local delete_results = measure_performance("DELETE", function() + db.execute(string.format("DELETE FROM load_collection WHERE _id = 'temp_%d'", + math.random(1, 100))) + end, 500) + + -- Тест 5: Сложный запрос с фильтром + local query_results = measure_performance("COMPLEX QUERY", function() + db.execute([[ + SELECT * FROM load_collection + WHERE value > 5000 + AND value < 8000 + LIMIT 50 + ]]) + end, 500) + + -- Тест 6: Агрегация + local agg_results = measure_performance("AGGREGATION", function() + db.execute([[ + SELECT + COUNT(*) as total, + AVG(value) as avg_value, + MIN(value) as min_value, + MAX(value) as max_value + FROM load_collection + WHERE value > 1000 + ]]) + end, 200) + + -- Тест 7: Пакетная вставка + local batch_results = measure_performance("BATCH INSERT", function() + local docs = {} + for i = 1, 10 do + table.insert(docs, { + _id = string.format("batch_%d_%d", os.time(), i), + value = i + }) + end + db.execute(string.format( + "INSERT INTO load_collection VALUES %s", + db.json_encode(docs) + )) + end, 100) + + -- Тест 8: Транзакция + local tx_results = measure_performance("TRANSACTION", function() + db.execute("BEGIN TRANSACTION") + db.execute("INSERT INTO load_collection VALUES {_id = 'tx1', value = 1}") + db.execute("INSERT INTO load_collection VALUES {_id = 'tx2', value = 2}") + db.execute("COMMIT") + end, 100) + + -- Тест 9: Поиск по индексу + db.execute("CREATE INDEX idx_value ON load_collection(value)") + + local index_results = measure_performance("INDEX SEARCH", function() + db.execute("SELECT * FROM load_collection WHERE value = 5000") + end, 500) + + -- Тест 10: Сканирование с лимитом + local scan_results = measure_performance("SCAN WITH LIMIT", function() + db.execute("SELECT * FROM load_collection LIMIT 1000") + end, 100) + + -- Сводка + print("\n╔══════════════════════════════════════════════════════════════╗") + print("║ PERFORMANCE SUMMARY ║") + print("╚══════════════════════════════════════════════════════════════╝") + + local function print_summary(name, results) + if results then + local status = results.success_rate > 0.99 and "✅" or "⚠️" + print(string.format("%s %-15s: avg=%.2fms, p95=%.2fms, success=%.1f%%", + status, name, results.avg_ms, results.p95_ms, results.success_rate * 100)) + else + print(string.format("❌ %-15s: FAILED", name)) + end + end + + print_summary("INSERT", insert_results) + print_summary("SELECT", read_results) + print_summary("UPDATE", update_results) + print_summary("DELETE", delete_results) + print_summary("QUERY", query_results) + print_summary("AGGREGATE", agg_results) + print_summary("BATCH", batch_results) + print_summary("TRANSACTION", tx_results) + print_summary("INDEX SEARCH", index_results) + print_summary("SCAN", scan_results) + + -- Общий вердикт + local success_rate = (insert_results and insert_results.success_rate or 0) + + (read_results and read_results.success_rate or 0) + + (update_results and update_results.success_rate or 0) + + (delete_results and delete_results.success_rate or 0) + + (query_results and query_results.success_rate or 0) + + (agg_results and agg_results.success_rate or 0) + + (batch_results and batch_results.success_rate or 0) + + (tx_results and tx_results.success_rate or 0) + + (index_results and index_results.success_rate or 0) + + (scan_results and scan_results.success_rate or 0) + + success_rate = success_rate / 10 * 100 + + print("\n╔══════════════════════════════════════════════════════════════╗") + print("║ FINAL VERDICT ║") + print("╚══════════════════════════════════════════════════════════════╝") + + if success_rate > 99 then + print("✅ EXCELLENT: All operations performing optimally") + elseif success_rate > 95 then + print("✅ GOOD: Most operations performing well") + elseif success_rate > 90 then + print("⚠️ ACCEPTABLE: Some operations need optimization") + else + print("❌ POOR: Significant performance issues detected") + end + + print(string.format("Overall success rate: %.1f%%", success_rate)) + + -- Очистка + print("\n🧹 Cleaning up...") + db.execute("DROP DATABASE IF EXISTS load_test") + + return success_rate > 90 +end + +-- Запуск всех тестов +local function run_all_tests() + print("🚀 STARTING FULL TEST SUITE\n") + + diff --git a/tests/runtime_limits_test.lua b/tests/runtime_limits_test.lua new file mode 100644 index 0000000..5f7a899 --- /dev/null +++ b/tests/runtime_limits_test.lua @@ -0,0 +1,318 @@ +-- tests/runtime_limits_test.lua +-- Тестирование runtime-ограничений на размер коллекции и документа + +local function setup_limits_test() + print("=== Setup Runtime Limits Test ===") + + db.execute("CREATE DATABASE IF NOT EXISTS limits_test") + db.execute("USE limits_test") + + -- Устанавливаем тестовые ограничения + db.execute([[ + SET RUNTIME_LIMITS { + global_max_doc_size_mb = 1, + global_max_coll_size_mb = 10, + global_max_docs_per_coll = 1000 + } + ]]) + + db.execute([[ + CREATE COLLECTION IF NOT EXISTS test_collection + ]]) + + return true +end + +local function test_max_document_size() + print("\n=== Test 1: Max Document Size ===") + + -- Создаём документ меньше лимита (0.5 MB) + local small_doc = { + _id = "small_doc", + data = string.rep("x", 500 * 1024) -- 500KB + } + + local success, err = pcall(function() + return db.execute(string.format( + "INSERT INTO test_collection VALUES (%s)", + db.json_encode(small_doc) + )) + end) + + if success then + print("✅ Small document (500KB) inserted successfully") + else + print("❌ Small document rejected: " .. tostring(err)) + return false + end + + -- Создаём документ больше лимита (2 MB) + local large_doc = { + _id = "large_doc", + data = string.rep("x", 2 * 1024 * 1024) -- 2MB + } + + success, err = pcall(function() + return db.execute(string.format( + "INSERT INTO test_collection VALUES (%s)", + db.json_encode(large_doc) + )) + end) + + if not success and string.find(err or "", "exceeds limit") then + print("✅ Large document (2MB) correctly rejected") + return true + else + print("❌ Large document should have been rejected: " .. tostring(err)) + return false + end +end + +local function test_max_collection_size() + print("\n=== Test 2: Max Collection Size ===") + + local inserted = 0 + local rejected = 0 + + for i = 1, 20 do + -- Каждый документ ~1MB + local doc = { + _id = string.format("doc_%d", i), + data = string.rep("x", 900 * 1024), -- 900KB + index = i + } + + local success, err = pcall(function() + return db.execute(string.format( + "INSERT INTO test_collection VALUES (%s)", + db.json_encode(doc) + )) + end) + + if success then + inserted = inserted + 1 + print(string.format(" Inserted doc %d, total inserted: %d", i, inserted)) + else + rejected = rejected + 1 + if string.find(err or "", "exceeds limit") then + print(string.format(" Doc %d rejected: collection size limit reached", i)) + end + end + end + + print(string.format("Final: %d inserted, %d rejected", inserted, rejected)) + + -- Должно быть вставлено не более 10 документов (~10MB) + if inserted <= 11 and inserted >= 8 then + print("✅ Collection size limit working correctly") + return true + else + print(string.format("❌ Unexpected number of inserts: %d", inserted)) + return false + end +end + +local function test_max_document_count() + print("\n=== Test 3: Max Document Count ===") + + -- Создаём новую коллекцию для этого теста + db.execute("CREATE COLLECTION IF NOT EXISTS count_test") + + -- Устанавливаем лимит в 50 документов + db.execute([[ + ALTER COLLECTION count_test SET LIMITS { + max_documents = 50 + } + ]]) + + local inserted = 0 + local rejected = 0 + + for i = 1, 60 do + local doc = { + _id = string.format("count_doc_%d", i), + value = i + } + + local success, err = pcall(function() + return db.execute(string.format( + "INSERT INTO count_test VALUES (%s)", + db.json_encode(doc) + )) + end) + + if success then + inserted = inserted + 1 + else + rejected = rejected + 1 + end + end + + print(string.format("Inserted: %d, Rejected: %d", inserted, rejected)) + + if inserted == 50 and rejected == 10 then + print("✅ Document count limit working correctly") + return true + else + print(string.format("❌ Expected 50 inserts, got %d", inserted)) + return false + end + + db.execute("DROP COLLECTION count_test") +end + +local function test_collection_override_limits() + print("\n=== Test 4: Collection Override Limits ===") + + -- Создаём коллекцию с собственными лимитами + db.execute([[ + CREATE COLLECTION override_test + WITH LIMITS { + max_doc_size_mb = 5, + max_docs = 200 + } + ]]) + + -- Проверяем, что можно вставить 4MB документ + local large_doc = { + _id = "large_override", + data = string.rep("y", 4 * 1024 * 1024) + } + + local success, err = pcall(function() + return db.execute(string.format( + "INSERT INTO override_test VALUES (%s)", + db.json_encode(large_doc) + )) + end) + + if not success then + print("❌ Failed to insert 4MB document with 5MB limit: " .. tostring(err)) + return false + end + + print("✅ Collection override limits working correctly") + + db.execute("DROP COLLECTION override_test") + return true +end + +local function test_runtime_limits_metrics() + print("\n=== Test 5: Runtime Limits Metrics ===") + + local metrics = db.execute("SHOW RUNTIME_LIMITS METRICS") + local metrics_data = db.json_decode(metrics) + + print("Runtime limits metrics:") + for key, value in pairs(metrics_data) do + print(string.format(" %s: %s", key, tostring(value))) + end + + local required_fields = { + "rejected_by_size", + "rejected_by_doc_count", + "rejected_by_coll_size", + "global_max_doc_size_mb", + "global_max_coll_size_mb", + "global_max_docs_per_coll", + "enabled" + } + + for _, field in ipairs(required_fields) do + if metrics_data[field] == nil then + print("❌ Missing metric: " .. field) + return false + end + end + + print("✅ All required metrics present") + return true +end + +local function test_dynamic_limit_change() + print("\n=== Test 6: Dynamic Limit Change ===") + + -- Создаём коллекцию + db.execute("CREATE COLLECTION dynamic_test") + + -- Меняем лимиты динамически + db.execute("ALTER COLLECTION dynamic_test SET LIMITS { max_docs = 10 }") + + -- Вставляем 10 документов + for i = 1, 10 do + db.execute(string.format( + "INSERT INTO dynamic_test VALUES {_id = 'dyn_%d'}", i + )) + end + + -- Пытаемся вставить 11-й + local success, err = pcall(function() + return db.execute("INSERT INTO dynamic_test VALUES {_id = 'dyn_11'}") + end) + + if success then + print("❌ Should have rejected after dynamic limit change") + return false + end + + -- Увеличиваем лимит + db.execute("ALTER COLLECTION dynamic_test SET LIMITS { max_docs = 20 }") + + -- Теперь 11-й должен пройти + success, err = pcall(function() + return db.execute("INSERT INTO dynamic_test VALUES {_id = 'dyn_11'}") + end) + + if not success then + print("❌ Should have accepted after limit increase") + return false + end + + print("✅ Dynamic limit change working correctly") + + db.execute("DROP COLLECTION dynamic_test") + return true +end + +local function cleanup_limits() + print("\n=== Cleanup ===") + db.execute("DROP DATABASE IF EXISTS limits_test") + db.execute("SET RUNTIME_LIMITS DEFAULT") + print("Cleanup completed") +end + +local function run_all_runtime_limits_tests() + print("╔══════════════════════════════════════════════════════════════╗") + print("║ RUNTIME LIMITS TESTS SUITE ║") + print("╚══════════════════════════════════════════════════════════════╝") + + setup_limits_test() + + local results = { + test_max_document_size(), + test_max_collection_size(), + test_max_document_count(), + test_collection_override_limits(), + test_runtime_limits_metrics(), + test_dynamic_limit_change() + } + + print("\n╔══════════════════════════════════════════════════════════════╗") + print("║ TEST RESULTS ║") + print("╚══════════════════════════════════════════════════════════════╝") + + local passed = 0 + for i, result in ipairs(results) do + local status = result and "✅ PASSED" or "⚠️ SKIPPED/FAILED" + print(string.format("Test %d: %s", i, status)) + if result then passed = passed + 1 end + end + + print(string.format("\nTotal: %d/%d tests passed", passed, #results)) + + cleanup_limits() + + return passed == #results +end + +return run_all_runtime_limits_tests()