Merge lp:~abentley/launchpad/recipe-build-email into lp:launchpad

Proposed by Aaron Bentley on 2010-06-09
Status: Merged
Approved by: Jelmer Vernooij on 2010-06-10
Approved revision: no longer in the source branch.
Merged at revision: 10992
Proposed branch: lp:~abentley/launchpad/recipe-build-email
Merge into: lp:launchpad
Diff against target: 508 lines (+273/-44)
8 files modified
lib/canonical/launchpad/emailtemplates/build-request.txt (+1/-0)
lib/lp/buildmaster/model/buildbase.py (+11/-5)
lib/lp/code/mail/branch.py (+8/-30)
lib/lp/code/mail/sourcepackagerecipebuild.py (+65/-0)
lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py (+59/-0)
lib/lp/code/model/sourcepackagerecipebuild.py (+13/-3)
lib/lp/code/model/tests/test_sourcepackagerecipebuild.py (+59/-3)
lib/lp/services/mail/basemailer.py (+57/-3)
To merge this branch: bzr merge lp:~abentley/launchpad/recipe-build-email
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) code Approve on 2010-06-10
Paul Hummer (community) code 2010-06-09 Approve on 2010-06-10
Review via email: mp+27199@code.launchpad.net

Commit Message

Notification for sourcepackagerecipebuilds.

Description of the Change

= Summary =
Fix bug #509893: Work out notifications for SourcePackageRecipeBuild

== Proposed fix ==
Provide notifications to the person who requested the build in all cases.

== Pre-implementation notes ==
Preimplementation was with thumper.

== Implementation details ==
Moved RecipientReason into basemailer because it's a core part of how
BaseMailer functions. Added BaseMailer_getFooter so we can provide footers
more conveniently.

Tweaked handle_status_for_build to accept a BuildBase subclass, to allow
polymorphism to work.

== Tests ==
bin/test -t test_generateEmail -t test_notify -t test_handleStatusNotifies

== Demo and Q/A ==
Request a build. An email should be sent when the build is complete.

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/code/model/tests/test_sourcepackagerecipebuild.py
  lib/lp/code/mail/sourcepackagerecipebuild.py
  lib/lp/buildmaster/model/buildbase.py
  lib/lp/code/mail/branch.py
  lib/lp/code/model/sourcepackagerecipebuild.py
  lib/canonical/launchpad/emailtemplates/build-request.txt
  lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py
  lib/lp/services/mail/basemailer.py

To post a comment you must log in.
Paul Hummer (rockstar) wrote :

Wow, the base mailer stuff has changed a lot since I last touched it. This looks good to land.

review: Approve (code)
Jelmer Vernooij (jelmer) wrote :

Nice.

Is there a particular reason that forBuildRequester is a class method and makeRational is static?

I realize they weren't documented earlier, but it would be nice to have documentation for the arguments for ``handle_status_for_build``.

review: Approve (code)
Aaron Bentley (abentley) wrote :

makeRationale doesn't need to refer to its class, so it can be a static method. forBuildRequester does refer to its class, so it must be a class method.

