Skip to content

Commit b65a614

Browse files
authored
Merge pull request #28793 from EPortman/28777_rfc_for_namex_emailer_cloud_tasks
28777 rfc to introduce gcp cloud tasks to the namex emailer
2 parents 6452a29 + 9a26cba commit b65a614

3 files changed

Lines changed: 168 additions & 0 deletions

File tree

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
- Start Date: 2025-06-03
2+
- Entity Issue: [bcgov/entity#21835](https://github.com/bcgov/entity/issues/21835)
3+
- Implementation PR: https://github.com/bcgov/namex/pull/1820
4+
5+
# Summary
6+
7+
The Names Team will integrate [**_GCP Cloud Tasks_**](https://cloud.google.com/tasks/docs) into the namex emailer to introduce a 5-minute grace period for approval, conditional, and rejection emails. Rather than sending every decision immediately, the emailer worker will:
8+
9+
1. Cancel any existing Cloud Task for that NR.
10+
2. Enqueue a new task, scheduled for 5 minutes in the future, containing the latest decision (NR number & option).
11+
12+
If a new decision arrives within five minutes, the previous task is revoked and replaced. Only when no further decision arrives for five minutes will the Cloud Task "survive" and invoke the namex emailer `/handle-send` endpoint and send the email. This prevents sending multiple emails if an examiner changes their mind quickly.
13+
14+
All other email types (initial notifications, consent letters, resends) continue to send immediately; only `APPROVED`, `CONDITIONAL`, and `REJECTED` decisions (excluding resends) follow this cloud tasks async flow.
15+
16+
# Basic Example
17+
18+
1. Name Examiner approves `NR 1234567` at 2:00 pm.
19+
2. Namex API publishes an "Approved" CE (Cloud Event) to Emailer Topic, and Emailer Worker picks it up.
20+
3. Worker sees the `APPROVED` option, deletes any existing task for that NR number, and creates a new Cloud Task:
21+
- **Name**: `NR_1234567-APPROVED-{random-6-digits}`
22+
- **Schedule time**: 2:00 pm + 5 min = 2:05 pm
23+
- **HTTP push**: POST `https://<namex_emailer>/tasks/handle-send`
24+
- **Body**: Includes NR-Number and Option
25+
4. At 2:02 pm, the examiner changes to "Conditional": Namex API publishes a "Conditional" CE.
26+
5. Worker revokes `NR_1234567-APPROVED-{random-6-digits}`, then enqueues a new task with the “Conditional” payload scheduled for 2:07 pm called `NR_1234567-CONDITIONAL-{random-6-digits}`
27+
6. No new events for `NR 1234567` arrive between 2:02 pm and 2:07 pm.
28+
7. At 2:07 pm, Cloud Tasks POSTs the "Conditional" CE to `/tasks/handle-send`.
29+
8. The `handle-send` handler builds and sends the conditional email; the client receives only that one email.
30+
31+
Note: The `{random-6-digits}` in the task name is a short hex UUID to ensure uniqueness. Cloud Tasks does not allow a task name to be reused for at least one hour after completion or deletion. This suffix prevents name collisions during rapid rescheduling - `NR_123_APPROVED_4hd231` and `NR_123_APPROVED_h3d987` won’t conflict, even if the same option is scheduled multiple times for the same NR.
32+
33+
# Motivation
34+
35+
To stop clients from getting spammed with emails when examiners make mistakes and change their minds in a quick succession. Without an external service like cloud tasks the Namex Emailer runs as a Flask WSGI service, so each request is handled synchronously by a single Gunicorn worker. Once a worker starts processing an incoming cloud event, it must finish that entire request before picking up the next one. There is no built-in mechanism to pause, delay, or maintain state between separate HTTP calls. As a result, we cannot implement a _"wait 5 minutes for a final decision"_ pattern purely within the existing WSGI request flow. By adding GCP Cloud Tasks as an external scheduler, we can offload the delay logic to a managed, cancelable queue. This allows us to "debounce" approval/conditional/rejection events without relying on in-process threads or long-running connections.
36+
37+
# Detailed Design
38+
39+
### High Level Flow
40+
41+
![Cloud Tasks Flow](rfc-cloud-tasks/emailer-cloud-tasks-diagram.png)
42+
43+
### Cloud Tasks Example Payload
44+
```json
45+
{
46+
"data": {
47+
"request": {
48+
"nrNum": "NR 6357469",
49+
"option": "APPROVED"
50+
}
51+
},
52+
"datacontenttype": "application/json",
53+
"id": "5ad47886-17c5-424f-9049-b1bca314d497",
54+
"source": "/requests/NR 6357469",
55+
"specversion": "1.0",
56+
"subject": "namerequest",
57+
"time": "2025-06-03T18:31:11.618490+00:00",
58+
"type": "bc.registry.names.request"
59+
}
60+
```
61+
62+
### Example of tasks on cloud tasks queue
63+
![Cloud Tasks on Queue](rfc-cloud-tasks/tasks_on_queue.png)
64+
65+
# Requirements List
66+
67+
- **Must Have:**
68+
- Debounce APPROVED, CONDITIONAL, and REJECTED emails via Cloud Tasks.
69+
- Immediate delivery for all other email types (notifications, consent letters, resends).
70+
- Dedicated Cloud Tasks queue with proper retry configuration.
71+
- `/handle-send` endpoint in the emailer to process Cloud Tasks callbacks.
72+
- Emailer service account granted `cloudtasks.tasks.list`, `cloudtasks.tasks.create`, and `cloudtasks.tasks.delete`; Cloud Tasks allowed to invoke `/handle-send`.
73+
- Idempotency cache (ce_cache) to skip duplicate CEs.
74+
75+
- **Should Have:**
76+
- End-to-end tests validating task scheduling, cancellation, and final send.
77+
- Monitoring and alerts on Cloud Tasks failures.
78+
- Logs for task creation, cancellation, and `/handle-send` execution.
79+
- Error handling in `/handle-send` that returns non-200 to trigger Cloud Tasks retries.
80+
81+
- **Could Have:**
82+
- Feature flag to toggle debounce logic on or off.
83+
- Configurable debounce interval via environment variable.
84+
85+
- **Won’t Have:**
86+
- Debounce for non-decision or resend Cloud Events - they send immediately.
87+
- Multi-region or fallback queue support (single queue only).
88+
- Custom retry logic beyond Cloud Tasks’ built-in policy.
89+
- Tracking or listing of failed sends.
90+
91+
# Drawbacks
92+
93+
1. Increased complexity and additional infrastructure
94+
- Introducing Cloud Tasks adds a new GCP component to manage: queue setup, IAM roles, retry policies, etc. Developers and operators must become familiar with Cloud Tasks (console, CLI, logs) in addition to Pub/Sub and the existing emailer code.
95+
96+
2. Expanded IAM and security scope
97+
- The emailer’s service account now requires new Cloud Tasks permissions (`cloudtasks.tasks.list`, `cloudtasks.tasks.create`, and `cloudtasks.tasks.delete`).
98+
- Cloud Tasks also needs permission to invoke the emailers `/handle-send` endpoint, meaning we must configure a service account or OIDC token for secure task execution.
99+
100+
3. Dual‐endpoint responsibilities in the emailer
101+
- The emailer is no longer just a Pub/Sub consumer; it must also accept incoming HTTP requests from Cloud Tasks. This means adding and maintaining a second route (/handle-send) and handling two different invocation sources.
102+
103+
4. Cost considerations
104+
- Adding a new google cloud service will come with an additional cost. Although the payload is minimal in each cloud task (just NR number and option), it will inevitably increase the monthly cloud bill for bcgov.
105+
106+
5. No debounce on decision --> reset only flows
107+
- If an NR is approved, then immediately reset and no new decision follows, the originally scheduled email still sends—because reset events don’t go through the emailer.
108+
- This matches the current behavior, though we could adjust code to route reset events through the debounce logic if needed.
109+
110+
# Alternatives
111+
112+
1. **Use a database table + periodic sweep job**
113+
114+
**Logic**
115+
- When an Approved, Conditional, or Rejected CE (Cloud Event) arrives, write (or overwrite) a row in a new `email_debounce` table keyed by NR number and event type.
116+
- A scheduled job runs every 5 minutes. It queries the table for any NR whose latest CE is at least 5 minutes old, sends the corresponding email, and deletes that row.
117+
118+
**Pros**
119+
- Simple and easy to reason about; avoids introducing new cloud services.
120+
- No need for async infrastructure - can be implemented with a cron job.
121+
122+
**Cons**
123+
- Polling-based, so emails may be delayed up to 10 minutes depending on job alignment.
124+
- Requires creating a new job that runs frequently, which could increase operational complexity and cost.
125+
- Requires a new `email_debounce` table in the Namex DB, and the emailer service currently has no DB connection - this would need to be added.
126+
127+
2. **Chain into a second Pub/Sub topic**
128+
129+
**Logic**
130+
- When a decision CE arrives, the emailer republishes it to a dedicated "debounce" Pub/Sub topic, overwriting any existing message for that NR.
131+
- An async subscriber listens to this topic, waits 5 minutes (e.g., via `time.sleep`), and then sends the email.
132+
- If a newer message for the same NR arrives during the wait, the subscriber drops the older one and resets the timer.
133+
134+
**Pros**
135+
- Reuses existing Pub/Sub-based patterns already common within BCGov.
136+
- All asynchronous logic is isolated in a dedicated service, separate from the core emailer.
137+
138+
**Cons**
139+
- Effectively introduces a Pub/Sub → Pub/Sub chain, which increases complexity and likely results in duplicate code between services.
140+
- Requires provisioning both a new Pub/Sub topic and a separate service to handle delayed processing.
141+
142+
3. **Use Cloud Scheduler (GCP Scheduler API) to schedule a publish to the Pub/Sub topic**
143+
144+
**Logic**
145+
- When a decision is made in Namex API, instead of publishing directly to the emailer topic, it creates a one-time scheduled job that will publish to the emailer topic after a delay.
146+
- If another decision occurs within the 5-minute window, the existing scheduled job is deleted and replaced (from the API) with a new one containing the updated payload.
147+
- When the delay expires, Cloud Scheduler publishes the message to the emailer topic. The emailer worker picks it up and processes it as usual.
148+
- The emailer deletes the scheduled job after processing to prevent a buildup of leftover scheduled jobs (this is the only change needed in the emailer).
149+
150+
**Pros**
151+
- Very limited changes to the emailer, it only needs to delete the job after processing.
152+
- Cancellation and rescheduling are straightforward using predictable job names.
153+
- Scheduler jobs are visible and traceable in the GCP console or CLI.
154+
- Cloud Scheduler is already used in other BCGov services, so this approach wouldn’t introduce a new technology.
155+
156+
**Cons**
157+
- Requires managing job names and deletion logic to prevent conflicts.
158+
- Cloud Scheduler jobs do not automatically delete themselves like Cloud Tasks, so an extra step is required to identify and remove the job after it completes.
159+
- Cloud Scheduler jobs appear individually in the GCP Console under the Scheduler Jobs page, and they are not grouped, which can make management and tracking more cluttered compared to Cloud Tasks.
160+
161+
162+
# Adoption Strategy
163+
164+
Deploy the Cloud Tasks changes to the Dev and Test environments, and verify that the queue functions correctly under thorough monitoring. Once validated, create the production Cloud Tasks queue, grant the necessary permissions, and deploy the changes to production for release.
165+
166+
# Unresolved Questions
167+
168+
None
35.7 KB
Loading
62.3 KB
Loading

0 commit comments

Comments
 (0)