Skip to content

Commit f0eae98

Browse files
authored
Use enum to differentiate different results of running a step (#761)
In this commit, we introduce an Enum that represents the different ways a step can resolve. This will: - help us track how often certain steps are called when they do "meaningful work" - helps as avoid flow-control via Exceptions
1 parent 3e7887b commit f0eae98

File tree

5 files changed

+145
-116
lines changed

5 files changed

+145
-116
lines changed

jbi/errors.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,3 @@ class IgnoreInvalidRequestError(Exception):
1111

1212
class ActionError(Exception):
1313
"""Error occurred during Action handling"""
14-
15-
16-
class IncompleteStepError(Exception):
17-
"""Raised when a step could not complete successfully."""
18-
19-
def __init__(self, context, *args: object) -> None:
20-
super().__init__(*args)
21-
self.context = context

jbi/runner.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,7 @@
1111
from jbi import ActionResult, Operation
1212
from jbi import steps as steps_module
1313
from jbi.environment import get_settings
14-
from jbi.errors import (
15-
ActionNotFoundError,
16-
IgnoreInvalidRequestError,
17-
IncompleteStepError,
18-
)
14+
from jbi.errors import ActionNotFoundError, IgnoreInvalidRequestError
1915
from jbi.models import (
2016
ActionContext,
2117
ActionParams,
@@ -26,6 +22,7 @@
2622
RunnerContext,
2723
)
2824
from jbi.services import bugzilla, jira
25+
from jbi.steps import StepStatus
2926

3027
logger = logging.getLogger(__name__)
3128

@@ -102,16 +99,16 @@ def __call__(self, context: ActionContext) -> ActionResult:
10299

103100
for step in self.steps[context.operation]:
104101
context = context.update(current_step=step.__name__)
102+
step_kwargs = self.build_step_kwargs(step)
105103
try:
106-
step_kwargs = self.build_step_kwargs(step)
107-
context = step(context=context, **step_kwargs)
108-
statsd.incr(f"jbi.steps.{step.__name__}.count")
109-
except IncompleteStepError as exc:
110-
# Step did not execute all its operations.
111-
context = exc.context
112-
statsd.incr(
113-
f"jbi.action.{context.action.whiteboard_tag}.incomplete.count"
114-
)
104+
result, context = step(context=context, **step_kwargs)
105+
if result == StepStatus.SUCCESS:
106+
statsd.incr(f"jbi.steps.{step.__name__}.count")
107+
elif result == StepStatus.INCOMPLETE:
108+
# Step did not execute all its operations.
109+
statsd.incr(
110+
f"jbi.action.{context.action.whiteboard_tag}.incomplete.count"
111+
)
115112
except Exception:
116113
if has_produced_request:
117114
# Count the number of workflows that produced at least one request,

jbi/steps.py

Lines changed: 70 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,39 @@
1111
from __future__ import annotations
1212

1313
import logging
14+
from enum import Enum, auto
1415
from typing import TYPE_CHECKING, Optional
1516

1617
from requests import exceptions as requests_exceptions
1718

1819
from jbi import Operation
19-
from jbi.errors import IncompleteStepError
20+
21+
22+
class StepStatus(Enum):
23+
"""
24+
Options for the result of executing a step function:
25+
SUCCESS: The step succeeded at doing meaningful work
26+
INCOMPLETE: The step did not execute successfully, but it's an error we anticipated
27+
NOOP: The step executed successfully, but didn't have any meaningful work to do
28+
"""
29+
30+
SUCCESS = auto()
31+
INCOMPLETE = auto()
32+
NOOP = auto()
33+
2034

2135
# https://docs.python.org/3.11/library/typing.html#typing.TYPE_CHECKING
2236
if TYPE_CHECKING:
2337
from jbi.models import ActionContext, ActionParams
2438
from jbi.services.bugzilla import BugzillaService
2539
from jbi.services.jira import JiraService
2640

41+
StepResult = tuple[StepStatus, ActionContext]
42+
2743
logger = logging.getLogger(__name__)
2844

2945

30-
def create_comment(context: ActionContext, *, jira_service: JiraService):
46+
def create_comment(context: ActionContext, *, jira_service: JiraService) -> StepResult:
3147
"""Create a Jira comment using `context.bug.comment`"""
3248
bug = context.bug
3349

@@ -36,11 +52,11 @@ def create_comment(context: ActionContext, *, jira_service: JiraService):
3652
"No matching comment found in payload",
3753
extra=context.model_dump(),
3854
)
39-
return context
55+
return (StepStatus.NOOP, context)
4056

4157
jira_response = jira_service.add_jira_comment(context)
4258
context = context.append_responses(jira_response)
43-
return context
59+
return (StepStatus.SUCCESS, context)
4460

4561

