Skip to content

Commit e5ed770

Browse files
authored
Merge pull request #162 from modoboa/fix/slow_perf_manual_learning
Enabled asynchronous learning from user using RQ
2 parents ca35d46 + ac929a6 commit e5ed770

File tree

5 files changed

+123
-33
lines changed

5 files changed

+123
-33
lines changed

.github/workflows/plugin.yml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ jobs:
3636
ports:
3737
- 3306/tcp
3838
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
39+
redis:
40+
image: redis
41+
ports:
42+
- 6379/tcp
43+
options: >-
44+
--health-cmd "redis-cli ping"
45+
--health-interval 10s
46+
--health-timeout 5s
47+
--health-retries 5
3948
4049
strategy:
4150
matrix:
@@ -51,7 +60,7 @@ jobs:
5160
python-version: ${{ matrix.python-version }}
5261
- name: Install dependencies
5362
run: |
54-
sudo apt-get update -y && sudo apt-get install -y librrd-dev rrdtool
63+
sudo apt-get update -y && sudo apt-get install -y librrd-dev rrdtool redis-server
5564
python -m pip install --upgrade pip
5665
#pip install -e git+https://github.com/modoboa/modoboa.git#egg=modoboa
5766
pip install -r requirements.txt
@@ -62,6 +71,11 @@ jobs:
6271
python setup.py develop
6372
cd ../modoboa-amavis
6473
python setup.py develop
74+
echo "Testing redis connection"
75+
redis-cli -h $REDIS_HOST -p $REDIS_PORT ping
76+
env:
77+
REDIS_HOST: localhost
78+
REDIS_PORT: ${{ job.services.redis.ports[6379] }}
6579
- name: Install postgres requirements
6680
if: ${{ matrix.database == 'postgres' }}
6781
run: |
@@ -85,7 +99,8 @@ jobs:
8599
MYSQL_HOST: 127.0.0.1
86100
MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} # get randomly assigned published port
87101
MYSQL_USER: root
88-
102+
REDIS_HOST: localhost
103+
REDIS_PORT: ${{ job.services.redis.ports[6379] }}
89104
- name: Test with pytest and coverage
90105
if: ${{ matrix.python-version == '3.10' && matrix.database == 'postgres' }}
91106
run: |
@@ -100,6 +115,8 @@ jobs:
100115
MYSQL_HOST: 127.0.0.1
101116
MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} # get randomly assigned published port
102117
MYSQL_USER: root
118+
REDIS_HOST: localhost
119+
REDIS_PORT: ${{ job.services.redis.ports[6379] }}
103120
- name: Upload coverage result
104121
if: ${{ matrix.python-version == '3.10' && matrix.database == 'postgres' }}
105122
uses: actions/upload-artifact@v3