handle_status_for_build was introduced two days ago. If you think that it should not have been approved without a fuller docstring, please contact its reviewer, Graham Binns. https://code.edge.launchpad.net/~michael.nelson/launchpad/587113-buildbase-handleStatus/+merge/27022

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lib/canonical/launchpad/emailtemplates/build-request.txt'
2--- lib/canonical/launchpad/emailtemplates/build-request.txt 1970-01-01 00:00:00 +0000
3+++ lib/canonical/launchpad/emailtemplates/build-request.txt 2010-06-10 21:13:30 +0000
4@@ -0,0 +1,1 @@
5+Build %(recipe_owner)s/%(recipe)s into %(archive)s for %(distroseries)s: %(status)s.
6
7=== modified file 'lib/lp/buildmaster/model/buildbase.py'
8--- lib/lp/buildmaster/model/buildbase.py 2010-06-07 10:43:01 +0000
9+++ lib/lp/buildmaster/model/buildbase.py 2010-06-10 21:13:30 +0000
10@@ -3,10 +3,12 @@
11
12 # pylint: disable-msg=E0211,E0213
13
14+
15+"""Common build base classes."""
16+
17+
18 from __future__ import with_statement
19
20-"""Common build base classes."""
21-
22 __metaclass__ = type
23
24 __all__ = [
25@@ -40,7 +42,8 @@
26 UPLOAD_LOG_FILENAME = 'uploader.log'
27
28
29-def handle_status_for_build(build, status, librarian, slave_status):
30+def handle_status_for_build(build, status, librarian, slave_status,
31+ build_class=None):
32 """Find and call the correct method for handling the build status.
33
34 This is extracted from build base so that the implementation
35@@ -48,7 +51,9 @@
36 """
37 logger = logging.getLogger(BUILDD_MANAGER_LOG_NAME)
38
39- method = getattr(BuildBase, '_handleStatus_' + status, None)
40+ if build_class is None:
41+ build_class = BuildBase
42+ method = getattr(build_class, '_handleStatus_' + status, None)
43
44 if method is None:
45 logger.critical("Unknown BuildStatus '%s' for builder '%s'"
46@@ -150,7 +155,8 @@
47
48 def handleStatus(self, status, librarian, slave_status):
49 """See `IBuildBase`."""
50- return handle_status_for_build(self, status, librarian, slave_status)
51+ return handle_status_for_build(
52+ self, status, librarian, slave_status, self.__class__)
53
54 @staticmethod
55 def _handleStatus_OK(build, librarian, slave_status, logger):
56
57=== modified file 'lib/lp/code/mail/branch.py'
58--- lib/lp/code/mail/branch.py 2010-04-26 01:58:45 +0000
59+++ lib/lp/code/mail/branch.py 2010-06-10 21:13:30 +0000
60@@ -13,6 +13,7 @@
61 BranchSubscriptionDiffSize, BranchSubscriptionNotificationLevel,
62 CodeReviewNotificationLevel)
63 from lp.registry.interfaces.person import IPerson
64+from lp.services.mail import basemailer
65 from lp.services.mail.basemailer import BaseMailer
66
67
68@@ -27,19 +28,16 @@
69 mailer.sendAll()
70
71
72-class RecipientReason:
73- """Reason for sending mail to a recipient."""
74+class RecipientReason(basemailer.RecipientReason):
75
76 def __init__(self, subscriber, recipient, branch, mail_header,
77 reason_template, merge_proposal=None,
78 max_diff_lines=BranchSubscriptionDiffSize.WHOLEDIFF,
79 branch_identity_cache=None,
80 review_level=CodeReviewNotificationLevel.FULL):
81- self.subscriber = subscriber
82- self.recipient = recipient
83+ super(RecipientReason, self).__init__(subscriber, recipient,
84+ mail_header, reason_template)
85 self.branch = branch
86- self.mail_header = mail_header
87- self.reason_template = reason_template
88 self.merge_proposal = merge_proposal
89 self.max_diff_lines = max_diff_lines
90 if branch_identity_cache is None:
91@@ -129,20 +127,9 @@
92 ' details.',
93 branch_identity_cache=branch_identity_cache)
94
95- @staticmethod
96- def makeRationale(rationale_base, person):
97- if person.is_team:
98- return '%s @%s' % (rationale_base, person.name)
99- else:
100- return rationale_base
101-
102- def getReason(self):
103- """Return a string explaining why the recipient is a recipient."""
104- template_values = {
105- 'branch_name': self._getBranchIdentity(self.branch),
106- 'entity_is': 'You are',
107- 'lc_entity_is': 'you are',
108- }
109+ def _getTemplateValues(self):
110+ template_values = super(RecipientReason, self)._getTemplateValues()
111+ template_values['branch_name'] = self._getBranchIdentity(self.branch)
112 if self.merge_proposal is not None:
113 source = self._getBranchIdentity(
114 self.merge_proposal.source_branch)
115@@ -150,16 +137,7 @@
116 self.merge_proposal.target_branch)
117 template_values['merge_proposal'] = (
118 'the proposed merge of %s into %s' % (source, target))
119- if self.recipient != self.subscriber:
120- assert self.recipient.hasParticipationEntryFor(self.subscriber), (
121- '%s does not participate in team %s.' %
122- (self.recipient.displayname, self.subscriber.displayname))
123- if self.recipient != self.subscriber or self.subscriber.is_team:
124- template_values['entity_is'] = (
125- 'Your team %s is' % self.subscriber.displayname)
126- template_values['lc_entity_is'] = (
127- 'your team %s is' % self.subscriber.displayname)
128- return (self.reason_template % template_values)
129+ return template_values
130
131
132 class BranchMailer(BaseMailer):
133
134=== added file 'lib/lp/code/mail/sourcepackagerecipebuild.py'
135--- lib/lp/code/mail/sourcepackagerecipebuild.py 1970-01-01 00:00:00 +0000
136+++ lib/lp/code/mail/sourcepackagerecipebuild.py 2010-06-10 21:13:30 +0000
137@@ -0,0 +1,65 @@
138+# Copyright 2010 Canonical Ltd. This software is licensed under the
139+# GNU Affero General Public License version 3 (see the file LICENSE).
140+
141+
142+__metaclass__ = type
143+
144+__all__ = [
145+ 'SourcePackageRecipeBuildMailer',
146+ ]
147+
148+
149+from canonical.config import config
150+from canonical.launchpad.webapp import canonical_url
151+from lp.services.mail.basemailer import BaseMailer, RecipientReason
152+
153+
154+class SourcePackageRecipeBuildMailer(BaseMailer):
155+
156+ @classmethod
157+ def forStatus(cls, build):
158+ """Create a mailer for notifying about build status.
159+
160+ :param build: The build to notify about the state of.
161+ """
162+ requester = build.requester
163+ recipients = {requester: RecipientReason.forBuildRequester(requester)}
164+ return cls(
165+ '%(status)s: %(recipe)s for %(distroseries)s',
166+ 'build-request.txt', recipients,
167+ config.canonical.noreply_from_address, build)
168+
169+ def __init__(self, subject, body_template, recipients, from_address,
170+ build):
171+ BaseMailer.__init__(
172+ self, subject, body_template, recipients, from_address,
173+ notification_type='recipe-build-status')
174+ self.build = build
175+
176+ def _getHeaders(self, email):
177+ """See `BaseMailer`"""
178+ headers = super(
179+ SourcePackageRecipeBuildMailer, self)._getHeaders(email)
180+ headers.update({
181+ 'X-Launchpad-Build-State': self.build.status.name,
182+ })
183+ return headers
184+
185+ def _getTemplateParams(self, email):
186+ """See `BaseMailer`"""
187+ params = super(
188+ SourcePackageRecipeBuildMailer, self)._getTemplateParams(email)
189+ params.update({
190+ 'status': self.build.buildstate.title,
191+ 'distroseries': self.build.distroseries.name,
192+ 'recipe': self.build.recipe.name,
193+ 'recipe_owner': self.build.recipe.owner.name,
194+ 'archive': self.build.archive.name,
195+ 'build_url': canonical_url(self.build),
196+ })
197+ return params
198+
199+ def _getFooter(self, params):
200+ """See `BaseMailer`"""
201+ return ('%(build_url)s\n'
202+ '%(reason)s\n' % params)
203
204=== added file 'lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py'
205--- lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py 1970-01-01 00:00:00 +0000
206+++ lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py 2010-06-10 21:13:30 +0000
207@@ -0,0 +1,59 @@
208+# Copyright 2010 Canonical Ltd. This software is licensed under the
209+# GNU Affero General Public License version 3 (see the file LICENSE).
210+
211+
212+__metaclass__ = type
213+
214+
215+from unittest import TestLoader
216+
217+from canonical.config import config
218+from canonical.launchpad.interfaces.lpstorm import IStore
219+from canonical.testing import DatabaseFunctionalLayer
220+from lp.buildmaster.interfaces.buildbase import BuildStatus
221+from lp.code.mail.sourcepackagerecipebuild import (
222+ SourcePackageRecipeBuildMailer)
223+from lp.testing import TestCaseWithFactory
224+
225+
226+class TestSourcePackageRecipeBuildMailer(TestCaseWithFactory):
227+
228+ layer = DatabaseFunctionalLayer
229+
230+ def test_generateEmail(self):
231+ """GenerateEmail produces the right headers and body."""
232+ person = self.factory.makePerson(name='person')
233+ cake = self.factory.makeSourcePackageRecipe(
234+ name=u'recipe', owner=person)
235+ pantry = self.factory.makeArchive(name='ppa')
236+ secret = self.factory.makeDistroSeries(name=u'distroseries')
237+ build = self.factory.makeSourcePackageRecipeBuild(
238+ recipe=cake, distroseries=secret, archive=pantry,
239+ status=BuildStatus.FULLYBUILT)
240+ IStore(build).flush()
241+ mailer = SourcePackageRecipeBuildMailer.forStatus(build)
242+ email = build.requester.preferredemail.email
243+ ctrl = mailer.generateEmail(email, build.requester)
244+ self.assertEqual('Successfully built: recipe for distroseries',
245+ ctrl.subject)
246+ body, footer = ctrl.body.split('\n-- \n')
247+ self.assertEqual(
248+ 'Build person/recipe into ppa for distroseries: Successfully'
249+ ' built.\n', body
250+ )
251+ self.assertEqual(
252+ 'http://code.launchpad.dev/~person/+recipe/recipe/+build/1\n'
253+ 'You are the requester of the build.\n', footer)
254+ self.assertEqual(
255+ config.canonical.noreply_from_address, ctrl.from_addr)
256+ self.assertEqual(
257+ 'Requester', ctrl.headers['X-Launchpad-Message-Rationale'])
258+ self.assertEqual(
259+ 'recipe-build-status',
260+ ctrl.headers['X-Launchpad-Notification-Type'])
261+ self.assertEqual(
262+ 'FULLYBUILT', ctrl.headers['X-Launchpad-Build-State'])
263+
264+
265+def test_suite():
266+ return TestLoader().loadTestsFromName(__name__)
267
268=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
269--- lib/lp/code/model/sourcepackagerecipebuild.py 2010-06-09 15:32:32 +0000
270+++ lib/lp/code/model/sourcepackagerecipebuild.py 2010-06-10 21:13:30 +0000
271@@ -34,6 +34,8 @@
272 from lp.code.interfaces.sourcepackagerecipebuild import (
273 ISourcePackageRecipeBuildJob, ISourcePackageRecipeBuildJobSource,
274 ISourcePackageRecipeBuild, ISourcePackageRecipeBuildSource)
275+from lp.code.mail.sourcepackagerecipebuild import (
276+ SourcePackageRecipeBuildMailer)
277 from lp.registry.interfaces.pocket import PackagePublishingPocket
278 from lp.services.job.model.job import Job
279 from lp.soyuz.adapters.archivedependencies import (
280@@ -65,7 +67,8 @@
281 def binary_builds(self):
282 """See `ISourcePackageRecipeBuild`."""
283 return Store.of(self).find(BinaryPackageBuild,
284- BinaryPackageBuild.source_package_release==SourcePackageRelease.id,
285+ BinaryPackageBuild.source_package_release==
286+ SourcePackageRelease.id,
287 SourcePackageRelease.source_package_recipe_build==self.id)
288
289 buildduration = TimeDelta(name='build_duration', default=None)
290@@ -240,8 +243,8 @@
291
292 def notify(self, extra_info=None):
293 """See `IBuildBase`."""
294- # XXX: wgrant 2010-01-20 bug=509893: Implement this.
295- return
296+ mailer = SourcePackageRecipeBuildMailer.forStatus(self)
297+ mailer.sendAll()
298
299 def getFileByName(self, filename):
300 """See `ISourcePackageRecipeBuild`."""
301@@ -253,6 +256,13 @@
302 except KeyError:
303 raise NotFoundError(filename)
304
305+ @staticmethod
306+ def _handleStatus_OK(build, librarian, slave_status, logger):
307+ """See `IBuildBase`."""
308+ BuildBase._handleStatus_OK(build, librarian, slave_status, logger)
309+ # base implementation doesn't notify on success.
310+ if build.status == BuildStatus.FULLYBUILT:
311+ build.notify()
312
313 class SourcePackageRecipeBuildJob(BuildFarmJobOldDerived, Storm):
314 classProvides(ISourcePackageRecipeBuildJobSource)
315
316=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
317--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-06-09 15:32:32 +0000
318+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-06-10 21:13:30 +0000
319@@ -8,6 +8,7 @@
320 __metaclass__ = type
321
322 import datetime
323+import re
324 import unittest
325
326 from pytz import utc
327@@ -16,9 +17,10 @@
328 from zope.component import getUtility
329 from zope.security.proxy import removeSecurityProxy
330
331-from canonical.testing.layers import LaunchpadFunctionalLayer
332-
333+from canonical.testing.layers import (
334+ LaunchpadFunctionalLayer, LaunchpadZopelessLayer)
335 from canonical.launchpad.interfaces.launchpad import NotFoundError
336+from canonical.launchpad.interfaces.lpstorm import IStore
337 from canonical.launchpad.webapp.authorization import check_permission
338 from lp.buildmaster.interfaces.buildbase import BuildStatus, IBuildBase
339 from lp.buildmaster.interfaces.buildqueue import IBuildQueue
340@@ -28,9 +30,13 @@
341 ISourcePackageRecipeBuildJob, ISourcePackageRecipeBuild,
342 ISourcePackageRecipeBuildSource)
343 from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild
344+from lp.services.mail.sendmail import format_address
345 from lp.soyuz.interfaces.processor import IProcessorFamilySet
346 from lp.soyuz.model.processor import ProcessorFamily
347+from lp.soyuz.tests.soyuzbuilddhelpers import WaitingSlave
348 from lp.testing import ANONYMOUS, login, person_logged_in, TestCaseWithFactory
349+from lp.testing.fakemethod import FakeMethod
350+from lp.testing.mail_helpers import pop_notifications
351
352
353 class TestSourcePackageRecipeBuild(TestCaseWithFactory):
354@@ -150,7 +156,6 @@
355 self.assertEqual(
356 datetime.timedelta(minutes=5), spb.estimateDuration())
357
358-
359 def test_datestarted(self):
360 """Datestarted is taken from job if not specified in the build.
361
362@@ -226,6 +231,57 @@
363 removeSecurityProxy(recent_build).datecreated += a_second
364 self.assertContentEqual([recent_build], get_recent())
365
366+class TestAsBuildmaster(TestCaseWithFactory):
367+
368+ layer = LaunchpadZopelessLayer
369+
370+ def test_notify(self):
371+ """Notify sends email."""
372+ person = self.factory.makePerson(name='person')
373+ cake = self.factory.makeSourcePackageRecipe(
374+ name=u'recipe', owner=person)
375+ pantry = self.factory.makeArchive(name='ppa')
376+ secret = self.factory.makeDistroSeries(name=u'distroseries')
377+ build = self.factory.makeSourcePackageRecipeBuild(
378+ recipe=cake, distroseries=secret, archive=pantry)
379+ removeSecurityProxy(build).buildstate = BuildStatus.FULLYBUILT
380+ IStore(build).flush()
381+ build.notify()
382+ (message,) = pop_notifications()
383+ requester = build.requester
384+ requester_address = format_address(
385+ requester.displayname, requester.preferredemail.email)
386+ self.assertEqual(
387+ requester_address, re.sub(r'\n\t+', ' ', message['To']))
388+ self.assertEqual('Successfully built: recipe for distroseries',
389+ message['Subject'])
390+ body, footer = message.get_payload(decode=True).split('\n-- \n')
391+ self.assertEqual(
392+ 'Build person/recipe into ppa for distroseries: Successfully'
393+ ' built.\n', body
394+ )
395+
396+ def test_handleStatusNotifies(self):
397+ """"handleStatus causes notification, even if OK."""
398+ def prepare_build():
399+ queue_record = self.factory.makeSourcePackageRecipeBuildJob()
400+ build = queue_record.specific_job.build
401+ removeSecurityProxy(build).buildstate = BuildStatus.FULLYBUILT
402+ queue_record.builder = self.factory.makeBuilder()
403+ slave = WaitingSlave('BuildStatus.OK')
404+ queue_record.builder.setSlaveForTesting(slave)
405+ return build
406+ def assertNotifyOnce(status, build):
407+ build.handleStatus(status, None, {'filemap': {}})
408+ self.assertEqual(1, len(pop_notifications()))
409+ for status in ['PACKAGEFAIL', 'OK']:
410+ assertNotifyOnce(status, prepare_build())
411+ build = prepare_build()
412+ removeSecurityProxy(build).verifySuccessfulUpload = FakeMethod(
413+ result=True)
414+ assertNotifyOnce('OK', prepare_build())
415+
416+
417 class MakeSPRecipeBuildMixin:
418 """Provide the common makeBuild method returning a queued build."""
419
420
421=== modified file 'lib/lp/services/mail/basemailer.py'
422--- lib/lp/services/mail/basemailer.py 2010-03-18 22:41:03 +0000
423+++ lib/lp/services/mail/basemailer.py 2010-06-10 21:13:30 +0000
424@@ -5,7 +5,7 @@
425
426 __metaclass__ = type
427
428-__all__ = ['BaseMailer']
429+__all__ = ['BaseMailer', 'RecipientReason']
430
431 import logging
432 from smtplib import SMTPException
433@@ -14,7 +14,9 @@
434
435 from lp.services.mail.notificationrecipientset import (
436 NotificationRecipientSet)
437-from lp.services.mail.sendmail import format_address, MailController
438+from lp.services.mail.sendmail import (
439+ append_footer, format_address, MailController
440+)
441 from lp.services.utils import text_delta
442
443
444@@ -130,7 +132,16 @@
445 def _getBody(self, email):
446 """Return the complete body to use for this email."""
447 template = get_email_template(self._template_name)
448- return template % self._getTemplateParams(email)
449+ params = self._getTemplateParams(email)
450+ body = template % params
451+ footer = self._getFooter(params)
452+ if footer is not None:
453+ body = append_footer(body, footer)
454+ return body
455+
456+ def _getFooter(self, params):
457+ """Provide a footer to attach to the body, or None."""
458+ return None
459
460 def sendAll(self):
461 """Send notifications to all recipients."""
462@@ -150,3 +161,46 @@
463 # Don't want an entire stack trace, just some details.
464 self.logger.warning(
465 'send failed for %s, %s' % (email, e))
466+
467+
468+class RecipientReason:
469+ """Reason for sending mail to a recipient."""
470+
471+ def __init__(self, subscriber, recipient, mail_header, reason_template):
472+ self.subscriber = subscriber
473+ self.recipient = recipient
474+ self.mail_header = mail_header
475+ self.reason_template = reason_template
476+
477+ @staticmethod
478+ def makeRationale(rationale_base, person):
479+ if person.is_team:
480+ return '%s @%s' % (rationale_base, person.name)
481+ else:
482+ return rationale_base
483+
484+ def _getTemplateValues(self):
485+ template_values = {
486+ 'entity_is': 'You are',
487+ 'lc_entity_is': 'you are',
488+ }
489+ if self.recipient != self.subscriber:
490+ assert self.recipient.hasParticipationEntryFor(self.subscriber), (
491+ '%s does not participate in team %s.' %
492+ (self.recipient.displayname, self.subscriber.displayname))
493+ if self.recipient != self.subscriber or self.subscriber.is_team:
494+ template_values['entity_is'] = (
495+ 'Your team %s is' % self.subscriber.displayname)
496+ template_values['lc_entity_is'] = (
497+ 'your team %s is' % self.subscriber.displayname)
498+ return template_values
499+
500+ def getReason(self):
501+ """Return a string explaining why the recipient is a recipient."""
502+ return (self.reason_template % self._getTemplateValues())
503+
504+ @classmethod
505+ def forBuildRequester(cls, requester):
506+ header = cls.makeRationale('Requester', requester)
507+ reason = '%(entity_is)s the requester of the build.'
508+ return cls(requester, requester, header, reason)