|
345 | 345 | end |
346 | 346 | end |
347 | 347 |
|
| 348 | + describe '#handle_timed_out_vm' do |
| 349 | + before do |
| 350 | + expect(subject).not_to be_nil |
| 351 | + end |
| 352 | + |
| 353 | + before(:each) do |
| 354 | + redis_connection_pool.with do |redis| |
| 355 | + create_pending_vm(pool, vm, redis) |
| 356 | + config[:config]['max_vm_retries'] = 3 |
| 357 | + end |
| 358 | + end |
| 359 | + |
| 360 | + context 'without request_id' do |
| 361 | + it 'moves VM to completed queue and returns error' do |
| 362 | + redis_connection_pool.with do |redis| |
| 363 | + redis.hset("vmpooler__vm__#{vm}", 'open_socket_error', 'connection failed') |
| 364 | + result = subject.handle_timed_out_vm(vm, pool, redis) |
| 365 | + |
| 366 | + expect(redis.sismember("vmpooler__pending__#{pool}", vm)).to be(false) |
| 367 | + expect(redis.sismember("vmpooler__completed__#{pool}", vm)).to be(true) |
| 368 | + expect(result).to eq('connection failed') |
| 369 | + end |
| 370 | + end |
| 371 | + end |
| 372 | + |
| 373 | + context 'with request_id and transient error' do |
| 374 | + before(:each) do |
| 375 | + redis_connection_pool.with do |redis| |
| 376 | + redis.hset("vmpooler__vm__#{vm}", 'request_id', request_id) |
| 377 | + redis.hset("vmpooler__vm__#{vm}", 'pool_alias', pool) |
| 378 | + redis.hset("vmpooler__odrequest__#{request_id}", 'status', 'pending') |
| 379 | + redis.hset("vmpooler__vm__#{vm}", 'clone_error', 'network timeout') |
| 380 | + redis.hset("vmpooler__vm__#{vm}", 'clone_error_class', 'Timeout::Error') |
| 381 | + end |
| 382 | + end |
| 383 | + |
| 384 | + it 'retries on first failure' do |
| 385 | + redis_connection_pool.with do |redis| |
| 386 | + subject.handle_timed_out_vm(vm, pool, redis) |
| 387 | + |
| 388 | + expect(redis.hget("vmpooler__odrequest__#{request_id}", 'retry_count')).to eq('1') |
| 389 | + expect(redis.zrange('vmpooler__odcreate__task', 0, -1)).to include("#{pool}:#{pool}:1:#{request_id}") |
| 390 | + end |
| 391 | + end |
| 392 | + |
| 393 | + it 'marks as failed after max retries' do |
| 394 | + redis_connection_pool.with do |redis| |
| 395 | + redis.hset("vmpooler__odrequest__#{request_id}", 'retry_count', '3') |
| 396 | + |
| 397 | + subject.handle_timed_out_vm(vm, pool, redis) |
| 398 | + |
| 399 | + expect(redis.hget("vmpooler__odrequest__#{request_id}", 'status')).to eq('failed') |
| 400 | + expect(redis.hget("vmpooler__odrequest__#{request_id}", 'failure_reason')).to eq('Max retry attempts exceeded') |
| 401 | + expect(redis.zrange('vmpooler__odcreate__task', 0, -1)).not_to include("#{pool}:#{pool}:1:#{request_id}") |
| 402 | + end |
| 403 | + end |
| 404 | + end |
| 405 | + |
| 406 | + context 'with request_id and permanent error' do |
| 407 | + before(:each) do |
| 408 | + redis_connection_pool.with do |redis| |
| 409 | + redis.hset("vmpooler__vm__#{vm}", 'request_id', request_id) |
| 410 | + redis.hset("vmpooler__vm__#{vm}", 'pool_alias', pool) |
| 411 | + redis.hset("vmpooler__odrequest__#{request_id}", 'status', 'pending') |
| 412 | + redis.hset("vmpooler__vm__#{vm}", 'clone_error', 'template not found') |
| 413 | + redis.hset("vmpooler__vm__#{vm}", 'clone_error_class', 'RuntimeError') |
| 414 | + end |
| 415 | + end |
| 416 | + |
| 417 | + it 'immediately marks as failed without retrying' do |
| 418 | + redis_connection_pool.with do |redis| |
| 419 | + subject.handle_timed_out_vm(vm, pool, redis) |
| 420 | + |
| 421 | + expect(redis.hget("vmpooler__odrequest__#{request_id}", 'status')).to eq('failed') |
| 422 | + expect(redis.hget("vmpooler__odrequest__#{request_id}", 'failure_reason')).to include('Configuration error') |
| 423 | + expect(redis.zrange('vmpooler__odcreate__task', 0, -1)).not_to include("#{pool}:#{pool}:1:#{request_id}") |
| 424 | + end |
| 425 | + end |
| 426 | + end |
| 427 | + end |
| 428 | + |
| 429 | + describe '#is_permanent_error?' do |
| 430 | + before do |
| 431 | + expect(subject).not_to be_nil |
| 432 | + end |
| 433 | + |
| 434 | + it 'identifies template not found errors as permanent' do |
| 435 | + expect(subject.is_permanent_error?('template not found', 'RuntimeError')).to be(true) |
| 436 | + end |
| 437 | + |
| 438 | + it 'identifies invalid path errors as permanent' do |
| 439 | + expect(subject.is_permanent_error?('invalid path specified', 'ArgumentError')).to be(true) |
| 440 | + end |
| 441 | + |
| 442 | + it 'identifies permission denied errors as permanent' do |
| 443 | + expect(subject.is_permanent_error?('permission denied', 'SecurityError')).to be(true) |
| 444 | + end |
| 445 | + |
| 446 | + it 'identifies ArgumentError class as permanent' do |
| 447 | + expect(subject.is_permanent_error?('some argument error', 'ArgumentError')).to be(true) |
| 448 | + end |
| 449 | + |
| 450 | + it 'identifies network errors as transient' do |
| 451 | + expect(subject.is_permanent_error?('connection timeout', 'Timeout::Error')).to be(false) |
| 452 | + end |
| 453 | + |
| 454 | + it 'identifies socket errors as transient' do |
| 455 | + expect(subject.is_permanent_error?('connection refused', 'Errno::ECONNREFUSED')).to be(false) |
| 456 | + end |
| 457 | + |
| 458 | + it 'returns false for nil inputs' do |
| 459 | + expect(subject.is_permanent_error?(nil, nil)).to be(false) |
| 460 | + expect(subject.is_permanent_error?('error', nil)).to be(false) |
| 461 | + expect(subject.is_permanent_error?(nil, 'Error')).to be(false) |
| 462 | + end |
| 463 | + end |
| 464 | + |
348 | 465 | describe '#move_pending_vm_to_ready' do |
349 | 466 | let(:host) { { 'hostname' => vm }} |
350 | 467 |
|
|
0 commit comments