Skip to content

Commit 4314648

Browse files
committed
Update
[ghstack-poisoned]
1 parent cd06a0d commit 4314648

3 files changed

Lines changed: 54 additions & 130 deletions

File tree

aws/lambda/gha-log-uploader/README.md

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -33,31 +33,27 @@ twice and then DLQs it.
3333

3434
## Classification
3535

36-
After a log is stored, `log_classifier` is invoked with `InvocationType: "Event"`
37-
and the result is not awaited. `github-status-test` called it with an untimed
38-
`urlopen` and blocked until classification finished, which is what produced its
39-
274s/344s/400s/900s duration tails — the fix is the invocation type, not a
40-
separate function.
41-
42-
It is reached through `lambda:InvokeFunction` rather than its public function URL
43-
(`AuthType: NONE`), so the path from here to classification never crosses a
44-
public endpoint. A function URL would not work anyway: those only support the
45-
`RequestResponse` invocation type, so calling one means waiting for
46-
classification to finish.
47-
48-
`log_classifier` builds on `lambda_http` with only the `apigw_http` feature, so
49-
`classifier_payload()` reproduces the API Gateway HTTP API v2.0 request it
50-
expects even on a direct invoke. Verified against the deployed function: a
51-
payload with no `job_id` returns its 400 "no job id provided" branch, and a
52-
non-numeric one fails inside its `parse::<usize>()`, which together show both the
53-
envelope and the query string are read.
54-
55-
That envelope is coupling to another lambda's framework, and it would break if
56-
`log_classifier`'s handler changed. Teaching it to accept a plain
57-
`{"job_id", "repo"}` payload is the real fix, and lets its public function URL be
58-
retired once `backfillJobs.mjs`, `keep-going-call-log-classifier` and
59-
`github-status-test` move off it too — worth doing on its own, not as a rider on
60-
this migration.
36+
After a log is stored, `log_classifier` is called through its function URL —
37+
byte for byte the call `github-status-test` makes today.
38+
39+
That call is synchronous. Function URLs only support the `RequestResponse`
40+
invocation type, so this function's duration includes the classification, and
41+
`github-status-test`'s 274s/344s/400s/900s duration maxima come from exactly
42+
this. **Keep the timeout at 900s**: on a slow classification a shorter one would
43+
kill the invocation mid-wait, and since callers invoke asynchronously, Lambda
44+
would then retry the whole thing and re-download the log.
45+
46+
Unlike in `github-status-test` the tail is no longer harmful. There it ran behind
47+
API Gateway on the webhook's critical path, so a slow classification risked a
48+
GitHub webhook timeout. Here the caller has already returned, and a long
49+
invocation costs GB-seconds and a concurrency slot, nothing more.
50+
51+
The way out is `lambda:InvokeFunction` with `InvocationType: "Event"`, which
52+
needs `log_classifier` to accept a plain `{"job_id", "repo"}` payload — it
53+
currently only parses the API Gateway request its `lambda_http` handler expects.
54+
That change also lets its `AuthType: NONE` function URL be retired, once
55+
`backfillJobs.mjs`, `keep-going-call-log-classifier` and `github-status-test`
56+
move off it. Worth doing on its own, not as a rider on this migration.
6157

6258
A failed handoff is logged and reported as `classified: false`, not raised.
6359
Raising would make Lambda retry the whole function, re-downloading a
@@ -112,13 +108,12 @@ Notes on the app path:
112108

113109
Not done by CI. Needed before the deploy workflow can run.
114110

115-
1. Create the function: python3.12, x86_64, handler `lambda_function.lambda_handler`.
116-
512 MB and a 60s timeout are plenty — the old function averaged 200ms and its
117-
long tail was the classifier ping this one does not make.
118-
2. Give its execution role `s3:PutObject` on `arn:aws:s3:::ossci-raw-job-status/log/*`,
119-
`lambda:InvokeFunction` on
120-
`arn:aws:lambda:us-east-1:308535385114:function:log_classifier`, plus the
121-
usual CloudWatch Logs permissions.
111+
1. Create the function: python3.12, x86_64, handler `lambda_function.lambda_handler`,
112+
512 MB, **900s timeout** — matching `github-status-test`, because the
113+
synchronous classifier call means a slow classification is a slow invocation.
114+
2. Give its execution role `s3:PutObject` on `arn:aws:s3:::ossci-raw-job-status/log/*`
115+
plus the usual CloudWatch Logs permissions. No `lambda:InvokeFunction` is
116+
needed while the classifier is reached over its function URL.
122117
3. Set the env vars above. Prefer fresh credentials over copying
123118
`github-status-test`'s, whose PATs sit in plaintext env vars and are due for
124119
rotation.
@@ -134,9 +129,8 @@ Not done by CI. Needed before the deploy workflow can run.
134129
`OUR_AWS_ACCESS_KEY_ID` before granting.
135130
6. Create the `gha_workflow_gha-log-uploader-lambda` IAM role the deploy workflow
136131
assumes, mirroring `gha_workflow_github-status-test-lambda`.
137-
7. Nothing to wire for classification: this function invokes `log_classifier`
138-
directly, so there is no S3 notification to add. `keep-going-call-log-classifier`
139-
still covers the separate `temp_logs/` prefix on `gha-artifacts`.
132+
7. Nothing to wire for classification: the classifier is called over its existing
133+
function URL, so there is no notification or extra permission to add.
140134

