Skip to content

Commit c819200

Browse files
authored
Merge pull request #140 from open-zaak/feature/1203-setup-config-retry-vars
✨ [open-zaak/open-zaak#1203] Setup configuration for retry variables
2 parents 9f8a46a + 43d42f1 commit c819200

File tree

8 files changed

+179
-33
lines changed

8 files changed

+179
-33
lines changed

docs/installation/configuration/env_config.md

+1-11
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,6 @@ on Docker, since `localhost` is contained within the container:
100100

101101
* `CELERY_RESULT_BACKEND`: the backend where the results of tasks will be stored (default: `redis://localhost:6379/1`)
102102

103-
* `NOTIFICATION_DELIVERY_MAX_RETRIES`: the maximum number of retries Celery will do if sending a notification failed.
104-
105-
* `NOTIFICATION_DELIVERY_RETRY_BACKOFF`: a boolean or a number. If this option is set to
106-
`True`, autoretries will be delayed following the rules of exponential backoff. If
107-
this option is set to a number, it is used as a delay factor.
108-
109-
* `NOTIFICATION_DELIVERY_RETRY_BACKOFF_MAX`: an integer, specifying number of seconds.
110-
If ``retry_backoff`` is enabled, this option will set a maximum delay in seconds
111-
between task autoretries. By default, this option is set to 48 seconds.
112-
113103
### Cross-Origin-Resource-Sharing
114104

115105
The following parameters control the CORS policy.
@@ -130,7 +120,7 @@ The following parameters control the CORS policy.
130120
### Initial configuration
131121

132122
Open Notificaties supports `setup_configuration` management command, which allows configuration via
133-
environment variables.
123+
environment variables.
134124
All these environment variables are described at CLI configuration.
135125

136126

docs/installation/configuration/opennotifs_config_cli.rst

+19-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ Preparation
2727

2828
The command executes the list of pluggable configuration steps, and each step
2929
requires specific environment variables, that should be prepared.
30-
Here is the description of all available configuration steps and the environment variables,
31-
use by each step.
30+
Here is the description of all available configuration steps and the environment variables,
31+
use by each step.
3232

3333
Sites configuration
3434
-------------------
@@ -65,6 +65,23 @@ Make sure that the correct permissions are configured in Open Zaak Autorisaties
6565
for example, ``open-zaak``. Required.
6666
* ``OPENZAAK_NOTIF_SECRET``: some random string. Required.
6767

68+
Notification retry configuration
69+
--------------------------------
70+
71+
Open Notifications has a retry mechanism to guarantee notification delivery, this mechanism
72+
is described in :ref:`delivery_guarantees`. The parameters for this behavior can be configured via the
73+
following environment variables.
74+
75+
* ``NOTIFICATION_DELIVERY_MAX_RETRIES``: the maximum number of retries Celery will do if sending a notification failed.
76+
* ``NOTIFICATION_DELIVERY_RETRY_BACKOFF``: a boolean or a number. If this option is set to
77+
``True``, autoretries will be delayed following the rules of exponential backoff. If
78+
this option is set to a number, it is used as a delay factor.
79+
* ``NOTIFICATION_DELIVERY_RETRY_BACKOFF_MAX``: an integer, specifying number of seconds.
80+
If ``retry_backoff`` is enabled, this option will set a maximum delay in seconds
81+
between task autoretries. By default, this option is set to 48 seconds.
82+
83+
These settings can also later be changed via the admin interface, under ``Configuration > Notification component configuration``.
84+
6885
Execution
6986
=========
7087

src/nrc/api/tasks.py

+12-10
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import json
22
import logging
33

4-
from django.conf import settings
54
from django.core.management import call_command
65
from django.core.serializers.json import DjangoJSONEncoder
76
from django.utils.translation import gettext_lazy as _
87

98
import requests
9+
from notifications_api_common.autoretry import add_autoretry_behaviour
1010

1111
from nrc.celery import app
1212
from nrc.datamodel.models import Abonnement, NotificatieResponse
@@ -18,15 +18,7 @@ class NotificationException(Exception):
1818
pass
1919

2020

21-
@app.task(
22-
autoretry_for=(
23-
NotificationException,
24-
requests.RequestException,
25-
),
26-
max_retries=settings.NOTIFICATION_DELIVERY_MAX_RETRIES,
27-
retry_backoff=settings.NOTIFICATION_DELIVERY_RETRY_BACKOFF,
28-
retry_backoff_max=settings.NOTIFICATION_DELIVERY_RETRY_BACKOFF_MAX,
29-
)
21+
@app.task
3022
def deliver_message(sub_id: int, msg: dict, **kwargs) -> None:
3123
"""
3224
send msg to subscriber
@@ -81,3 +73,13 @@ def clean_old_notifications() -> None:
8173
cleans up old "Notificatie" and "NotificatieResponse"
8274
"""
8375
call_command("clean_old_notifications")
76+
77+
78+
add_autoretry_behaviour(
79+
deliver_message,
80+
autoretry_for=(
81+
NotificationException,
82+
requests.RequestException,
83+
),
84+
retry_jitter=False,
85+
)

src/nrc/api/tests/test_notificatie.py

+58-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
from unittest.mock import patch
22

3-
from django.test import override_settings
3+
from django.test import TestCase, override_settings
44
from django.utils.timezone import now
55

6+
import requests_mock
7+
from celery.exceptions import Retry
8+
from notifications_api_common.models import NotificationsConfig
69
from rest_framework import status
710
from rest_framework.reverse import reverse
811
from rest_framework.test import APITestCase
912
from vng_api_common.conf.api import BASE_REST_FRAMEWORK
1013
from vng_api_common.tests import JWTAuthMixin
1114

15+
from nrc.api.tasks import deliver_message
1216
from nrc.datamodel.models import Notificatie
1317
from nrc.datamodel.tests.factories import (
1418
AbonnementFactory,
@@ -142,3 +146,56 @@ def test_notificatie_send_empty_kenmerk_value(self, mock_task):
142146
mock_task.assert_called_once_with(
143147
abon.id, msg, notificatie_id=Notificatie.objects.get().id, attempt=1
144148
)
149+
150+
151+
@patch("notifications_api_common.autoretry.get_exponential_backoff_interval")
152+
@patch("notifications_api_common.autoretry.NotificationsConfig.get_solo")
153+
@patch("nrc.api.serializers.deliver_message.retry")
154+
class NotificatieRetryTests(TestCase):
155+
def test_notificatie_retry_use_global_config(
156+
self, mock_retry, mock_config, mock_get_exponential_backoff
157+
):
158+
"""
159+
Verify that retry variables configured on `NotificationsConfig` override the
160+
variables from the settings
161+
"""
162+
mock_config.return_value = NotificationsConfig(
163+
notification_delivery_max_retries=4,
164+
notification_delivery_retry_backoff=4,
165+
notification_delivery_retry_backoff_max=28,
166+
)
167+
kanaal = KanaalFactory.create(
168+
naam="zaken", filters=["bron", "zaaktype", "vertrouwelijkheidaanduiding"]
169+
)
170+
abon = AbonnementFactory.create(callback_url="https://example.com/callback")
171+
filter_group = FilterGroupFactory.create(kanaal=kanaal, abonnement=abon)
172+
FilterFactory.create(
173+
filter_group=filter_group, key="bron", value="082096752011"
174+
)
175+
msg = {
176+
"kanaal": "zaken",
177+
"hoofdObject": "https://ref.tst.vng.cloud/zrc/api/v1/zaken/d7a22",
178+
"resource": "status",
179+
"resourceUrl": "https://ref.tst.vng.cloud/zrc/api/v1/statussen/d7a22/721c9",
180+
"actie": "create",
181+
"aanmaakdatum": now(),
182+
"kenmerken": {
183+
"bron": "082096752011",
184+
"zaaktype": "example.com/api/v1/zaaktypen/5aa5c",
185+
"vertrouwelijkheidaanduiding": "openbaar",
186+
},
187+
}
188+
189+
mock_retry.side_effect = Retry()
190+
with requests_mock.Mocker() as m:
191+
m.post(abon.callback_url, status_code=404)
192+
with self.assertRaises(Retry):
193+
deliver_message(abon.id, msg)
194+
195+
mock_get_exponential_backoff.assert_called_once_with(
196+
factor=4,
197+
retries=0,
198+
maximum=28,
199+
full_jitter=False,
200+
)
201+
self.assertEqual(deliver_message.max_retries, 4)

src/nrc/conf/includes/base.py

+14-7
Original file line numberDiff line numberDiff line change
@@ -496,13 +496,6 @@
496496
},
497497
}
498498

