Add built-in storage support from v-hono-storage module: - StorageProvider interface with 4 backends - FileService with HTTP handlers - Multipart upload support - 86 property-based tests - Retry mechanism with exponential backoff Breaking: None (backward compatible)
672 lines
19 KiB
V
672 lines
19 KiB
V
module main
|
|
|
|
import rand
|
|
import time
|
|
import math
|
|
|
|
// ============================================================================
|
|
// Property 9: Retry Mechanism
|
|
// Feature: v-hono-upload-integration, Property 9: Retry Mechanism
|
|
// Validates: Requirements 9.1, 9.2, 9.3
|
|
//
|
|
// *For any* retryable error, the system should retry with exponential backoff,
|
|
// respect configured retry count, and return comprehensive error with retry
|
|
// history when all retries are exhausted.
|
|
// ============================================================================
|
|
|
|
const test_iterations = 100
|
|
|
|
// ============================================================================
|
|
// Type definitions (copied from retry.v and storage_errors.v for standalone testing)
|
|
// ============================================================================
|
|
|
|
// 存储错误类型枚举
|
|
enum StorageErrorKind {
|
|
// 可重试错误
|
|
network_timeout
|
|
service_unavailable
|
|
rate_limited
|
|
connection_reset
|
|
temporary_failure
|
|
// 不可重试错误
|
|
invalid_credentials
|
|
access_denied
|
|
bucket_not_found
|
|
object_not_found
|
|
invalid_object_key
|
|
quota_exceeded
|
|
invalid_config
|
|
bucket_not_empty
|
|
bucket_already_exists
|
|
object_already_exists
|
|
invalid_range
|
|
checksum_mismatch
|
|
// 其他错误
|
|
unknown
|
|
}
|
|
|
|
// 重试尝试记录
|
|
struct RetryAttempt {
|
|
pub:
|
|
attempt_number int
|
|
timestamp i64
|
|
error_message string
|
|
delay_ms int
|
|
}
|
|
|
|
// 存储错误结构
|
|
struct StorageError {
|
|
pub:
|
|
kind StorageErrorKind
|
|
message string
|
|
provider string
|
|
operation string
|
|
http_status int
|
|
retry_count int
|
|
details map[string]string
|
|
pub mut:
|
|
retry_history []RetryAttempt
|
|
}
|
|
|
|
|
|
// 判断错误是否可重试
|
|
fn (e StorageError) is_retryable() bool {
|
|
return e.kind in [
|
|
.network_timeout,
|
|
.service_unavailable,
|
|
.rate_limited,
|
|
.connection_reset,
|
|
.temporary_failure,
|
|
]
|
|
}
|
|
|
|
// 重试配置结构
|
|
struct RetryConfig {
|
|
pub:
|
|
max_retries int = 3
|
|
initial_delay int = 1000
|
|
max_delay int = 30000
|
|
multiplier f64 = 2.0
|
|
jitter bool = true
|
|
jitter_factor f64 = 0.25
|
|
}
|
|
|
|
// 默认重试配置
|
|
fn default_retry_config() RetryConfig {
|
|
return RetryConfig{
|
|
max_retries: 3
|
|
initial_delay: 1000
|
|
max_delay: 30000
|
|
multiplier: 2.0
|
|
jitter: true
|
|
jitter_factor: 0.25
|
|
}
|
|
}
|
|
|
|
// 激进重试配置
|
|
fn aggressive_retry_config() RetryConfig {
|
|
return RetryConfig{
|
|
max_retries: 5
|
|
initial_delay: 500
|
|
max_delay: 10000
|
|
multiplier: 1.5
|
|
jitter: true
|
|
jitter_factor: 0.2
|
|
}
|
|
}
|
|
|
|
// 保守重试配置
|
|
fn conservative_retry_config() RetryConfig {
|
|
return RetryConfig{
|
|
max_retries: 2
|
|
initial_delay: 2000
|
|
max_delay: 60000
|
|
multiplier: 3.0
|
|
jitter: true
|
|
jitter_factor: 0.3
|
|
}
|
|
}
|
|
|
|
// 重试执行结果
|
|
struct RetryResult[T] {
|
|
pub:
|
|
success bool
|
|
value T
|
|
total_retries int
|
|
total_delay int
|
|
final_error StorageError
|
|
history []RetryAttempt
|
|
}
|
|
|
|
// 重试执行器
|
|
struct RetryExecutor {
|
|
config RetryConfig
|
|
}
|
|
|
|
// 创建重试执行器
|
|
fn new_retry_executor(config RetryConfig) RetryExecutor {
|
|
return RetryExecutor{
|
|
config: config
|
|
}
|
|
}
|
|
|
|
// 使用默认配置创建重试执行器
|
|
fn new_default_retry_executor() RetryExecutor {
|
|
return RetryExecutor{
|
|
config: default_retry_config()
|
|
}
|
|
}
|
|
|
|
|
|
// 计算下一次重试的延迟时间
|
|
fn (r RetryExecutor) calculate_delay(attempt int) int {
|
|
// 指数退避: delay = initial_delay * (multiplier ^ attempt)
|
|
base_delay := f64(r.config.initial_delay) * math.pow(r.config.multiplier, f64(attempt))
|
|
|
|
// 限制最大延迟
|
|
mut delay := int(math.min(base_delay, f64(r.config.max_delay)))
|
|
|
|
// 添加随机抖动
|
|
if r.config.jitter && delay > 0 {
|
|
jitter_range := int(f64(delay) * r.config.jitter_factor)
|
|
if jitter_range > 0 {
|
|
jitter := rand.int_in_range(0, jitter_range * 2) or { 0 }
|
|
delay = delay - jitter_range + jitter
|
|
}
|
|
}
|
|
|
|
// 确保延迟不为负
|
|
if delay < 0 {
|
|
delay = 0
|
|
}
|
|
|
|
return delay
|
|
}
|
|
|
|
// 计算不带抖动的基础延迟(用于测试)
|
|
fn calculate_base_delay(config RetryConfig, attempt int) int {
|
|
base_delay := f64(config.initial_delay) * math.pow(config.multiplier, f64(attempt))
|
|
return int(math.min(base_delay, f64(config.max_delay)))
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test infrastructure
|
|
// ============================================================================
|
|
|
|
struct PropertyTestStats {
|
|
mut:
|
|
total_tests int
|
|
passed_tests int
|
|
failed_tests int
|
|
}
|
|
|
|
fn (mut stats PropertyTestStats) run_property_test(test_name string, test_func fn () bool) {
|
|
stats.total_tests++
|
|
print('🔬 ${test_name}... ')
|
|
|
|
if test_func() {
|
|
stats.passed_tests++
|
|
println('✅')
|
|
} else {
|
|
stats.failed_tests++
|
|
println('❌')
|
|
}
|
|
}
|
|
|
|
fn (stats PropertyTestStats) print_summary() {
|
|
println('\n=== Retry Mechanism 属性测试总结 ===')
|
|
println('总测试数: ${stats.total_tests}')
|
|
println('通过: ${stats.passed_tests}')
|
|
println('失败: ${stats.failed_tests}')
|
|
|
|
if stats.failed_tests == 0 {
|
|
println('🎉 所有属性测试通过!')
|
|
} else {
|
|
println('⚠️ 有 ${stats.failed_tests} 个属性测试失败')
|
|
}
|
|
}
|
|
|
|
fn generate_random_int(min int, max int) int {
|
|
return rand.int_in_range(min, max) or { min }
|
|
}
|
|
|
|
fn generate_random_f64(min f64, max f64) f64 {
|
|
return min + rand.f64() * (max - min)
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Property 9.1: Exponential Backoff Delay Calculation
|
|
// For any retry configuration and attempt number, the delay should follow
|
|
// exponential backoff formula: delay = initial_delay * (multiplier ^ attempt)
|
|
// Validates: Requirements 9.1
|
|
// ============================================================================
|
|
fn test_property_9_1_exponential_backoff() bool {
|
|
for _ in 0 .. test_iterations {
|
|
// Generate random config
|
|
initial_delay := generate_random_int(100, 5000)
|
|
max_delay := generate_random_int(10000, 60000)
|
|
multiplier := generate_random_f64(1.5, 3.0)
|
|
|
|
config := RetryConfig{
|
|
max_retries: 5
|
|
initial_delay: initial_delay
|
|
max_delay: max_delay
|
|
multiplier: multiplier
|
|
jitter: false // Disable jitter for deterministic testing
|
|
jitter_factor: 0.0
|
|
}
|
|
|
|
executor := new_retry_executor(config)
|
|
|
|
// Test multiple attempts
|
|
for attempt in 0 .. 5 {
|
|
actual_delay := executor.calculate_delay(attempt)
|
|
expected_delay := calculate_base_delay(config, attempt)
|
|
|
|
// Without jitter, delays should match exactly
|
|
if actual_delay != expected_delay {
|
|
println(' Failed: Attempt ${attempt} expected delay ${expected_delay} but got ${actual_delay}')
|
|
return false
|
|
}
|
|
|
|
// Verify delay doesn't exceed max_delay
|
|
if actual_delay > max_delay {
|
|
println(' Failed: Delay ${actual_delay} exceeds max_delay ${max_delay}')
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 9.2: Delay Increases with Each Attempt
|
|
// For any retry configuration without jitter, delay should increase
|
|
// (or stay at max) with each subsequent attempt
|
|
// Validates: Requirements 9.1
|
|
// ============================================================================
|
|
fn test_property_9_2_delay_increases() bool {
|
|
for _ in 0 .. test_iterations {
|
|
initial_delay := generate_random_int(100, 2000)
|
|
max_delay := generate_random_int(10000, 60000)
|
|
multiplier := generate_random_f64(1.5, 3.0)
|
|
|
|
config := RetryConfig{
|
|
max_retries: 5
|
|
initial_delay: initial_delay
|
|
max_delay: max_delay
|
|
multiplier: multiplier
|
|
jitter: false
|
|
jitter_factor: 0.0
|
|
}
|
|
|
|
executor := new_retry_executor(config)
|
|
|
|
mut prev_delay := 0
|
|
for attempt in 0 .. 5 {
|
|
current_delay := executor.calculate_delay(attempt)
|
|
|
|
// Delay should be >= previous delay (monotonically increasing until max)
|
|
if current_delay < prev_delay {
|
|
println(' Failed: Delay decreased from ${prev_delay} to ${current_delay} at attempt ${attempt}')
|
|
return false
|
|
}
|
|
|
|
prev_delay = current_delay
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Property 9.3: Max Delay is Respected
|
|
// For any retry configuration, delay should never exceed max_delay
|
|
// Validates: Requirements 9.2
|
|
// ============================================================================
|
|
fn test_property_9_3_max_delay_respected() bool {
|
|
for _ in 0 .. test_iterations {
|
|
initial_delay := generate_random_int(1000, 5000)
|
|
max_delay := generate_random_int(5000, 20000)
|
|
multiplier := generate_random_f64(2.0, 4.0)
|
|
|
|
config := RetryConfig{
|
|
max_retries: 10
|
|
initial_delay: initial_delay
|
|
max_delay: max_delay
|
|
multiplier: multiplier
|
|
jitter: true
|
|
jitter_factor: 0.25
|
|
}
|
|
|
|
executor := new_retry_executor(config)
|
|
|
|
// Test many attempts to ensure max_delay is always respected
|
|
for attempt in 0 .. 20 {
|
|
delay := executor.calculate_delay(attempt)
|
|
|
|
// With jitter, delay can vary but should still be bounded
|
|
// Max possible delay with jitter = max_delay + (max_delay * jitter_factor)
|
|
max_possible := max_delay + int(f64(max_delay) * config.jitter_factor)
|
|
|
|
if delay > max_possible {
|
|
println(' Failed: Delay ${delay} exceeds max possible ${max_possible} at attempt ${attempt}')
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 9.4: Jitter Adds Randomness Within Bounds
|
|
// For any retry configuration with jitter enabled, delay should vary
|
|
// within the jitter range
|
|
// Validates: Requirements 9.1
|
|
// ============================================================================
|
|
fn test_property_9_4_jitter_within_bounds() bool {
|
|
for _ in 0 .. test_iterations {
|
|
initial_delay := generate_random_int(1000, 3000)
|
|
max_delay := generate_random_int(30000, 60000)
|
|
multiplier := 2.0
|
|
jitter_factor := generate_random_f64(0.1, 0.4)
|
|
|
|
config := RetryConfig{
|
|
max_retries: 5
|
|
initial_delay: initial_delay
|
|
max_delay: max_delay
|
|
multiplier: multiplier
|
|
jitter: true
|
|
jitter_factor: jitter_factor
|
|
}
|
|
|
|
executor := new_retry_executor(config)
|
|
|
|
for attempt in 0 .. 5 {
|
|
base_delay := calculate_base_delay(config, attempt)
|
|
jitter_range := int(f64(base_delay) * jitter_factor)
|
|
|
|
// Calculate expected bounds
|
|
min_expected := base_delay - jitter_range
|
|
max_expected := base_delay + jitter_range
|
|
|
|
// Test multiple times to verify randomness
|
|
for _ in 0 .. 10 {
|
|
delay := executor.calculate_delay(attempt)
|
|
|
|
// Delay should be within jitter bounds (with some tolerance for edge cases)
|
|
if delay < min_expected - 1 || delay > max_expected + 1 {
|
|
println(' Failed: Delay ${delay} outside jitter bounds [${min_expected}, ${max_expected}] at attempt ${attempt}')
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Property 9.5: Retry Count Configuration is Respected
|
|
// For any retry configuration, the max_retries setting should be respected
|
|
// Validates: Requirements 9.2
|
|
// ============================================================================
|
|
fn test_property_9_5_retry_count_respected() bool {
|
|
for _ in 0 .. test_iterations {
|
|
max_retries := generate_random_int(1, 10)
|
|
|
|
config := RetryConfig{
|
|
max_retries: max_retries
|
|
initial_delay: 100
|
|
max_delay: 10000
|
|
multiplier: 2.0
|
|
jitter: false
|
|
jitter_factor: 0.0
|
|
}
|
|
|
|
// Verify config stores the correct max_retries
|
|
if config.max_retries != max_retries {
|
|
println(' Failed: Config max_retries ${config.max_retries} != expected ${max_retries}')
|
|
return false
|
|
}
|
|
|
|
// Verify executor uses the config
|
|
executor := new_retry_executor(config)
|
|
if executor.config.max_retries != max_retries {
|
|
println(' Failed: Executor config max_retries ${executor.config.max_retries} != expected ${max_retries}')
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 9.6: Default Config Has Sensible Values
|
|
// The default retry configuration should have reasonable default values
|
|
// Validates: Requirements 9.2
|
|
// ============================================================================
|
|
fn test_property_9_6_default_config_values() bool {
|
|
config := default_retry_config()
|
|
|
|
// Verify default values
|
|
if config.max_retries != 3 {
|
|
println(' Failed: Default max_retries should be 3 but got ${config.max_retries}')
|
|
return false
|
|
}
|
|
|
|
if config.initial_delay != 1000 {
|
|
println(' Failed: Default initial_delay should be 1000 but got ${config.initial_delay}')
|
|
return false
|
|
}
|
|
|
|
if config.max_delay != 30000 {
|
|
println(' Failed: Default max_delay should be 30000 but got ${config.max_delay}')
|
|
return false
|
|
}
|
|
|
|
if config.multiplier != 2.0 {
|
|
println(' Failed: Default multiplier should be 2.0 but got ${config.multiplier}')
|
|
return false
|
|
}
|
|
|
|
if !config.jitter {
|
|
println(' Failed: Default jitter should be true')
|
|
return false
|
|
}
|
|
|
|
if config.jitter_factor != 0.25 {
|
|
println(' Failed: Default jitter_factor should be 0.25 but got ${config.jitter_factor}')
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Property 9.7: Aggressive Config Has Shorter Delays
|
|
// The aggressive retry configuration should have shorter delays than default
|
|
// Validates: Requirements 9.2
|
|
// ============================================================================
|
|
fn test_property_9_7_aggressive_config() bool {
|
|
default_config := default_retry_config()
|
|
aggressive_config := aggressive_retry_config()
|
|
|
|
// Aggressive should have more retries
|
|
if aggressive_config.max_retries <= default_config.max_retries {
|
|
println(' Failed: Aggressive max_retries should be > default')
|
|
return false
|
|
}
|
|
|
|
// Aggressive should have shorter initial delay
|
|
if aggressive_config.initial_delay >= default_config.initial_delay {
|
|
println(' Failed: Aggressive initial_delay should be < default')
|
|
return false
|
|
}
|
|
|
|
// Aggressive should have shorter max delay
|
|
if aggressive_config.max_delay >= default_config.max_delay {
|
|
println(' Failed: Aggressive max_delay should be < default')
|
|
return false
|
|
}
|
|
|
|
// Aggressive should have smaller multiplier
|
|
if aggressive_config.multiplier >= default_config.multiplier {
|
|
println(' Failed: Aggressive multiplier should be < default')
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 9.8: Conservative Config Has Longer Delays
|
|
// The conservative retry configuration should have longer delays than default
|
|
// Validates: Requirements 9.2
|
|
// ============================================================================
|
|
fn test_property_9_8_conservative_config() bool {
|
|
default_config := default_retry_config()
|
|
conservative_config := conservative_retry_config()
|
|
|
|
// Conservative should have fewer retries
|
|
if conservative_config.max_retries >= default_config.max_retries {
|
|
println(' Failed: Conservative max_retries should be < default')
|
|
return false
|
|
}
|
|
|
|
// Conservative should have longer initial delay
|
|
if conservative_config.initial_delay <= default_config.initial_delay {
|
|
println(' Failed: Conservative initial_delay should be > default')
|
|
return false
|
|
}
|
|
|
|
// Conservative should have longer max delay
|
|
if conservative_config.max_delay <= default_config.max_delay {
|
|
println(' Failed: Conservative max_delay should be > default')
|
|
return false
|
|
}
|
|
|
|
// Conservative should have larger multiplier
|
|
if conservative_config.multiplier <= default_config.multiplier {
|
|
println(' Failed: Conservative multiplier should be > default')
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Property 9.9: Delay is Non-Negative
|
|
// For any retry configuration and attempt, delay should never be negative
|
|
// Validates: Requirements 9.1
|
|
// ============================================================================
|
|
fn test_property_9_9_delay_non_negative() bool {
|
|
for _ in 0 .. test_iterations {
|
|
// Test with various configurations including edge cases
|
|
configs := [
|
|
RetryConfig{
|
|
max_retries: 5
|
|
initial_delay: 1
|
|
max_delay: 100
|
|
multiplier: 1.1
|
|
jitter: true
|
|
jitter_factor: 0.5
|
|
},
|
|
RetryConfig{
|
|
max_retries: 3
|
|
initial_delay: 0
|
|
max_delay: 1000
|
|
multiplier: 2.0
|
|
jitter: true
|
|
jitter_factor: 0.25
|
|
},
|
|
RetryConfig{
|
|
max_retries: 10
|
|
initial_delay: 10000
|
|
max_delay: 5000 // max_delay < initial_delay
|
|
multiplier: 2.0
|
|
jitter: true
|
|
jitter_factor: 0.25
|
|
},
|
|
]
|
|
|
|
for config in configs {
|
|
executor := new_retry_executor(config)
|
|
|
|
for attempt in 0 .. 10 {
|
|
delay := executor.calculate_delay(attempt)
|
|
|
|
if delay < 0 {
|
|
println(' Failed: Delay ${delay} is negative at attempt ${attempt}')
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 9.10: First Attempt Has Initial Delay
|
|
// For any retry configuration without jitter, the first attempt (attempt 0)
|
|
// should have delay equal to initial_delay
|
|
// Validates: Requirements 9.1
|
|
// ============================================================================
|
|
fn test_property_9_10_first_attempt_delay() bool {
|
|
for _ in 0 .. test_iterations {
|
|
initial_delay := generate_random_int(100, 5000)
|
|
|
|
config := RetryConfig{
|
|
max_retries: 5
|
|
initial_delay: initial_delay
|
|
max_delay: 60000
|
|
multiplier: 2.0
|
|
jitter: false
|
|
jitter_factor: 0.0
|
|
}
|
|
|
|
executor := new_retry_executor(config)
|
|
first_delay := executor.calculate_delay(0)
|
|
|
|
// First attempt delay should equal initial_delay (multiplier^0 = 1)
|
|
if first_delay != initial_delay {
|
|
println(' Failed: First attempt delay ${first_delay} != initial_delay ${initial_delay}')
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
fn main() {
|
|
println('🚀 开始 Retry Mechanism 属性测试...')
|
|
println('Feature: v-hono-upload-integration, Property 9: Retry Mechanism')
|
|
println('Validates: Requirements 9.1, 9.2, 9.3')
|
|
println('每个属性测试运行 ${test_iterations} 次迭代\n')
|
|
|
|
rand.seed([u32(time.now().unix()), u32(98765)])
|
|
|
|
mut stats := PropertyTestStats{}
|
|
|
|
// Run property tests
|
|
stats.run_property_test('Property 9.1: Exponential Backoff Delay Calculation', test_property_9_1_exponential_backoff)
|
|
stats.run_property_test('Property 9.2: Delay Increases with Each Attempt', test_property_9_2_delay_increases)
|
|
stats.run_property_test('Property 9.3: Max Delay is Respected', test_property_9_3_max_delay_respected)
|
|
stats.run_property_test('Property 9.4: Jitter Adds Randomness Within Bounds', test_property_9_4_jitter_within_bounds)
|
|
stats.run_property_test('Property 9.5: Retry Count Configuration is Respected', test_property_9_5_retry_count_respected)
|
|
stats.run_property_test('Property 9.6: Default Config Has Sensible Values', test_property_9_6_default_config_values)
|
|
stats.run_property_test('Property 9.7: Aggressive Config Has Shorter Delays', test_property_9_7_aggressive_config)
|
|
stats.run_property_test('Property 9.8: Conservative Config Has Longer Delays', test_property_9_8_conservative_config)
|
|
stats.run_property_test('Property 9.9: Delay is Non-Negative', test_property_9_9_delay_non_negative)
|
|
stats.run_property_test('Property 9.10: First Attempt Has Initial Delay', test_property_9_10_first_attempt_delay)
|
|
|
|
// Print summary
|
|
stats.print_summary()
|
|
|
|
// Exit with error code if any tests failed
|
|
if stats.failed_tests > 0 {
|
|
exit(1)
|
|
}
|
|
}
|