|
1 | 1 | import logging |
2 | 2 | import time |
3 | 3 | import uuid |
4 | | -from datetime import timedelta |
| 4 | +from datetime import datetime, timedelta |
5 | 5 | from threading import Thread |
6 | 6 |
|
7 | 7 | import pytest |
|
11 | 11 | from pytest_django.fixtures import SettingsWrapper |
12 | 12 | from pytest_mock import MockerFixture |
13 | 13 |
|
14 | | -from common.test_tools import AssertMetricFixture |
| 14 | +from common.test_tools.types import AssertMetricFixture |
15 | 15 | from task_processor.decorators import ( |
16 | 16 | TaskHandler, |
17 | 17 | register_recurring_task, |
18 | 18 | register_task_handler, |
19 | 19 | ) |
| 20 | +from task_processor.exceptions import TaskBackoffError |
20 | 21 | from task_processor.models import ( |
21 | 22 | RecurringTask, |
22 | 23 | RecurringTaskRun, |
@@ -500,7 +501,7 @@ def test_run_task_runs_task_and_creates_task_run_object_when_failure( |
500 | 501 | ), |
501 | 502 | ( |
502 | 503 | logging.DEBUG, |
503 | | - f"Running task {task.task_identifier} id={task.id} args=['{msg}'] kwargs={{}}", |
| 504 | + f"Running task {task.task_identifier} id={task.id} args=('{msg}',) kwargs={{}}", |
504 | 505 | ), |
505 | 506 | ( |
506 | 507 | logging.ERROR, |
@@ -636,6 +637,111 @@ def test_run_task_runs_tasks_in_correct_priority( |
636 | 637 | assert task_runs_3[0].task == task_2 |
637 | 638 |
|
638 | 639 |
|
| 640 | +@pytest.mark.parametrize( |
| 641 | + "exception, expected_scheduled_for", |
| 642 | + [ |
| 643 | + (TaskBackoffError(), datetime.fromisoformat("2023-12-08T06:05:57+00:00")), |
| 644 | + ( |
| 645 | + TaskBackoffError( |
| 646 | + delay_until=datetime.fromisoformat("2023-12-08T06:15:52+00:00") |
| 647 | + ), |
| 648 | + datetime.fromisoformat("2023-12-08T06:15:52+00:00"), |
| 649 | + ), |
| 650 | + ], |
| 651 | +) |
| 652 | +@pytest.mark.freeze_time("2023-12-08T06:05:47+00:00") |
| 653 | +@pytest.mark.multi_database |
| 654 | +@pytest.mark.task_processor_mode |
| 655 | +def test_run_task__backoff__persists_expected( |
| 656 | + exception: TaskBackoffError, |
| 657 | + expected_scheduled_for: datetime, |
| 658 | + current_database: str, |
| 659 | + settings: SettingsWrapper, |
| 660 | + caplog: pytest.LogCaptureFixture, |
| 661 | +) -> None: |
| 662 | + # Given |
| 663 | + settings.TASK_BACKOFF_DEFAULT_DELAY_SECONDS = 10 |
| 664 | + |
| 665 | + @register_task_handler() |
| 666 | + def backoff_task() -> None: |
| 667 | + raise exception |
| 668 | + |
| 669 | + task = Task.create( |
| 670 | + backoff_task.task_identifier, |
| 671 | + scheduled_for=timezone.now(), |
| 672 | + args=(), |
| 673 | + priority=TaskPriority.HIGH, |
| 674 | + ) |
| 675 | + task.save(using=current_database) |
| 676 | + |
| 677 | + caplog.set_level(logging.INFO) |
| 678 | + expected_log_message = f"Backoff requested. Task '{backoff_task.task_identifier}' set to retry at {expected_scheduled_for}" |
| 679 | + |
| 680 | + # When |
| 681 | + run_tasks(current_database) |
| 682 | + |
| 683 | + # Then |
| 684 | + assert [ |
| 685 | + record.message for record in caplog.records if record.levelno == logging.INFO |
| 686 | + ] == [expected_log_message] |
| 687 | + task.refresh_from_db(using=current_database) |
| 688 | + assert task.scheduled_for == expected_scheduled_for |
| 689 | + |
| 690 | + |
| 691 | +@pytest.mark.multi_database |
| 692 | +@pytest.mark.task_processor_mode |
| 693 | +def test_run_task__backoff__recurring__raises_expected( |
| 694 | + current_database: str, |
| 695 | +) -> None: |
| 696 | + # Given |
| 697 | + @register_recurring_task(run_every=timedelta(seconds=1)) |
| 698 | + def backoff_task() -> None: |
| 699 | + raise TaskBackoffError() |
| 700 | + |
| 701 | + initialise() |
| 702 | + |
| 703 | + # When & Then |
| 704 | + with pytest.raises(AssertionError) as exc_info: |
| 705 | + run_recurring_tasks(current_database) |
| 706 | + |
| 707 | + assert ( |
| 708 | + str(exc_info.value) |
| 709 | + == "Attempt to back off a recurring task (currently not supported)" |
| 710 | + ) |
| 711 | + |
| 712 | + |
| 713 | +@pytest.mark.multi_database |
| 714 | +@pytest.mark.task_processor_mode |
| 715 | +def test_run_task__backoff__max_num_failures__noop( |
| 716 | + current_database: str, |
| 717 | + caplog: pytest.LogCaptureFixture, |
| 718 | +) -> None: |
| 719 | + # Given |
| 720 | + @register_task_handler() |
| 721 | + def backoff_task() -> None: |
| 722 | + raise TaskBackoffError() |
| 723 | + |
| 724 | + expected_scheduled_for = timezone.now() |
| 725 | + task = Task.create( |
| 726 | + backoff_task.task_identifier, |
| 727 | + scheduled_for=expected_scheduled_for, |
| 728 | + args=(), |
| 729 | + priority=TaskPriority.HIGH, |
| 730 | + ) |
| 731 | + task.num_failures = 4 |
| 732 | + task.save(using=current_database) |
| 733 | + |
| 734 | + caplog.set_level(logging.INFO) |
| 735 | + |
| 736 | + # When |
| 737 | + run_tasks(current_database) |
| 738 | + |
| 739 | + # Then |
| 740 | + task.refresh_from_db(using=current_database) |
| 741 | + assert task.scheduled_for == expected_scheduled_for |
| 742 | + assert not [record for record in caplog.records if record.levelno == logging.INFO] |
| 743 | + |
| 744 | + |
639 | 745 | @pytest.mark.multi_database |
640 | 746 | def test_run_tasks__fails_if_not_in_task_processor_mode( |
641 | 747 | current_database: str, |
|
0 commit comments