[IMP] event: allow to send registrations emails asynchronously

This commit introduces a new configuration parameter 'event.event_mail_async'
forcing registrations-based communication to be asynchronous. Instead of
directly sending communication it triggers the cron to be run as soon as
possible.

When having large volume of registrations, and especially concurrent
registrations it saves a DB to avoid generating tickets and preparing emails
synchronously to the registration creation.

Task-3764894: Event: Allow using cron triggers for communication
Part of Task-3084943: Event: Improve communication scheduler scalability

Part-of: odoo/odoo#155777
This commit is contained in:
Thibault Delavallée
2024-03-18 10:23:55 +00:00
parent b9f0f3676a
commit 793a42ff13
4 changed files with 132 additions and 28 deletions
+9 -4
View File
@@ -308,10 +308,15 @@ class EventRegistration(models.Model):
return
onsubscribe_schedulers.update({'mail_done': False})
# we could simply call _create_missing_mail_registrations and let cron do their job
# but it currently leads to several delays. We therefore call execute until
# cron triggers are correctly used
onsubscribe_schedulers.with_user(SUPERUSER_ID).execute()
# either trigger the cron, either run schedulers immediately (scaling choice)
async_scheduler = self.env['ir.config_parameter'].sudo().get_param('event.event_mail_async')
if async_scheduler:
self.env.ref('event.event_mail_scheduler')._trigger()
else:
# we could simply call _create_missing_mail_registrations and let cron do their job
# but it currently leads to several delays. We therefore call execute until
# cron triggers are correctly used
onsubscribe_schedulers.with_user(SUPERUSER_ID).execute()
# ------------------------------------------------------------
# MAILING / GATEWAY
+53
View File
@@ -104,3 +104,56 @@ class EventCase(common.TransactionCase):
'phone': f'04560000{idx}{idx}',
} for idx in range(0, reg_count)])
return registrations
@classmethod
def _setup_test_reports(cls):
cls.test_report_view = cls.env["ir.ui.view"].create({
"arch_db": """
<t t-call="web.html_container">
<t t-foreach="docs" t-as="registration">
<t t-call="web.external_layout">
<div class="page">
<p>This is a sample of an external report.</p>
</div>
</t>
</t>
</t>""",
"key": "event_registration_test_report",
"name": "event_registration_test_report",
"type": "qweb",
})
cls.env["ir.model.data"].create({
"model": "ir.ui.view",
"module": "event",
"name": "event_registration_test_report",
"res_id": cls.test_report_view.id,
})
cls.test_report_action = cls.env['ir.actions.report'].create({
'name': 'Test Report on event.registration',
'model': 'event.registration',
'print_report_name': "f'TestReport for {object.name}'",
'report_type': 'qweb-pdf',
'report_name': 'event.event_registration_test_report',
})
cls.template_subscription = cls.env['mail.template'].create({
"body_html": """<div>Hello your registration to <t t-out="object.event_id.name"/> is confirmed.</div>""",
"email_from": "{{ (object.event_id.organizer_id.email_formatted or object.event_id.user_id.email_formatted or '') }}",
"email_to": """{{ (object.email and '"%s" <%s>' % (object.name, object.email)) or object.partner_id.email_formatted or '' }}""",
"lang": "{{ object.event_id.lang or object.partner_id.lang }}",
"model_id": cls.env['ir.model']._get_id("event.registration"),
"name": "Event: Registration Confirmation TEST",
"subject": "Confirmation for {{ object.event_id.name }}",
"report_template_ids": [(4, cls.test_report_action.id)],
})
cls.template_reminder = cls.env['mail.template'].create({
"body_html": """<div>Hello this is a reminder for your registration to <t t-out="object.event_id.name"/>.</div>""",
"email_from": "{{ (object.event_id.organizer_id.email_formatted or object.event_id.user_id.email_formatted or '') }}",
"email_to": """{{ (object.email and '"%s" <%s>' % (object.name, object.email)) or object.partner_id.email_formatted or '' }}""",
"lang": "{{ object.event_id.lang or object.partner_id.lang }}",
"model_id": cls.env['ir.model']._get_id("event.registration"),
"name": "Event: Registration Reminder TEST",
"subject": "Reminder for {{ object.event_id.name }}: {{ object.get_date_range_str() }}",
"report_template_ids": [(4, cls.test_report_action.id)],
})
+67 -24
View File
@@ -6,6 +6,7 @@ from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo import Command
from odoo.addons.base.tests.test_ir_cron import CronMixinCase
from odoo.addons.event.tests.common import EventCase
from odoo.addons.mail.tests.common import MockEmail
from odoo.tests import tagged, users
@@ -13,7 +14,7 @@ from odoo.tools import formataddr, mute_logger
@tagged('event_mail', 'post_install', '-at_install')
class TestMailSchedule(EventCase, MockEmail):
class TestMailSchedule(EventCase, MockEmail, CronMixinCase):
@classmethod
def setUpClass(cls):
@@ -27,6 +28,8 @@ class TestMailSchedule(EventCase, MockEmail):
# deactivate other schedulers to avoid messing with crons
cls.env['event.mail'].search([]).unlink()
# consider asynchronous sending as default sending
cls.env["ir.config_parameter"].set_param("event.event_mail_async", False)
# freeze some datetimes, and ensure more than 1D+1H before event starts
# to ease time-based scheduler check
@@ -37,9 +40,7 @@ class TestMailSchedule(EventCase, MockEmail):
cls.event_date_begin = datetime(2021, 3, 22, 8, 0, 0)
cls.event_date_end = datetime(2021, 3, 24, 18, 0, 0)
cls.template_subscription_id = cls.env['ir.model.data']._xmlid_to_res_id('event.event_subscription')
cls.template_reminder_id = cls.env['ir.model.data']._xmlid_to_res_id('event.event_reminder')
cls._setup_test_reports()
with cls.mock_datetime_and_now(cls, cls.reference_now):
# create with admin to force create_date
cls.test_event = cls.env['event.event'].create({
@@ -52,28 +53,28 @@ class TestMailSchedule(EventCase, MockEmail):
'interval_unit': 'now',
'interval_type': 'after_sub',
'notification_type': 'mail',
'template_ref': f'mail.template,{cls.template_subscription_id}',
'template_ref': f'mail.template,{cls.template_subscription.id}',
}),
(0, 0, { # one hour after subscription
'interval_nbr': 1,
'interval_unit': 'hours',
'interval_type': 'after_sub',
'notification_type': 'mail',
'template_ref': f'mail.template,{cls.template_subscription_id}',
'template_ref': f'mail.template,{cls.template_subscription.id}',
}),
(0, 0, { # 1 days before event
'interval_nbr': 1,
'interval_unit': 'days',
'interval_type': 'before_event',
'notification_type': 'mail',
'template_ref': f'mail.template,{cls.template_reminder_id}',
'template_ref': f'mail.template,{cls.template_reminder.id}',
}),
(0, 0, { # immediately after event
'interval_nbr': 1,
'interval_unit': 'hours',
'interval_type': 'after_event',
'notification_type': 'mail',
'template_ref': f'mail.template,{cls.template_reminder_id}',
'template_ref': f'mail.template,{cls.template_reminder.id}',
}),
]
})
@@ -160,9 +161,10 @@ class TestMailSchedule(EventCase, MockEmail):
[formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))],
'outgoing',
content=None,
fields_values={'subject': 'Your registration at %s' % test_event.name,
'email_from': self.user_eventmanager.company_id.email_formatted,
})
fields_values={
'email_from': self.user_eventmanager.company_id.email_formatted,
'subject': f'Confirmation for {test_event.name}',
})
# same for second scheduler: scheduled but not sent
self.assertEqual(len(after_sub_scheduler_2.mail_registration_ids), 2, 'event: should have 2 scheduled communication (1 / registration)')
@@ -198,9 +200,10 @@ class TestMailSchedule(EventCase, MockEmail):
[formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))],
'outgoing',
content=None,
fields_values={'subject': 'Your registration at %s' % test_event.name,
'email_from': self.user_eventmanager.company_id.email_formatted,
})
fields_values={
'email_from': self.user_eventmanager.company_id.email_formatted,
'subject': f'Confirmation for {test_event.name}',
})
# PRE SCHEDULERS (MOVE FORWARD IN TIME)
# --------------------------------------------------
@@ -233,9 +236,10 @@ class TestMailSchedule(EventCase, MockEmail):
[formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))],
'outgoing',
content=None,
fields_values={'subject': '%s: tomorrow' % test_event.name,
'email_from': self.user_eventmanager.company_id.email_formatted,
})
fields_values={
'email_from': self.user_eventmanager.company_id.email_formatted,
'subject': f'Reminder for {test_event.name}: tomorrow',
})
# NEW REGISTRATION EFFECT ON SCHEDULERS
# --------------------------------------------------
@@ -283,7 +287,7 @@ class TestMailSchedule(EventCase, MockEmail):
# manual check because 2 identical mails are sent and mail tools do not support it easily
for mail in self._new_mails:
self.assertEqual(mail.email_from, self.user_eventmanager.company_id.email_formatted)
self.assertEqual(mail.subject, 'Your registration at %s' % test_event.name)
self.assertEqual(mail.subject, f'Confirmation for {test_event.name}')
self.assertEqual(mail.state, 'outgoing')
self.assertEqual(mail.email_to, formataddr((reg3.name, reg3.email)))
@@ -308,9 +312,10 @@ class TestMailSchedule(EventCase, MockEmail):
[formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email)), formataddr((reg3.name, reg3.email))],
'outgoing',
content=None,
fields_values={'subject': '%s: today' % test_event.name,
'email_from': self.user_eventmanager.company_id.email_formatted,
})
fields_values={
'email_from': self.user_eventmanager.company_id.email_formatted,
'subject': f"Reminder for {test_event.name}: today",
})
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
@users('user_eventmanager')
@@ -327,8 +332,8 @@ class TestMailSchedule(EventCase, MockEmail):
# consider having hanging registrations, still not processed (e.g. adding
# a new scheduler after)
self.env.invalidate_all()
# com 58, event 36
with self.assertQueryCount(61), self.mock_datetime_and_now(reference_now), \
# com 59, event 37
with self.assertQueryCount(62), self.mock_datetime_and_now(reference_now), \
self.mock_mail_gateway():
_existing = self.env['event.registration'].create([
{
@@ -347,7 +352,7 @@ class TestMailSchedule(EventCase, MockEmail):
'interval_unit': 'now',
'interval_type': 'after_sub',
'notification_type': 'mail',
'template_ref': f'mail.template,{self.template_subscription_id}',
'template_ref': f'mail.template,{self.template_subscription.id}',
}),
]})
self.env.invalidate_all()
@@ -369,6 +374,44 @@ class TestMailSchedule(EventCase, MockEmail):
self.assertEqual(self.mail_mail_create_mocked.call_count, 7,
'EventMail: TODO: should create one mail / new registration')
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
@users('user_eventmanager')
def test_event_mail_schedule_on_subscription_async(self):
""" Async mode for schedulers activated, should not send communication
in the same transaction. """
test_event = self.test_event.with_env(self.env)
cron = self.env.ref('event.event_mail_scheduler')
reference_now = self.reference_now
self.env['ir.config_parameter'].sudo().set_param('event.event_mail_async', True)
with self.capture_triggers(cron.id) as capt, \
self.mock_datetime_and_now(reference_now + relativedelta(minutes=10)), \
self.mock_mail_gateway():
existing = self.env['event.registration'].create([
{
'email': f'new.async.attendee.{idx}@test.example.com',
'event_id': test_event.id,
'name': f'New Async Attendee {idx}',
} for idx in range(5)
])
self.assertEqual(len(self._new_mails), 0)
self.assertEqual(self.mail_mail_create_mocked.call_count, 0)
capt.records.ensure_one()
self.assertEqual(capt.records.call_at, reference_now.replace(microsecond=0) + relativedelta(minutes=10))
# run cron: emails should be send for registrations
with self.mock_datetime_and_now(reference_now + relativedelta(minutes=10)), \
self.mock_mail_gateway():
cron.sudo().method_direct_trigger()
self.assertMailMailWEmails(
[formataddr((reg.name, reg.email)) for reg in existing],
"outgoing",
content=f"Hello your registration to {test_event.name} is confirmed",
fields_values={
'email_from': self.user_eventmanager.company_id.email_formatted,
'subject': f'Confirmation for {test_event.name}',
})
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
def test_unique_event_mail_ids(self):
# create event with default event_mail_ids lines
@@ -17,6 +17,9 @@ class TestSMSSchedule(EventCase, SMSCase):
def setUpClass(cls):
super(TestSMSSchedule, cls).setUpClass()
# consider asynchronous sending as default sending
cls.env["ir.config_parameter"].set_param("event.event_mail_async", False)
cls.sms_template_sub = cls.env['sms.template'].create({
'name': 'Test subscription',
'model_id': cls.env.ref('event.model_event_registration').id,