141135
## Deployment
142136

aws/lambda/gha-log-uploader/lambda_function.py

Lines changed: 16 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,20 @@
77
API Gateway integration and no Lambda function URL: the only way in is
88
``lambda:InvokeFunction``, which is IAM-authenticated.
99
10-
The classifier is invoked the same way, asynchronously, so this function never
11-
blocks on classification finishing. ``github-status-test`` called it with an
12-
untimed ``urlopen`` and waited, which is what produced its 274s/344s/400s/900s
13-
duration tails -- the fix is the invocation type, not a separate function.
10+
Classification is kicked off exactly as ``github-status-test`` does it, through
11+
log_classifier's function URL. That call is synchronous, so this function's
12+
duration includes the classification -- which is why its timeout has to stay at
13+
900s. Switching to an async ``lambda:InvokeFunction`` needs log_classifier to
14+
accept a plain payload first; see the README.
1415
"""
1516

1617
import base64
1718
import contextlib
1819
import gzip
19-
import json
2020
import os
2121
import random
2222
import time
23+
from urllib.request import urlopen
2324

2425
import boto3
2526
import requests
@@ -28,13 +29,12 @@
2829

2930

3031
s3 = boto3.resource("s3")
31-
lambda_client = boto3.client("lambda")
3232
GITHUB_TOKENS = os.environ.get("GITHUB_TOKENS")
3333
GITHUB_APP_ID = os.environ.get("GITHUB_APP_ID")
3434
# Base64-encoded PEM, the same encoding torchci uses for its app key
3535
GITHUB_APP_PRIVATE_KEY = os.environ.get("GITHUB_APP_PRIVATE_KEY")
3636
BUCKET_NAME = "ossci-raw-job-status"
37-
LOG_CLASSIFIER_FUNCTION = "log_classifier"
37+
LOG_CLASSIFIER_URL = "https://vwg52br27lx5oymv4ouejwf4re0akoeg.lambda-url.us-east-1.on.aws"
3838

3939
GITHUB_API_URL = "https://api.github.com"
4040
# Installation tokens last an hour. Refresh early so a warm invocation never
@@ -135,66 +135,21 @@ def log_object_path(full_name, job_id):
135135
return f"log/{full_name}/{job_id}"
136136

137137

138-
def classifier_payload(full_name, job_id):
139-
"""An API Gateway HTTP API v2.0 request, which is what lambda_http parses.
140-
141-
log_classifier is built on lambda_http with only the `apigw_http` feature, so
142-
it expects this envelope even on a direct invoke. Verified against the
143-
deployed function: a payload with no `job_id` returns its 400 "no job id
144-
provided" branch, and a non-numeric one fails in its `parse::<usize>()`,
145-
which together show both the envelope and the query string are read.
146-
"""
147-
return {
148-
"version": "2.0",
149-
"routeKey": "$default",
150-
"rawPath": "/",
151-
"rawQueryString": f"job_id={job_id}&repo={full_name}",
152-
"headers": {},
153-
"queryStringParameters": {"job_id": str(job_id), "repo": full_name},
154-
"requestContext": {
155-
"accountId": "308535385114",
156-
"apiId": "gha-log-uploader",
157-
"domainName": "lambda-invoke",
158-
"domainPrefix": "lambda-invoke",
159-
"http": {
160-
"method": "GET",
161-
"path": "/",
162-
"protocol": "HTTP/1.1",
163-
"sourceIp": "127.0.0.1",
164-
"userAgent": "gha-log-uploader",
165-
},
166-
"requestId": f"gha-log-uploader-{job_id}",
167-
"routeKey": "$default",
168-
"stage": "$default",
169-
"time": "01/Jan/1970:00:00:00 +0000",
170-
"timeEpoch": 0,
171-
},
172-
"isBase64Encoded": False,
173-
}
174-
175-
176138
def classify_log(full_name, job_id):
177-
"""Kick off classification for a log we just stored. Returns True on handoff.
178-
179-
Asynchronous, and reached through `lambda:InvokeFunction` rather than
180-
log_classifier's public function URL, so the path from here to classification
181-
never crosses a public endpoint. A function URL would not do: it only supports
182-
the RequestResponse invocation type, so using one means waiting for
183-
classification to finish -- which is exactly the mistake that gave
184-
github-status-test its multi-hundred-second tails.
139+
"""Kick off classification for a log we just stored. Returns True on success.
140+
141+
Same call github-status-test makes. The function URL only supports the
142+
RequestResponse invocation type, so this blocks until classification
143+
finishes; the function's 900s timeout has to cover that.
185144
"""
186145
try:
187-
lambda_client.invoke(
188-
FunctionName=LOG_CLASSIFIER_FUNCTION,
189-
InvocationType="Event",
190-
Payload=json.dumps(classifier_payload(full_name, job_id)).encode(),
191-
)
146+
urlopen(f"{LOG_CLASSIFIER_URL}/?job_id={job_id}&repo={full_name}")
192147
return True
193148
except Exception as err:
194149
# Best effort, deliberately. Raising would make Lambda retry the whole
195-
# function, re-downloading a multi-megabyte log from GitHub to retry a
196-
# handoff that takes milliseconds. The log itself is already safe in S3.
197-
print(f"ERROR invoking the classifier for {full_name} job {job_id}: {err}")
150+
# function and re-download a multi-megabyte log from GitHub, when the log
151+
# itself is already safe in S3.
152+
print(f"ERROR calling the classifier for {full_name} job {job_id}: {err}")
198153
return False
199154