499-
# Retry settings for delivering notifications to subscriptions
500-
NOTIFICATION_DELIVERY_MAX_RETRIES = config("NOTIFICATION_DELIVERY_MAX_RETRIES", 5)
501-
NOTIFICATION_DELIVERY_RETRY_BACKOFF = config("NOTIFICATION_DELIVERY_RETRY_BACKOFF", 3)
502-
NOTIFICATION_DELIVERY_RETRY_BACKOFF_MAX = config(
503-
"NOTIFICATION_DELIVERY_RETRY_BACKOFF_MAX", 48
504-
)
505-
506499
#
507500
# DJANGO-ADMIN-INDEX
508501
#
@@ -565,6 +558,7 @@
565558
"nrc.config.site.SiteConfigurationStep",
566559
"nrc.config.authorization.AuthorizationStep",
567560
"nrc.config.authorization.OpenZaakAuthStep",
561+
"nrc.config.notification_retry.NotificationRetryConfigurationStep",
568562
]
569563

570564
#
@@ -585,3 +579,16 @@
585579
OPENZAAK_NOTIF_CONFIG_ENABLE = config("OPENZAAK_NOTIF_CONFIG_ENABLE", default=True)
586580
OPENZAAK_NOTIF_CLIENT_ID = config("OPENZAAK_NOTIF_CLIENT_ID", "")
587581
OPENZAAK_NOTIF_SECRET = config("OPENZAAK_NOTIF_SECRET", "")
582+
583+
# setup configuration for Notification retry
584+
# Retry settings for delivering notifications to subscriptions
585+
NOTIFICATION_RETRY_CONFIG_ENABLE = config(
586+
"NOTIFICATION_RETRY_CONFIG_ENABLE", default=True
587+
)
588+
NOTIFICATION_DELIVERY_MAX_RETRIES = config("NOTIFICATION_DELIVERY_MAX_RETRIES", None)
589+
NOTIFICATION_DELIVERY_RETRY_BACKOFF = config(
590+
"NOTIFICATION_DELIVERY_RETRY_BACKOFF", None
591+
)
592+
NOTIFICATION_DELIVERY_RETRY_BACKOFF_MAX = config(
593+
"NOTIFICATION_DELIVERY_RETRY_BACKOFF_MAX", None
594+
)

