Skip to content

Commit 52d15e5

Browse files
authored
Merge branch 'master' into fix/1070-status-modified-on-variable-change
2 parents e148b04 + 45b24b6 commit 52d15e5

File tree

3 files changed

+116
-17
lines changed

3 files changed

+116
-17
lines changed

openwisp_controller/connection/tasks.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,25 @@
1616
_TASK_NAME = "openwisp_controller.connection.tasks.update_config"
1717

1818

19-
def _is_update_in_progress(device_id):
19+
def _is_update_in_progress(device_id, current_task_id=None):
2020
active = current_app.control.inspect().active()
2121
if not active:
2222
return False
2323
# check if there's any other running task before adding it
24+
# exclude the current task by comparing task IDs
2425
for task_list in active.values():
2526
for task in task_list:
26-
if task["name"] == _TASK_NAME and str(device_id) in task["args"]:
27+
if (
28+
task["name"] == _TASK_NAME
29+
and str(device_id) in task["args"]
30+
and task["id"] != current_task_id
31+
):
2732
return True
2833
return False
2934

3035

31-
@shared_task
32-
def update_config(device_id):
36+
@shared_task(bind=True)
37+
def update_config(self, device_id):
3338
"""
3439
Launches the ``update_config()`` operation
3540
of a specific device in the background
@@ -48,7 +53,7 @@ def update_config(device_id):
4853
except ObjectDoesNotExist as e:
4954
logger.warning(f'update_config("{device_id}") failed: {e}')
5055
return
51-
if _is_update_in_progress(device_id):
56+
if _is_update_in_progress(device_id, current_task_id=self.request.id):
5257
return
5358
try:
5459
device_conn = DeviceConnection.get_working_connection(device)

openwisp_controller/connection/tests/test_models.py

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import socket
22
from unittest import mock
33
from unittest.mock import PropertyMock
4+
from uuid import uuid4
45

56
import paramiko
67
from django.contrib.auth.models import ContentType
@@ -1026,20 +1027,56 @@ def _assert_applying_conf_test_command(mocked_exec):
10261027
@mock.patch.object(DeviceConnection, "update_config")
10271028
@mock.patch.object(DeviceConnection, "get_working_connection")
10281029
def test_device_update_config_in_progress(
1029-
self, mocked_get_working_connection, update_config, mocked_sleep
1030+
self, mocked_get_working_connection, mocked_update_config, mocked_sleep
10301031
):
10311032
conf = self._prepare_conf_object()
10321033

1033-
with mock.patch("celery.app.control.Inspect.active") as mocked_active:
1034-
mocked_active.return_value = {
1035-
"task": [{"name": _TASK_NAME, "args": [str(conf.device.pk)]}]
1036-
}
1037-
conf.config = {"general": {"timezone": "UTC"}}
1038-
conf.full_clean()
1039-
conf.save()
1040-
mocked_active.assert_called_once()
1041-
mocked_get_working_connection.assert_not_called()
1042-
update_config.assert_not_called()
1034+
with self.subTest("More than one update_config task active for the device"):
1035+
with mock.patch("celery.app.control.Inspect.active") as mocked_active:
1036+
mocked_active.return_value = {
1037+
"task": [
1038+
{
1039+
"name": _TASK_NAME,
1040+
"args": [str(conf.device.pk)],
1041+
"id": str(uuid4()),
1042+
}
1043+
]
1044+
}
1045+
conf.config = {"general": {"timezone": "UTC"}}
1046+
conf.full_clean()
1047+
conf.save()
1048+
mocked_active.assert_called_once()
1049+
mocked_get_working_connection.assert_not_called()
1050+
mocked_update_config.assert_not_called()
1051+
1052+
Config.objects.update(status="applied")
1053+
mocked_get_working_connection.return_value = (
1054+
conf.device.deviceconnection_set.first()
1055+
)
1056+
with self.subTest("Only one task is active for the device"):
1057+
task_id = str(uuid4())
1058+
with mock.patch(
1059+
"celery.app.control.Inspect.active"
1060+
) as mocked_active, mock.patch(
1061+
"celery.app.task.Context.id",
1062+
new_callable=mock.PropertyMock,
1063+
return_value=task_id,
1064+
):
1065+
mocked_active.return_value = {
1066+
"task": [
1067+
{
1068+
"name": _TASK_NAME,
1069+
"args": [str(conf.device.pk)],
1070+
"id": task_id,
1071+
}
1072+
]
1073+
}
1074+
conf.config = {"general": {"timezone": "Asia/Kolkata"}}
1075+
conf.full_clean()
1076+
conf.save()
1077+
mocked_active.assert_called_once()
1078+
mocked_get_working_connection.assert_called_once()
1079+
mocked_update_config.assert_called_once()
10431080

10441081
@mock.patch("time.sleep")
10451082
@mock.patch.object(DeviceConnection, "update_config")
@@ -1053,8 +1090,15 @@ def test_device_update_config_not_in_progress(
10531090
)
10541091

10551092
with mock.patch("celery.app.control.Inspect.active") as mocked_active:
1093+
# Mock a task running for a different device (args is different)
10561094
mocked_active.return_value = {
1057-
"task": [{"name": _TASK_NAME, "args": ["..."]}]
1095+
"task": [
1096+
{
1097+
"name": _TASK_NAME,
1098+
"args": ["another-device-id"], # Different device
1099+
"id": "different-task-id",
1100+
}
1101+
]
10581102
}
10591103
conf.config = {"general": {"timezone": "UTC"}}
10601104
conf.full_clean()

openwisp_controller/connection/tests/test_tasks.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,56 @@ class TestTasks(CreateConnectionsMixin, TestCase):
2121
"openwisp_controller.connection.base.models.AbstractDeviceConnection.connect"
2222
)
2323

24+
def _get_mocked_celery_active(self, device_id, task_id=None):
25+
return {
26+
"worker1": [
27+
{
28+
"name": tasks._TASK_NAME,
29+
"args": [device_id],
30+
"id": task_id or str(uuid.uuid4()),
31+
}
32+
]
33+
}
34+
35+
def test_is_update_in_progress_same_task(self):
36+
device_id = str(uuid.uuid4())
37+
task_id = str(uuid.uuid4())
38+
with mock.patch(
39+
"celery.app.control.Inspect.active",
40+
return_value=self._get_mocked_celery_active(device_id, task_id),
41+
):
42+
result = tasks._is_update_in_progress(device_id, current_task_id=task_id)
43+
self.assertEqual(result, False)
44+
45+
def test_is_update_in_progress_different_task(self):
46+
device_id = str(uuid.uuid4())
47+
current_task_id = str(uuid.uuid4())
48+
other_task_id = str(uuid.uuid4())
49+
with mock.patch(
50+
"celery.app.control.Inspect.active",
51+
return_value=self._get_mocked_celery_active(device_id, other_task_id),
52+
):
53+
result = tasks._is_update_in_progress(
54+
device_id, current_task_id=current_task_id
55+
)
56+
self.assertEqual(result, True)
57+
58+
def test_is_update_in_progress_no_tasks(self):
59+
device_id = str(uuid.uuid4())
60+
with mock.patch("celery.app.control.Inspect.active", return_value={}):
61+
result = tasks._is_update_in_progress(device_id)
62+
self.assertEqual(result, False)
63+
64+
def test_is_update_in_progress_different_device(self):
65+
device_id = str(uuid.uuid4())
66+
other_device_id = str(uuid.uuid4())
67+
with mock.patch(
68+
"celery.app.control.Inspect.active",
69+
return_value=self._get_mocked_celery_active(other_device_id),
70+
):
71+
result = tasks._is_update_in_progress(device_id)
72+
self.assertEqual(result, False)
73+
2474
@mock.patch("logging.Logger.warning")
2575
@mock.patch("time.sleep")
2676
def test_update_config_missing_config(self, mocked_sleep, mocked_warning):

0 commit comments

Comments
 (0)