|
286 | 286 | expect(counts).to all(eq(expected_per_node)) |
287 | 287 | end |
288 | 288 |
|
| 289 | + it 'never returns a node held unhealthy while next_node is called concurrently' do |
| 290 | + unhealthy_node = api_call.instance_variable_get(:@nodes)[1] |
| 291 | + api_call.send(:set_node_healthcheck, unhealthy_node, is_healthy: false) |
| 292 | + |
| 293 | + threads = Array.new(8) do |
| 294 | + Thread.new do |
| 295 | + Array.new(200) { api_call.send(:next_node)[:index] } |
| 296 | + end |
| 297 | + end |
| 298 | + |
| 299 | + results = threads.flat_map(&:value) |
| 300 | + expect(results).not_to include(1) |
| 301 | + expect(results).to include(0).and include(2) |
| 302 | + end |
| 303 | + |
| 304 | + it 'still returns a node when every node is unhealthy under concurrent calls' do |
| 305 | + nodes = api_call.instance_variable_get(:@nodes) |
| 306 | + nodes.each { |node| api_call.send(:set_node_healthcheck, node, is_healthy: false) } |
| 307 | + |
| 308 | + threads = Array.new(8) do |
| 309 | + Thread.new do |
| 310 | + Array.new(50) { api_call.send(:next_node) } |
| 311 | + end |
| 312 | + end |
| 313 | + |
| 314 | + results = threads.flat_map(&:value) |
| 315 | + expect(results.length).to eq(8 * 50) |
| 316 | + expect(results).to all(be_a(Hash)) |
| 317 | + expect(results.map { |n| n[:index] }).to all(be_between(0, nodes.length - 1).inclusive) |
| 318 | + end |
| 319 | + |
289 | 320 | context 'with a single node' do |
290 | 321 | let(:typesense) do |
291 | 322 | Typesense::Client.new( |
|
314 | 345 | expect(node[:last_access_timestamp]).to be_a(Integer) |
315 | 346 | end |
316 | 347 | end |
| 348 | + |
| 349 | + context 'with nearest_node configured' do |
| 350 | + let(:typesense) do |
| 351 | + Typesense::Client.new( |
| 352 | + api_key: 'abcd', |
| 353 | + nearest_node: { host: 'nearestNode', port: 6108, protocol: 'http' }, |
| 354 | + nodes: [ |
| 355 | + { host: 'node0', port: 8108, protocol: 'http' }, |
| 356 | + { host: 'node1', port: 8108, protocol: 'http' }, |
| 357 | + { host: 'node2', port: 8108, protocol: 'http' } |
| 358 | + ], |
| 359 | + connection_timeout_seconds: 10, |
| 360 | + retry_interval_seconds: 0.01, |
| 361 | + log_level: Logger::ERROR |
| 362 | + ) |
| 363 | + end |
| 364 | + |
| 365 | + it 'serializes reads and writes of nearest_node health state under concurrent access' do |
| 366 | + nearest_node = api_call.instance_variable_get(:@nearest_node) |
| 367 | + |
| 368 | + writer_threads = Array.new(4) do |i| |
| 369 | + Thread.new do |
| 370 | + 100.times { api_call.send(:set_node_healthcheck, nearest_node, is_healthy: i.even?) } |
| 371 | + end |
| 372 | + end |
| 373 | + |
| 374 | + reader_threads = Array.new(8) do |
| 375 | + Thread.new do |
| 376 | + Array.new(100) { api_call.send(:next_node) } |
| 377 | + end |
| 378 | + end |
| 379 | + |
| 380 | + writer_threads.each(&:join) |
| 381 | + reader_results = reader_threads.flat_map(&:value) |
| 382 | + |
| 383 | + expect(reader_results).to all(be_a(Hash)) |
| 384 | + expect([true, false]).to include(nearest_node[:is_healthy]) |
| 385 | + expect(nearest_node[:last_access_timestamp]).to be_a(Integer) |
| 386 | + end |
| 387 | + end |
317 | 388 | end |
318 | 389 | end |
0 commit comments