src/nrc/config/notification_retry.py

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from django.conf import settings
2+
3+
from django_setup_configuration.configuration import BaseConfigurationStep
4+
from notifications_api_common.models import NotificationsConfig
5+
6+
7+
class NotificationRetryConfigurationStep(BaseConfigurationStep):
8+
"""
9+
Configure the notifications retry behaviour.
10+
"""
11+
12+
verbose_name = "Notification retry configuration"
13+
required_settings = []
14+
optional_settings = [
15+
"NOTIFICATION_DELIVERY_MAX_RETRIES",
16+
"NOTIFICATION_DELIVERY_RETRY_BACKOFF",
17+
"NOTIFICATION_DELIVERY_RETRY_BACKOFF_MAX",
18+
]
19+
enable_setting = "NOTIFICATION_RETRY_CONFIG_ENABLE"
20+
21+
def is_configured(self) -> bool:
22+
config = NotificationsConfig.get_solo()
23+
for setting_name in self.optional_settings:
24+
# It is considered configured if one or more fields have non-default values
25+
model_field = getattr(NotificationsConfig, setting_name.lower()).field
26+
if getattr(config, setting_name.lower()) != model_field.default:
27+
return True
28+
return False
29+
30+
def configure(self):
31+
config = NotificationsConfig.get_solo()
32+
for setting_name in self.optional_settings:
33+
if (setting_value := getattr(settings, setting_name)) is not None:
34+
setattr(config, setting_name.lower(), setting_value)
35+
config.save()
36+
37+
def test_configuration(self): ...

