|
1 | 1 | import asyncio
|
| 2 | +import concurrent.futures |
| 3 | +from concurrent.futures.process import BrokenProcessPool, ProcessPoolExecutor |
2 | 4 | import json
|
3 | 5 | import os
|
4 | 6 | import unittest
|
@@ -387,6 +389,92 @@ async def responder(handler):
|
387 | 389 | assert len(result['diff'][0][1]) == 1024
|
388 | 390 |
|
389 | 391 |
|
| 392 | +class BrokenProcessPoolExecutor(concurrent.futures.Executor): |
| 393 | + "Fake process pool that only raises BrokenProcessPool exceptions." |
| 394 | + submit_count = 0 |
| 395 | + |
| 396 | + def submit(self, fn, *args, **kwargs): |
| 397 | + self.submit_count += 1 |
| 398 | + result = concurrent.futures.Future() |
| 399 | + result.set_exception(BrokenProcessPool( |
| 400 | + 'This pool is broken, yo' |
| 401 | + )) |
| 402 | + return result |
| 403 | + |
| 404 | + |
| 405 | +class ExecutionPoolTestCase(DiffingServerTestCase): |
| 406 | + def fetch_async(self, path, **kwargs): |
| 407 | + "Like AyncHTTPTestCase.fetch, but async." |
| 408 | + url = self.get_url(path) |
| 409 | + return self.http_client.fetch(url, **kwargs) |
| 410 | + |
| 411 | + def test_rebuilds_process_pool_when_broken(self): |
| 412 | + # Get a custom executor that will always fail the first time, but get |
| 413 | + # a real one that will succeed afterward. |
| 414 | + did_get_executor = False |
| 415 | + def get_executor(self, reset=False): |
| 416 | + nonlocal did_get_executor |
| 417 | + if did_get_executor: |
| 418 | + return ProcessPoolExecutor(1) |
| 419 | + else: |
| 420 | + did_get_executor = True |
| 421 | + return BrokenProcessPoolExecutor() |
| 422 | + |
| 423 | + with patch.object(df.DiffHandler, 'get_diff_executor', get_executor): |
| 424 | + response = self.fetch('/html_source_dmp?format=json&' |
| 425 | + f'a=file://{fixture_path("empty.txt")}&' |
| 426 | + f'b=file://{fixture_path("empty.txt")}') |
| 427 | + assert response.code == 200 |
| 428 | + assert did_get_executor == True |
| 429 | + |
| 430 | + def test_diff_returns_error_if_process_pool_repeatedly_breaks(self): |
| 431 | + # Set a custom executor that will always fail. |
| 432 | + def get_executor(self, reset=False): |
| 433 | + return BrokenProcessPoolExecutor() |
| 434 | + |
| 435 | + with patch.object(df.DiffHandler, 'get_diff_executor', get_executor): |
| 436 | + response = self.fetch('/html_source_dmp?format=json&' |
| 437 | + f'a=file://{fixture_path("empty.txt")}&' |
| 438 | + f'b=file://{fixture_path("empty.txt")}') |
| 439 | + self.json_check(response) |
| 440 | + assert response.code == 500 |
| 441 | + |
| 442 | + @tornado.testing.gen_test |
| 443 | + async def test_rebuilds_process_pool_cooperatively(self): |
| 444 | + """ |
| 445 | + Make sure that two parallel diffing failures only cause the process |
| 446 | + pool to be rebuilt once, not multiple times. |
| 447 | + """ |
| 448 | + # Get a custom executor that will always fail the first time, but get |
| 449 | + # a real one that will succeed afterward. |
| 450 | + executor_resets = 0 |
| 451 | + good_executor = ProcessPoolExecutor(1) |
| 452 | + bad_executor = BrokenProcessPoolExecutor() |
| 453 | + def get_executor(self, reset=False): |
| 454 | + nonlocal executor_resets |
| 455 | + if reset: |
| 456 | + executor_resets += 1 |
| 457 | + if executor_resets > 0: |
| 458 | + return good_executor |
| 459 | + else: |
| 460 | + return bad_executor |
| 461 | + |
| 462 | + with patch.object(df.DiffHandler, 'get_diff_executor', get_executor): |
| 463 | + one = self.fetch_async('/html_source_dmp?format=json&' |
| 464 | + f'a=file://{fixture_path("empty.txt")}&' |
| 465 | + f'b=file://{fixture_path("empty.txt")}') |
| 466 | + two = self.fetch_async('/html_source_dmp?format=json&' |
| 467 | + f'a=file://{fixture_path("empty.txt")}&' |
| 468 | + f'b=file://{fixture_path("empty.txt")}') |
| 469 | + response1, response2 = await asyncio.gather(one, two) |
| 470 | + assert response1.code == 200 |
| 471 | + assert response2.code == 200 |
| 472 | + assert executor_resets == 1 |
| 473 | + # Ensure *both* diffs hit the bad executor, so we know we didn't |
| 474 | + # have one reset because only one request hit the bad executor. |
| 475 | + assert bad_executor.submit_count == 2 |
| 476 | + |
| 477 | + |
390 | 478 | def mock_diffing_method(c_body):
|
391 | 479 | return
|
392 | 480 |
|
|
0 commit comments