modoboa_amavis/tasks.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Async tasks."""
2+
3+
from typing import List
4+
5+
from django.utils.translation import ngettext
6+
7+
from modoboa.core import models as core_models
8+
9+
from .lib import SpamassassinClient
10+
from .sql_connector import SQLconnector
11+
12+
13+
def manual_learning(user_pk: int,
14+
mtype: str,
15+
selection: List[str],
16+
recipient_db: str):
17+
"""Task to trigger manual learning for given selection."""
18+
user = core_models.User.objects.get(pk=user_pk)
19+
connector = SQLconnector()
20+
saclient = SpamassassinClient(user, recipient_db)
21+
for item in selection:
22+
rcpt, mail_id = item.split()
23+
content = connector.get_mail_content(mail_id.encode("ascii"))
24+
result = saclient.learn_spam(rcpt, content) if mtype == "spam" \
25+
else saclient.learn_ham(rcpt, content)
26+
if not result:
27+
break
28+
connector.set_msgrcpt_status(
29+
rcpt, mail_id, mtype[0].upper()
30+
)
31+
if saclient.error is None:
32+
saclient.done()
33+
message = ngettext("%(count)d message processed successfully",
34+
"%(count)d messages processed successfully",
35+
len(selection)) % {"count": len(selection)}
36+
else:
37+
message = saclient.error

modoboa_amavis/tests/test_views.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55
import os
66
from unittest import mock
77

8+
from rq import SimpleWorker
9+
810
from django.core import mail
911
from django.core.management import call_command
1012
from django.test import override_settings
1113
from django.urls import reverse
1214

15+
import django_rq
16+
1317
from modoboa.admin import factories as admin_factories
1418
from modoboa.core import models as core_models
1519
from modoboa.lib.tests import ModoTestCase
@@ -226,7 +230,10 @@ def _test_mark_message(self, action, status):
226230
data = {"rcpt": smart_str(self.msgrcpt.rid.email)}
227231
response = self.ajax_post(url, data)
228232
self.assertEqual(
229-
response["message"], "1 message processed successfully")
233+
response["message"], "Your request is being processed...")
234+
queue = django_rq.get_queue("default")
235+
worker = SimpleWorker([queue], connection=queue.connection)
236+
worker.work(burst=True)
230237
self.msgrcpt.refresh_from_db()
231238
self.assertEqual(self.msgrcpt.rs, status)
232239

@@ -235,7 +242,8 @@ def _test_mark_message(self, action, status):
235242
self.set_global_parameter("sa_is_local", False)
236243
response = self.ajax_post(url, data)
237244
self.assertEqual(
238-
response["message"], "1 message processed successfully")
245+
response["message"], "Your request is being processed...")
246+
worker.work(burst=True)
239247
self.msgrcpt.refresh_from_db()
240248
self.assertEqual(self.msgrcpt.rs, status)
241249

@@ -289,7 +297,7 @@ def test_manual_learning_as_user(self):
289297
self._test_mark_message("ham", "H")
290298

291299
@mock.patch("socket.socket")
292-
def test_process(self, mock_socket):
300+
def test_process_release(self, mock_socket):
293301
"""Test process mode (bulk)."""
294302
# Initiate session
295303
url = reverse("modoboa_amavis:_mail_list")
@@ -305,8 +313,9 @@ def test_process(self, mock_socket):
305313
smart_str(msgrcpt.rid.email),
306314
smart_str(msgrcpt.mail.mail_id)),
307315
]
308-
mock_socket.return_value.recv.side_effect = (
309-
b"250 1234 Ok\r\n", b"250 1234 Ok\r\n")
316+
mock_socket.return_value.recv.side_effect = [
317+
b"250 1234 Ok\r\n", b"250 1234 Ok\r\n"
318+
]
310319
data = {
311320
"action": "release",
312321
"rcpt": smart_str(self.msgrcpt.rid.email),
@@ -317,15 +326,36 @@ def test_process(self, mock_socket):
317326
self.assertEqual(
318327
response["message"], "2 messages released successfully")
319328

329+
def test_process_all(self):
330+
"""Test process mode (bulk)."""
331+
# Initiate session
332+
url = reverse("modoboa_amavis:_mail_list")
333+
response = self.ajax_get(url)
334+
335+
msgrcpt = factories.create_spam("user@test.com")
336+
url = reverse("modoboa_amavis:mail_process")
337+
selection = [
338+
"{} {}".format(
339+
smart_str(self.msgrcpt.rid.email),
340+
smart_str(self.msgrcpt.mail.mail_id)),
341+
"{} {}".format(
342+
smart_str(msgrcpt.rid.email),
343+
smart_str(msgrcpt.mail.mail_id)),
344+
]
345+
data = {
346+
"rcpt": smart_str(self.msgrcpt.rid.email),
347+
"selection": ",".join(selection)
348+
}
349+
320350
data["action"] = "mark_as_spam"
321351
response = self.ajax_post(url, data)
322352
self.assertEqual(
323-
response["message"], "2 messages processed successfully")
353+
response["message"], "Your request is being processed...")
324354

325355
data["action"] = "mark_as_ham"
326356
response = self.ajax_post(url, data)
327357
self.assertEqual(
328-
response["message"], "2 messages processed successfully")
358+
response["message"], "Your request is being processed...")
329359

330360
data = {
331361
"action": "delete",

modoboa_amavis/views.py

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@
1414
from django.utils.translation import gettext as _, ngettext
1515
from django.views.decorators.csrf import csrf_exempt
1616

17+
import django_rq
18+
1719
from modoboa.admin.models import Domain, Mailbox
20+
from modoboa_amavis import tasks
1821
from modoboa.lib.exceptions import BadRequest
1922
from modoboa.lib.paginator import Paginator
2023
from modoboa.lib.web_utils import getctx, render_to_json_response
2124
from modoboa.parameters import tools as param_tools
2225
from . import constants
2326
from .forms import LearningRecipientForm
2427
from .lib import (
25-
AMrelease, QuarantineNavigationParameters, SpamassassinClient,
28+
AMrelease, QuarantineNavigationParameters,
2629
manual_learning_enabled, selfservice
2730
)
2831
from .models import Msgrcpt, Msgs
@@ -192,7 +195,7 @@ def viewheaders(request, mail_id):
192195
return render(request, "modoboa_amavis/viewheader.html", context)
193196

194197

195-
def check_mail_id(request, mail_id):
198+
def check_mail_id(request, mail_id) -> list:
196199
if isinstance(mail_id, six.string_types):
197200
if "rcpt" in request.POST:
198201
mail_id = ["%s %s" % (request.POST["rcpt"], mail_id)]
@@ -345,29 +348,17 @@ def mark_messages(request, selection, mtype, recipient_db=None):
345348
"user" if request.user.role == "SimpleUsers" else "global"
346349
)
347350
selection = check_mail_id(request, selection)
348-
connector = SQLconnector()
349-
saclient = SpamassassinClient(request.user, recipient_db)
350-
for item in selection:
351-
rcpt, mail_id = item.split()
352-
content = connector.get_mail_content(mail_id.encode("ascii"))
353-
result = saclient.learn_spam(rcpt, content) if mtype == "spam" \
354-
else saclient.learn_ham(rcpt, content)
355-
if not result:
356-
break
357-
connector.set_msgrcpt_status(
358-
rcpt, mail_id, mtype[0].upper()
359-
)
360-
if saclient.error is None:
361-
saclient.done()
362-
message = ngettext("%(count)d message processed successfully",
363-
"%(count)d messages processed successfully",
364-
len(selection)) % {"count": len(selection)}
365-
else:
366-
message = saclient.error
367-
status = 400 if saclient.error else 200
351+
352+
queue = django_rq.get_queue("default")
353+
queue.enqueue(tasks.manual_learning,
354+
request.user.pk,
355+
mtype,
356+
selection,
357+
recipient_db)
358+
368359
return render_to_json_response({
369-
"message": message, "reload": True
370-
}, status=status)
360+
"message": _("Your request is being processed..."), "reload": True
361+
})
371362

372363

373364
@login_required

test_project/test_project/settings.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,21 @@
189189

190190
MODOBOA_API_URL = "https://api.modoboa.org/1/"
191191

192+
# REDIS
193+
194+
REDIS_HOST = os.environ.get('REDIS_HOST', '127.0.0.1')
195+
REDIS_PORT = os.environ.get('REDIS_PORT', 6379)
196+
REDIS_QUOTA_DB = 0
197+
REDIS_URL = 'redis://{}:{}/{}'.format(REDIS_HOST, REDIS_PORT, REDIS_QUOTA_DB)
198+
199+
# RQ
200+
201+
RQ_QUEUES = {
202+
'default': {
203+
'URL': REDIS_URL,
204+
},
205+
}
206+
192207
# Password validation
193208
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
194209

0 commit comments

Comments
 (0)