src/nrc/tests/commands/test_setup_configuration.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from zgw_consumers.test import mock_service_oas_get
1717

1818
from nrc.config.authorization import AuthorizationStep, OpenZaakAuthStep
19+
from nrc.config.notification_retry import NotificationRetryConfigurationStep
1920
from nrc.config.site import SiteConfigurationStep
2021

2122

@@ -29,6 +30,8 @@
2930
OPENZAAK_NOTIF_SECRET="oz-secret",
3031
)
3132
class SetupConfigurationTests(TestCase):
33+
maxDiff = None
34+
3235
def setUp(self):
3336
super().setUp()
3437

@@ -60,19 +63,21 @@ def test_setup_configuration(self, m):
6063
},
6164
)
6265

63-
call_command("setup_configuration", stdout=stdout)
66+
call_command("setup_configuration", stdout=stdout, no_color=True)
6467

6568
with self.subTest("Command output"):
6669
command_output = stdout.getvalue().splitlines()
6770
expected_output = [
6871
f"Configuration will be set up with following steps: [{SiteConfigurationStep()}, "
69-
f"{AuthorizationStep()}, {OpenZaakAuthStep()}]",
72+
f"{AuthorizationStep()}, {OpenZaakAuthStep()}, {NotificationRetryConfigurationStep()}]",
7073
f"Configuring {SiteConfigurationStep()}...",
7174
f"{SiteConfigurationStep()} is successfully configured",
7275
f"Configuring {AuthorizationStep()}...",
7376
f"{AuthorizationStep()} is successfully configured",
7477
f"Configuring {OpenZaakAuthStep()}...",
7578
f"{OpenZaakAuthStep()} is successfully configured",
79+
f"Configuring {NotificationRetryConfigurationStep()}...",
80+
f"{NotificationRetryConfigurationStep()} is successfully configured",
7681
"Instance configuration completed.",
7782
]
7883

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from django.test import TestCase, override_settings
2+
3+
from notifications_api_common.models import NotificationsConfig
4+
5+
from nrc.config.notification_retry import NotificationRetryConfigurationStep
6+
7+
8+
@override_settings(
9+
NOTIFICATION_DELIVERY_MAX_RETRIES=4,
10+
NOTIFICATION_DELIVERY_RETRY_BACKOFF=5,
11+
NOTIFICATION_DELIVERY_RETRY_BACKOFF_MAX=6,
12+
)
13+
class NotificationRetryConfigurationTests(TestCase):
14+
def test_configure(self):
15+
configuration = NotificationRetryConfigurationStep()
16+
configuration.configure()
17+
18+
config = NotificationsConfig.get_solo()
19+
20+
self.assertEqual(config.notification_delivery_max_retries, 4)
21+
self.assertEqual(config.notification_delivery_retry_backoff, 5)
22+
self.assertEqual(config.notification_delivery_retry_backoff_max, 6)
23+
24+
def test_is_configured(self):
25+
configuration = NotificationRetryConfigurationStep()
26+
27+
self.assertFalse(configuration.is_configured())
28+
29+
configuration.configure()
30+
31+
self.assertTrue(configuration.is_configured())

0 commit comments

Comments
 (0)