4662
def create_issue(
@@ -49,7 +65,7 @@ def create_issue(
4965
parameters: ActionParams,
5066
jira_service: JiraService,
5167
bugzilla_service: BugzillaService,
52-
):
68+
) -> StepResult:
5369
"""Create the Jira issue with the first comment as the description."""
5470
bug = context.bug
5571
issue_type = parameters.issue_type_map.get(bug.type or "", "Task")
@@ -65,29 +81,33 @@ def create_issue(
6581
jira=context.jira.update(issue=issue_key),
6682
)
6783
context = context.append_responses(jira_create_response)
68-
return context
84+
return (StepStatus.SUCCESS, context)
6985

7086

71-
def add_link_to_jira(context: ActionContext, *, bugzilla_service: BugzillaService):
87+
def add_link_to_jira(
88+
context: ActionContext, *, bugzilla_service: BugzillaService
89+
) -> StepResult:
7290
"""Add the URL to the Jira issue in the `see_also` field on the Bugzilla ticket"""
7391
bugzilla_response = bugzilla_service.add_link_to_jira(context)
7492
context = context.append_responses(bugzilla_response)
75-
return context
93+
return (StepStatus.SUCCESS, context)
7694

7795

78-
def add_link_to_bugzilla(context: ActionContext, *, jira_service: JiraService):
96+
def add_link_to_bugzilla(
97+
context: ActionContext, *, jira_service: JiraService
98+
) -> StepResult:
7999
"""Add the URL of the Bugzilla ticket to the links of the Jira issue"""
80100
jira_response = jira_service.add_link_to_bugzilla(context)
81101
context = context.append_responses(jira_response)
82-
return context
102+
return (StepStatus.SUCCESS, context)
83103

84104

85105
def maybe_delete_duplicate(
86106
context: ActionContext,
87107
*,
88108
bugzilla_service: BugzillaService,
89109
jira_service: JiraService,
90-
):
110+
) -> StepResult:
91111
"""
92112
In the time taken to create the Jira issue the bug may have been updated so
93113
re-retrieve it to ensure we have the latest data, and delete any duplicate
@@ -99,29 +119,36 @@ def maybe_delete_duplicate(
99119
)
100120
if jira_response_delete:
101121
context = context.append_responses(jira_response_delete)
102-
return context
122+
return (StepStatus.SUCCESS, context)
123+
return (StepStatus.NOOP, context)
103124

104125

105-
def update_issue_summary(context: ActionContext, *, jira_service: JiraService):
126+
def update_issue_summary(
127+
context: ActionContext, *, jira_service: JiraService
128+
) -> StepResult:
106129
"""Update the Jira issue's summary if the linked bug is modified."""
107130

108131
if "summary" not in context.event.changed_fields():
109-
return context
132+
return (StepStatus.NOOP, context)
110133

111134
jira_response_update = jira_service.update_issue_summary(context)
112135
context = context.append_responses(jira_response_update)
113-
return context
136+
return (StepStatus.SUCCESS, context)
114137

115138

116-
def add_jira_comments_for_changes(context: ActionContext, *, jira_service: JiraService):
139+
def add_jira_comments_for_changes(
140+
context: ActionContext, *, jira_service: JiraService
141+
) -> StepResult:
117142
"""Add a Jira comment for each field (assignee, status, resolution) change on
118143
the Bugzilla ticket."""
119144
comments_responses = jira_service.add_jira_comments_for_changes(context)
120145
context.append_responses(comments_responses)
121-
return context
146+
return (StepStatus.SUCCESS, context)
122147

123148

124-
def maybe_assign_jira_user(context: ActionContext, *, jira_service: JiraService):
149+
def maybe_assign_jira_user(
150+
context: ActionContext, *, jira_service: JiraService
151+
) -> StepResult:
125152
"""Assign the user on the Jira issue, based on the Bugzilla assignee email.
126153
127154
It will attempt to assign the Jira issue the same person as the bug is assigned to. This relies on
@@ -134,19 +161,19 @@ def maybe_assign_jira_user(context: ActionContext, *, jira_service: JiraService)
134161

135162
if context.operation == Operation.CREATE:
136163
if not bug.is_assigned():
137-
return context
164+
return (StepStatus.NOOP, context)
138165

139166
try:
140167
resp = jira_service.assign_jira_user(context, bug.assigned_to) # type: ignore
141168
context.append_responses(resp)
142-
return context
169+
return (StepStatus.SUCCESS, context)
143170
except ValueError as exc:
144171
logger.debug(str(exc), extra=context.model_dump())
145-
raise IncompleteStepError(context) from exc
172+
return (StepStatus.INCOMPLETE, context)
146173

147174
if context.operation == Operation.UPDATE:
148175
if "assigned_to" not in event.changed_fields():
149-
return context
176+
return (StepStatus.SUCCESS, context)
150177

151178
if not bug.is_assigned():
152179
resp = jira_service.clear_assignee(context)
@@ -158,15 +185,14 @@ def maybe_assign_jira_user(context: ActionContext, *, jira_service: JiraService)
158185
# If that failed then just fall back to clearing the assignee.
159186
resp = jira_service.clear_assignee(context)
160187
context.append_responses(resp)
161-
return context
188+
return (StepStatus.SUCCESS, context)
162189

163-
# This happens when exceptions are raised an ignored.
164-
return context
190+
return (StepStatus.NOOP, context)
165191

166192

167193
def maybe_update_issue_resolution(
168194
context: ActionContext, *, parameters: ActionParams, jira_service: JiraService
169-
):
195+
) -> StepResult:
170196
"""
171197
Update the Jira issue status
172198
https://support.atlassian.com/jira-cloud-administration/docs/what-are-issue-statuses-priorities-and-resolutions/
@@ -183,25 +209,25 @@ def maybe_update_issue_resolution(
183209
operation=Operation.IGNORE,
184210
).model_dump(),
185211
)
186-
raise IncompleteStepError(context)
212+
return (StepStatus.INCOMPLETE, context)
187213