200155

aws/lambda/gha-log-uploader/test_lambda_function.py

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import json
21
import unittest
32
from unittest.mock import MagicMock, patch
43

54
import lambda_function
65
from lambda_function import (
7-
classifier_payload,
86
classify_log,
97
download_log,
108
installation_token,
@@ -56,43 +54,20 @@ def test_rejects_a_non_object_payload(self):
5654
parse_event(["pytorch/pytorch", 123])
5755

5856

59-
class TestClassifierPayload(unittest.TestCase):
60-
def test_is_a_v2_request_the_classifier_can_parse(self):
61-
payload = classifier_payload("pytorch/executorch", 999)
62-
# log_classifier builds on lambda_http with only the apigw_http feature,
63-
# so version 2.0 and requestContext.http are what make it deserialize.
64-
self.assertEqual(payload["version"], "2.0")
65-
self.assertIn("http", payload["requestContext"])
66-
self.assertEqual(
67-
payload["queryStringParameters"],
68-
{"job_id": "999", "repo": "pytorch/executorch"},
69-
)
70-
self.assertEqual(payload["rawQueryString"], "job_id=999&repo=pytorch/executorch")
71-
72-
def test_is_json_serializable(self):
73-
json.dumps(classifier_payload("pytorch/pytorch", 1))
74-
75-
7657
class TestClassifyLog(unittest.TestCase):
77-
def test_invokes_the_classifier_asynchronously(self):
78-
with patch.object(lambda_function, "lambda_client") as client:
58+
def test_calls_the_classifier(self):
59+
with patch.object(lambda_function, "urlopen") as urlopen:
7960
self.assertTrue(classify_log("pytorch/pytorch", 123))
8061

81-
kwargs = client.invoke.call_args.kwargs
82-
self.assertEqual(kwargs["FunctionName"], "log_classifier")
83-
# Event, not RequestResponse: waiting on classification is exactly the
84-
# mistake that gave github-status-test its multi-hundred-second tails.
85-
self.assertEqual(kwargs["InvocationType"], "Event")
86-
self.assertEqual(
87-
json.loads(kwargs["Payload"])["queryStringParameters"],
88-
{"job_id": "123", "repo": "pytorch/pytorch"},
62+
urlopen.assert_called_once_with(
63+
f"{lambda_function.LOG_CLASSIFIER_URL}/?job_id=123&repo=pytorch/pytorch"
8964
)
9065

91-
def test_a_failed_invoke_is_reported_not_raised(self):
92-
# Raising would make Lambda retry the whole function, re-downloading a
93-
# multi-megabyte log to retry a handoff that takes milliseconds.
94-
with patch.object(lambda_function, "lambda_client") as client:
95-
client.invoke.side_effect = RuntimeError("throttled")
66+
def test_a_failed_call_is_reported_not_raised(self):
67+
# Raising would make Lambda retry the whole function and re-download a
68+
# multi-megabyte log, when the log is already safe in S3.
69+
with patch.object(lambda_function, "urlopen") as urlopen:
70+
urlopen.side_effect = RuntimeError("connection reset")
9671
self.assertFalse(classify_log("pytorch/pytorch", 123))
9772

9873

0 commit comments

Comments
 (0)