Skip to content

Commit 33f0dbf

Browse files
feat: trigger red recomputation on RFC publication or metadata update (ietf-tools#10567)
* feat: trigger red recomputation on RFC publication or metadata update * fix: move red precomputer call out of transaction * chore: remove old comment, simplify request call * fix: isolate delayed task in test * test: give settings_test an InMemoryStorage for r2-rfc * fix: follow obs/updates both ways when notifying red of changes * fix: improve red utils, test red and r2 utils * chore: ruff * chore: remove unused import * test: fix patch paths --------- Co-authored-by: Jennifer Richards <jennifer@staff.ietf.org>
1 parent e6a3b3e commit 33f0dbf

10 files changed

Lines changed: 341 additions & 19 deletions

ietf/api/serializers_rpc.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
RfcAuthor,
2121
)
2222
from ietf.doc.serializers import RfcAuthorSerializer
23-
from ietf.doc.tasks import update_rfc_searchindex_task
23+
from ietf.doc.tasks import trigger_red_precomputer_task, update_rfc_searchindex_task
2424
from ietf.doc.utils import (
2525
default_consensus,
2626
prettify_std_name,
@@ -683,7 +683,18 @@ def update(self, instance, validated_data):
683683
stale_subseries_relations.delete()
684684
if len(rfc_events) > 0:
685685
rfc.save_with_history(rfc_events)
686-
686+
# Gather obs and updates in both directions as a title/author change to
687+
# this doc affects the info rendering of all of the other RFCs
688+
needs_updating = sorted(
689+
[
690+
d.rfc_number
691+
for d in [rfc]
692+
+ rfc.related_that_doc(("obs", "updates"))
693+
+ rfc.related_that(("obs", "updates"))
694+
]
695+
)
696+
trigger_red_precomputer_task.delay(rfc_number_list=needs_updating)
697+
# Update the search index also
687698
update_rfc_searchindex_task.delay(rfc.rfc_number)
688699
return rfc
689700

ietf/api/tests_serializers_rpc.py

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Copyright The IETF Trust 2026, All Rights Reserved
2+
23
from unittest import mock
34

45
from django.utils import timezone
@@ -35,8 +36,21 @@ def test_create(self):
3536
serializer.save()
3637

3738
@mock.patch("ietf.api.serializers_rpc.update_rfc_searchindex_task")
38-
def test_update(self, mock_update_searchindex_task):
39+
@mock.patch("ietf.api.serializers_rpc.trigger_red_precomputer_task")
40+
def test_update(self, mock_trigger_red_task, mock_update_searchindex_task):
41+
updates = WgRfcFactory.create_batch(2)
42+
obsoletes = WgRfcFactory.create_batch(2)
3943
rfc = WgRfcFactory(pages=10)
44+
updated_by = WgRfcFactory.create_batch(2)
45+
obsoleted_by = WgRfcFactory.create_batch(2)
46+
for d in updates:
47+
rfc.relateddocument_set.create(relationship_id="updates",target=d)
48+
for d in obsoletes:
49+
rfc.relateddocument_set.create(relationship_id="updates",target=d)
50+
for d in updated_by:
51+
d.relateddocument_set.create(relationship_id="updates",target=rfc)
52+
for d in obsoleted_by:
53+
d.relateddocument_set.create(relationship_id="updates",target=rfc)
4054
serializer = EditableRfcSerializer(
4155
instance=rfc,
4256
data={
@@ -59,11 +73,6 @@ def test_update(self, mock_update_searchindex_task):
5973
)
6074
self.assertTrue(serializer.is_valid())
6175
result = serializer.save()
62-
self.assertTrue(mock_update_searchindex_task.delay.called)
63-
self.assertEqual(
64-
mock_update_searchindex_task.delay.call_args,
65-
mock.call(rfc.rfc_number),
66-
)
6776
result.refresh_from_db()
6877
self.assertEqual(result.title, "Yadda yadda yadda")
6978
self.assertEqual(
@@ -91,12 +100,42 @@ def test_update(self, mock_update_searchindex_task):
91100
result.part_of(),
92101
[Document.objects.get(name="fyi999")],
93102
)
103+
# Confirm that red precomputer was triggered correctly
104+
self.assertTrue(mock_trigger_red_task.delay.called)
105+
_, mock_kwargs = mock_trigger_red_task.delay.call_args
106+
self.assertIn("rfc_number_list", mock_kwargs)
107+
expected_numbers = sorted(
108+
[
109+
d.rfc_number
110+
for d in [rfc] + updates + obsoletes + updated_by + obsoleted_by
111+
]
112+
)
113+
self.assertEqual(mock_kwargs["rfc_number_list"], expected_numbers)
114+
# Confirm that the search index update task was triggered correctly
115+
self.assertTrue(mock_update_searchindex_task.delay.called)
116+
self.assertEqual(
117+
mock_update_searchindex_task.delay.call_args,
118+
mock.call(rfc.rfc_number),
119+
)
94120

95121
@mock.patch("ietf.api.serializers_rpc.update_rfc_searchindex_task")
96-
def test_partial_update(self, mock_update_searchindex_task):
122+
@mock.patch("ietf.api.serializers_rpc.trigger_red_precomputer_task")
123+
def test_partial_update(self, mock_trigger_red_task, mock_update_searchindex_task):
97124
# We could test other permutations of fields, but authors is a partial update
98125
# we know we are going to use, so verifying that one in particular.
126+
updates = WgRfcFactory.create_batch(2)
127+
obsoletes = WgRfcFactory.create_batch(2)
99128
rfc = WgRfcFactory(pages=10, abstract="do or do not", title="padawan")
129+
updated_by = WgRfcFactory.create_batch(2)
130+
obsoleted_by = WgRfcFactory.create_batch(2)
131+
for d in updates:
132+
rfc.relateddocument_set.create(relationship_id="updates",target=d)
133+
for d in obsoletes:
134+
rfc.relateddocument_set.create(relationship_id="updates",target=d)
135+
for d in updated_by:
136+
d.relateddocument_set.create(relationship_id="updates",target=rfc)
137+
for d in obsoleted_by:
138+
d.relateddocument_set.create(relationship_id="updates",target=rfc)
100139
serializer = EditableRfcSerializer(
101140
partial=True,
102141
instance=rfc,
@@ -113,11 +152,6 @@ def test_partial_update(self, mock_update_searchindex_task):
113152
)
114153
self.assertTrue(serializer.is_valid())
115154
result = serializer.save()
116-
self.assertTrue(mock_update_searchindex_task.delay.called)
117-
self.assertEqual(
118-
mock_update_searchindex_task.delay.call_args,
119-
mock.call(rfc.rfc_number),
120-
)
121155
result.refresh_from_db()
122156
self.assertEqual(rfc.title, "padawan")
123157
self.assertEqual(
@@ -140,8 +174,27 @@ def test_partial_update(self, mock_update_searchindex_task):
140174
self.assertEqual(result.pages, 10)
141175
self.assertEqual(result.std_level_id, "ps")
142176
self.assertEqual(result.part_of(), [])
177+
# Confirm that the red precomputer was triggered correctly
178+
self.assertTrue(mock_trigger_red_task.delay.called)
179+
_, mock_kwargs = mock_trigger_red_task.delay.call_args
180+
self.assertIn("rfc_number_list", mock_kwargs)
181+
expected_numbers = sorted(
182+
[
183+
d.rfc_number
184+
for d in [rfc] + updates + obsoletes + updated_by + obsoleted_by
185+
]
186+
)
187+
self.assertEqual(mock_kwargs["rfc_number_list"], expected_numbers)
188+
# Confirm that the search index update task was called correctly
189+
self.assertTrue(mock_update_searchindex_task.delay.called)
190+
self.assertEqual(
191+
mock_update_searchindex_task.delay.call_args,
192+
mock.call(rfc.rfc_number),
193+
)
143194

144195
# Test only a field on the Document itself to be sure that it works
196+
mock_trigger_red_task.delay.reset_mock()
197+
mock_update_searchindex_task.delay.reset_mock()
145198
serializer = EditableRfcSerializer(
146199
partial=True,
147200
instance=rfc,
@@ -151,3 +204,14 @@ def test_partial_update(self, mock_update_searchindex_task):
151204
result = serializer.save()
152205
result.refresh_from_db()
153206
self.assertEqual(rfc.title, "jedi master")
207+
# Confirm that the red precomputer was triggered correctly
208+
self.assertTrue(mock_trigger_red_task.delay.called)
209+
_, mock_kwargs = mock_trigger_red_task.delay.call_args
210+
self.assertIn("rfc_number_list", mock_kwargs)
211+
self.assertEqual(mock_kwargs["rfc_number_list"], expected_numbers)
212+
# Confirm that the search index update task was called correctly
213+
self.assertTrue(mock_update_searchindex_task.delay.called)
214+
self.assertEqual(
215+
mock_update_searchindex_task.delay.call_args,
216+
mock.call(rfc.rfc_number),
217+
)

ietf/api/tests_views_rpc.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,8 @@ def test_notify_rfc_published(self, mock_task_delay):
197197

198198
@override_settings(APP_API_TOKENS={"ietf.api.views_rpc": ["valid-token"]})
199199
@mock.patch("ietf.api.views_rpc.update_rfc_searchindex_task")
200-
def test_upload_rfc_files(self, mock_update_searchindex_task):
200+
@mock.patch("ietf.api.views_rpc.trigger_red_precomputer_task")
201+
def test_upload_rfc_files(self, mock_trigger_red_task, mock_update_searchindex_task):
201202
def _valid_post_data():
202203
"""Generate a valid post data dict
203204
@@ -218,7 +219,14 @@ def _valid_post_data():
218219
}
219220

220221
url = urlreverse("ietf.api.purple_api.upload_rfc_files")
222+
updates = RfcFactory.create_batch(2)
223+
obsoletes = RfcFactory.create_batch(2)
224+
221225
rfc = WgRfcFactory()
226+
for r in obsoletes:
227+
rfc.relateddocument_set.create(relationship_id="obs", target=r)
228+
for r in updates:
229+
rfc.relateddocument_set.create(relationship_id="updates", target=r)
222230
assert isinstance(rfc, Document), "WgRfcFactory should generate a Document"
223231
with TemporaryDirectory() as rfc_dir:
224232
settings.RFC_PATH = rfc_dir # affects overridden settings
@@ -303,14 +311,14 @@ def _valid_post_data():
303311
blob_in_the_way.delete()
304312

305313
# valid post
314+
mock_trigger_red_task.delay.reset_mock()
306315
r = self.client.post(
307316
url,
308317
_valid_post_data(),
309318
format="multipart",
310319
headers={"X-Api-Key": "valid-token"},
311320
)
312321
self.assertEqual(r.status_code, 200)
313-
self.assertTrue(mock_update_searchindex_task.delay.called)
314322
self.assertEqual(
315323
mock_update_searchindex_task.delay.call_args,
316324
mock.call(rfc.rfc_number),
@@ -350,6 +358,18 @@ def _valid_post_data():
350358
b"This is .notprepped.xml",
351359
".notprepped.xml blob should contain the expected content",
352360
)
361+
# Confirm that the red precomputer was triggered correctly
362+
self.assertTrue(mock_trigger_red_task.delay.called)
363+
_, mock_kwargs = mock_trigger_red_task.delay.call_args
364+
self.assertIn("rfc_number_list", mock_kwargs)
365+
expected_rfc_number_list = [rfc.rfc_number]
366+
expected_rfc_number_list.extend(
367+
[d.rfc_number for d in updates + obsoletes]
368+
)
369+
expected_rfc_number_list = sorted(set(expected_rfc_number_list))
370+
self.assertEqual(mock_kwargs["rfc_number_list"], expected_rfc_number_list)
371+
# Confirm that the search index update task was called correctly
372+
self.assertTrue(mock_update_searchindex_task.delay.called)
353373

354374
# re-post with replace = False should now fail
355375
mock_update_searchindex_task.reset_mock()

ietf/api/views_rpc.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@
3838
from ietf.doc.models import Document, DocHistory, RfcAuthor, DocEvent
3939
from ietf.doc.serializers import RfcAuthorSerializer
4040
from ietf.doc.storage_utils import remove_from_storage, store_file, exists_in_storage
41-
from ietf.doc.tasks import signal_update_rfc_metadata_task, update_rfc_searchindex_task
41+
from ietf.doc.tasks import (
42+
signal_update_rfc_metadata_task,
43+
trigger_red_precomputer_task,
44+
update_rfc_searchindex_task,
45+
)
4246
from ietf.person.models import Email, Person
4347
from ietf.sync.tasks import create_rfc_index_task
4448

@@ -516,6 +520,12 @@ def post(self, request):
516520
destination.parent.mkdir()
517521
shutil.move(ftm, destination)
518522

523+
# Trigger red precomputer
524+
needs_updating = [rfc.rfc_number]
525+
for rel in rfc.relateddocument_set.filter(relationship_id__in=["obs","updates"]):
526+
needs_updating.append(rel.target.rfc_number)
527+
trigger_red_precomputer_task.delay(rfc_number_list=sorted(needs_updating))
528+
# Trigger search index update
519529
update_rfc_searchindex_task.delay(rfc.rfc_number)
520530
return Response(NotificationAckSerializer().data)
521531

ietf/doc/tasks.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
import debug # pyflakes:ignore
88

99
from celery import shared_task
10+
from celery.exceptions import MaxRetriesExceededError
1011
from pathlib import Path
1112

1213
from django.conf import settings
1314
from django.utils import timezone
1415

16+
from ietf.doc.utils_r2 import rfcs_are_in_r2
17+
from ietf.doc.utils_red import trigger_red_precomputer
1518
from ietf.utils import log, searchindex
1619
from ietf.utils.timezone import datetime_today
1720

@@ -169,6 +172,20 @@ def signal_update_rfc_metadata_task(rfc_number_list=()):
169172
signal_update_rfc_metadata(rfc_number_list)
170173

171174

175+
@shared_task(bind=True)
176+
def trigger_red_precomputer_task(self, rfc_number_list=()):
177+
if not rfcs_are_in_r2(rfc_number_list):
178+
log.log(f"Objects are not yet in R2 for RFCs {rfc_number_list}")
179+
try:
180+
countdown = getattr(settings, "RED_PRECOMPUTER_TRIGGER_RETRY_DELAY", 10)
181+
max_retries = getattr(settings, "RED_PRECOMPUTER_TRIGGER_MAX_RETRIES", 12)
182+
self.retry(countdown=countdown, max_retries=max_retries)
183+
except MaxRetriesExceededError:
184+
log.log(f"Gave up waiting for objects in R2 for RFCs {rfc_number_list}")
185+
else:
186+
trigger_red_precomputer(rfc_number_list)
187+
188+
172189
@shared_task(bind=True)
173190
def update_rfc_searchindex_task(self, rfc_number: int):
174191
"""Update the search index for one RFC"""

0 commit comments

Comments
 (0)