188214
if context.operation == Operation.CREATE:
189215
resp = jira_service.update_issue_resolution(context, jira_resolution)
190216
context.append_responses(resp)
191-
return context
217+
return (StepStatus.SUCCESS, context)
192218

193219
if context.operation == Operation.UPDATE:
194220
if "resolution" in context.event.changed_fields():
195221
resp = jira_service.update_issue_resolution(context, jira_resolution)
196222
context.append_responses(resp)
197-
return context
223+
return (StepStatus.SUCCESS, context)
198224

199-
return context
225+
return (StepStatus.NOOP, context)
200226

201227

202228
def maybe_update_issue_status(
203229
context: ActionContext, *, parameters: ActionParams, jira_service: JiraService
204-
):
230+
) -> StepResult:
205231
"""
206232
Update the Jira issue resolution
207233
https://support.atlassian.com/jira-cloud-administration/docs/what-are-issue-statuses-priorities-and-resolutions/
@@ -217,27 +243,27 @@ def maybe_update_issue_status(
217243
operation=Operation.IGNORE,
218244
).model_dump(),
219245
)
220-
raise IncompleteStepError(context)
246+
return (StepStatus.INCOMPLETE, context)
221247

222248
if context.operation == Operation.CREATE:
223249
resp = jira_service.update_issue_status(context, jira_status)
224250
context.append_responses(resp)
225-
return context
251+
return (StepStatus.SUCCESS, context)
226252

227253
if context.operation == Operation.UPDATE:
228254
changed_fields = context.event.changed_fields()
229255

230256
if "status" in changed_fields or "resolution" in changed_fields:
231257
resp = jira_service.update_issue_status(context, jira_status)
232258
context.append_responses(resp)
233-
return context
259+
return (StepStatus.SUCCESS, context)
234260

235-
return context
261+
return (StepStatus.NOOP, context)
236262

237263

238264
def maybe_update_components(
239265
context: ActionContext, *, parameters: ActionParams, jira_service: JiraService
240-
):
266+
) -> StepResult:
241267
"""
242268
Update the Jira issue components
243269
"""
@@ -254,7 +280,7 @@ def maybe_update_components(
254280

255281
if not candidate_components:
256282
# no components to update
257-
return context
283+
return (StepStatus.NOOP, context)
258284

259285
# Although we previously introspected the project components, we
260286
# still have to catch any potential 400 error response here, because
@@ -280,18 +306,18 @@ def maybe_update_components(
280306
extra=context.model_dump(),
281307
)
282308
context.append_responses(exc.response)
283-
raise IncompleteStepError(context) from exc
309+
return (StepStatus.INCOMPLETE, context)
284310

285311
if missing_components:
286312
logger.warning(
287313
"Could not find components '%s' in project",
288314
",".join(sorted(missing_components)),
289315
extra=context.model_dump(),
290316
)
291-
raise IncompleteStepError(context)
317+
return (StepStatus.INCOMPLETE, context)
292318

293319
context.append_responses(resp)
294-
return context
320+
return (StepStatus.SUCCESS, context)
295321

296322

297323
def _whiteboard_as_labels(labels_brackets: str, whiteboard: Optional[str]) -> list[str]:
@@ -328,7 +354,7 @@ def _build_labels_update(
328354

329355
def sync_whiteboard_labels(
330356
context: ActionContext, *, parameters: ActionParams, jira_service: JiraService
331-
):
357+
) -> StepResult:
332358
"""
333359
Set whiteboard tags as labels on the Jira issue.
334360
"""
@@ -342,8 +368,7 @@ def sync_whiteboard_labels(
342368
labels_brackets=parameters.labels_brackets,
343369
)
344370
else:
345-
# Whiteboard field not changed, ignore.
346-
return context
371+
return (StepStatus.NOOP, context)
347372
else:
348373
# On creation, just add them all.
349374
additions, removals = _build_labels_update(
@@ -368,7 +393,7 @@ def sync_whiteboard_labels(
368393
extra=context.model_dump(),
369394
)
370395
context.append_responses(exc.response)
371-
raise IncompleteStepError(context) from exc
396+
return (StepStatus.INCOMPLETE, context)
372397

373398
context.append_responses(resp)
374-
return context
399+
return (StepStatus.SUCCESS, context)

0 commit comments

Comments
 (0)