Skip to content
This repository was archived by the owner on Nov 1, 2023. It is now read-only.

Commit aa28550

Browse files
chkeitatevoinea
andauthored
update the ado logic to consume the list of existing items once (#3014)
* update the ado logic to consume the list of existing items once * format * Update src/ApiService/ApiService/onefuzzlib/notifications/Ado.cs Co-authored-by: Teo Voinea <58236992+tevoinea@users.noreply.github.com> * Adding a notification testing endpoint * fix tests * format * regen docs * update logic * format * fix dummy name * mypy fix * make mypy happy * bandit fix * renaming * address PR Comment --------- Co-authored-by: Teo Voinea <58236992+tevoinea@users.noreply.github.com>
1 parent 6f06b8f commit aa28550

File tree

17 files changed

+240
-99
lines changed

17 files changed

+240
-99
lines changed

docs/webhook_events.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,10 @@ If webhook is set to have Event Grid message format then the payload will look a
238238
"title": "Onefuzz Version",
239239
"type": "string"
240240
},
241+
"report_url": {
242+
"title": "Report Url",
243+
"type": "string"
244+
},
241245
"scariness_description": {
242246
"title": "Scariness Description",
243247
"type": "string"
@@ -2158,6 +2162,10 @@ If webhook is set to have Event Grid message format then the payload will look a
21582162
"title": "Onefuzz Version",
21592163
"type": "string"
21602164
},
2165+
"report_url": {
2166+
"title": "Report Url",
2167+
"type": "string"
2168+
},
21612169
"scariness_description": {
21622170
"title": "Scariness Description",
21632171
"type": "string"
@@ -6578,6 +6586,10 @@ If webhook is set to have Event Grid message format then the payload will look a
65786586
"title": "Onefuzz Version",
65796587
"type": "string"
65806588
},
6589+
"report_url": {
6590+
"title": "Report Url",
6591+
"type": "string"
6592+
},
65816593
"scariness_description": {
65826594
"title": "Scariness Description",
65836595
"type": "string"

src/ApiService/ApiService/Functions/Notifications.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ private async Async.Task<HttpResponseData> Get(HttpRequestData req) {
2222
return await _context.RequestHandling.NotOk(req, request.ErrorV, "notification search");
2323
}
2424

25-
var entries = request.OkV switch { { Container: null, NotificationId: null } => _context.NotificationOperations.SearchAll(), { Container: var c, NotificationId: null } => _context.NotificationOperations.SearchByRowKeys(c.Select(x => x.String)), { Container: var _, NotificationId: var n } => new[] { await _context.NotificationOperations.GetNotification(n.Value) }.ToAsyncEnumerable(),
25+
var entries = request.OkV switch { { Container: null, NotificationId: null } => _context.NotificationOperations.SearchAll(), { Container: var c, NotificationId: null } => _context.NotificationOperations.SearchByRowKeys(c.Select(x => x.String)), { Container: var _, NotificationId: var n } => new[] { await _context.NotificationOperations.GetNotification(n.Value) }.ToAsyncEnumerable()
2626
};
2727

2828
var response = req.CreateResponse(HttpStatusCode.OK);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.Net;
2+
using Microsoft.Azure.Functions.Worker;
3+
using Microsoft.Azure.Functions.Worker.Http;
4+
5+
namespace Microsoft.OneFuzz.Service.Functions;
6+
7+
public class NotificationsTest {
8+
private readonly ILogTracer _log;
9+
private readonly IEndpointAuthorization _auth;
10+
private readonly IOnefuzzContext _context;
11+
12+
public NotificationsTest(ILogTracer log, IEndpointAuthorization auth, IOnefuzzContext context) {
13+
_log = log;
14+
_auth = auth;
15+
_context = context;
16+
}
17+
18+
private async Async.Task<HttpResponseData> Post(HttpRequestData req) {
19+
_log.WithTag("HttpRequest", "GET").Info($"Notification test");
20+
var request = await RequestHandling.ParseRequest<NotificationTest>(req);
21+
if (!request.IsOk) {
22+
return await _context.RequestHandling.NotOk(req, request.ErrorV, "notification search");
23+
}
24+
25+
var notificationTest = request.OkV;
26+
var result = await _context.NotificationOperations.TriggerNotification(notificationTest.Notification.Container, notificationTest.Notification,
27+
notificationTest.Report, isLastRetryAttempt: true);
28+
var response = req.CreateResponse(HttpStatusCode.OK);
29+
await response.WriteAsJsonAsync(new NotificationTestResponse(result.IsOk, result.ErrorV?.ToString()));
30+
return response;
31+
}
32+
33+
34+
[Function("NotificationsTest")]
35+
public Async.Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "POST", Route = "notifications/test")] HttpRequestData req) {
36+
return _auth.CallIfUser(req, r => r.Method switch {
37+
"POST" => Post(r),
38+
_ => throw new InvalidOperationException("Unsupported HTTP method"),
39+
});
40+
}
41+
}

src/ApiService/ApiService/OneFuzzTypes/Model.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,10 @@ public RegressionReport Truncate(int maxLength) {
527527
}
528528
}
529529

530+
public record UnknownReportType(
531+
Uri? ReportUrl
532+
) : IReport;
533+
530534
[JsonConverter(typeof(NotificationTemplateConverter))]
531535
#pragma warning disable CA1715
532536
public interface NotificationTemplate {

src/ApiService/ApiService/OneFuzzTypes/Requests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ public record NotificationSearch(
129129
Guid? NotificationId
130130
) : BaseRequest;
131131

132+
133+
public record NotificationTest(
134+
[property: Required] Report Report,
135+
[property: Required] Notification Notification
136+
) : BaseRequest;
137+
132138
public record NotificationGet(
133139
[property: Required] Guid NotificationId
134140
) : BaseRequest;

src/ApiService/ApiService/OneFuzzTypes/Responses.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,8 @@ List<Guid> FailedNotificationIds
205205
public record JinjaToScribanMigrationDryRunResponse(
206206
List<Guid> NotificationIdsToUpdate
207207
) : BaseResponse();
208+
209+
public record NotificationTestResponse(
210+
bool Success,
211+
string? Error = null
212+
) : BaseResponse();

src/ApiService/ApiService/onefuzzlib/NotificationOperations.cs

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ public interface INotificationOperations : IOrm<Notification> {
1010
IAsyncEnumerable<(Task, IEnumerable<Container>)> GetQueueTasks();
1111
Async.Task<OneFuzzResult<Notification>> Create(Container container, NotificationTemplate config, bool replaceExisting);
1212
Async.Task<Notification?> GetNotification(Guid notifificationId);
13+
14+
System.Threading.Tasks.Task<OneFuzzResultVoid> TriggerNotification(Container container,
15+
Notification notification, IReport? reportOrRegression, bool isLastRetryAttempt = false);
1316
}
1417

1518
public class NotificationOperations : Orm<Notification>, INotificationOperations {
@@ -30,22 +33,7 @@ public async Async.Task NewFiles(Container container, string filename, bool isLa
3033
}
3134

3235
done.Add(notification.Config);
33-
34-
if (notification.Config is TeamsTemplate teamsTemplate) {
35-
await _context.Teams.NotifyTeams(teamsTemplate, container, filename, reportOrRegression!, notification.NotificationId);
36-
}
37-
38-
if (reportOrRegression == null) {
39-
continue;
40-
}
41-
42-
if (notification.Config is AdoTemplate adoTemplate) {
43-
await _context.Ado.NotifyAdo(adoTemplate, container, filename, reportOrRegression, isLastRetryAttempt, notification.NotificationId);
44-
}
45-
46-
if (notification.Config is GithubIssuesTemplate githubIssuesTemplate) {
47-
await _context.GithubIssues.GithubIssue(githubIssuesTemplate, container, filename, reportOrRegression, notification.NotificationId);
48-
}
36+
_ = await TriggerNotification(container, notification, reportOrRegression, isLastRetryAttempt);
4937
}
5038
}
5139

@@ -74,6 +62,25 @@ public async Async.Task NewFiles(Container container, string filename, bool isLa
7462
}
7563
}
7664

65+
public async System.Threading.Tasks.Task<OneFuzzResultVoid> TriggerNotification(Container container,
66+
Notification notification, IReport? reportOrRegression, bool isLastRetryAttempt = false) {
67+
switch (notification.Config) {
68+
case TeamsTemplate teamsTemplate:
69+
await _context.Teams.NotifyTeams(teamsTemplate, container, reportOrRegression!,
70+
notification.NotificationId);
71+
break;
72+
case AdoTemplate adoTemplate when reportOrRegression is not null:
73+
return await _context.Ado.NotifyAdo(adoTemplate, container, reportOrRegression, isLastRetryAttempt,
74+
notification.NotificationId);
75+
case GithubIssuesTemplate githubIssuesTemplate when reportOrRegression is not null:
76+
await _context.GithubIssues.GithubIssue(githubIssuesTemplate, container, reportOrRegression,
77+
notification.NotificationId);
78+
break;
79+
}
80+
81+
return OneFuzzResultVoid.Ok;
82+
}
83+
7784
public IAsyncEnumerable<Notification> GetNotifications(Container container) {
7885
return SearchByRowKeys(new[] { container.String });
7986
}

src/ApiService/ApiService/onefuzzlib/Reports.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,17 @@ public Reports(ILogTracer log, IContainers containers) {
3535
return null;
3636
}
3737

38-
var blob = await _containers.GetBlob(container, fileName, StorageType.Corpus);
38+
var containerClient = await _containers.FindContainer(container, StorageType.Corpus);
39+
if (containerClient == null) {
40+
if (expectReports) {
41+
_log.Error($"get_report invalid container: {filePath:Tag:FilePath}");
42+
}
43+
return null;
44+
}
45+
46+
Uri reportUrl = containerClient.GetBlobClient(fileName).Uri;
47+
48+
var blob = (await containerClient.GetBlobClient(fileName).DownloadContentAsync()).Value.Content;
3949

4050
if (blob == null) {
4151
if (expectReports) {
@@ -44,11 +54,9 @@ public Reports(ILogTracer log, IContainers containers) {
4454
return null;
4555
}
4656

47-
var reportUrl = await _containers.GetFileUrl(container, fileName, StorageType.Corpus);
48-
4957
var reportOrRegression = ParseReportOrRegression(blob.ToString(), reportUrl);
5058

51-
if (reportOrRegression == null && expectReports) {
59+
if (reportOrRegression is UnknownReportType && expectReports) {
5260
_log.Error($"unable to parse report ({filePath:Tag:FilePath}) as a report or regression");
5361
}
5462

@@ -64,7 +72,7 @@ public Reports(ILogTracer log, IContainers containers) {
6472
}
6573
}
6674

67-
public static IReport? ParseReportOrRegression(string content, Uri? reportUrl) {
75+
public static IReport ParseReportOrRegression(string content, Uri reportUrl) {
6876
var regressionReport = TryDeserialize<RegressionReport>(content);
6977
if (regressionReport is { CrashTestResult: { } }) {
7078
return regressionReport with { ReportUrl = reportUrl };
@@ -73,12 +81,17 @@ public Reports(ILogTracer log, IContainers containers) {
7381
if (report is { CrashType: { } }) {
7482
return report with { ReportUrl = reportUrl };
7583
}
76-
return null;
84+
return new UnknownReportType(reportUrl);
7785
}
7886
}
7987

8088
public interface IReport {
8189
Uri? ReportUrl {
8290
init;
91+
get;
92+
}
93+
public string FileName() {
94+
var segments = (this.ReportUrl ?? throw new ArgumentException()).Segments.Skip(2);
95+
return string.Concat(segments);
8396
}
8497
};

src/ApiService/ApiService/onefuzzlib/Secrets.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,11 @@ public virtual async Task<SecretData<T>> SaveToKeyvault<T>(SecretData<T> secretD
5656
}
5757

5858
public async Task<string?> GetSecretStringValue<T>(SecretData<T> data) {
59-
60-
if (data.Secret is SecretAddress<T> secretAddress) {
61-
var secret = await GetSecret(secretAddress.Url);
62-
return secret.Value;
63-
} else {
64-
return data.Secret.ToString();
65-
}
59+
return (data.Secret) switch {
60+
SecretAddress<T> secretAddress => (await GetSecret(secretAddress.Url)).Value,
61+
SecretValue<T> sValue => sValue.Value?.ToString(),
62+
_ => data.Secret.ToString(),
63+
};
6664
}
6765

6866
public Uri GetKeyvaultAddress() {

src/ApiService/ApiService/onefuzzlib/notifications/Ado.cs

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@
88
namespace Microsoft.OneFuzz.Service;
99

1010
public interface IAdo {
11-
public Async.Task NotifyAdo(AdoTemplate config, Container container, string filename, IReport reportable, bool isLastRetryAttempt, Guid notificationId);
11+
public Async.Task<OneFuzzResultVoid> NotifyAdo(AdoTemplate config, Container container, IReport reportable, bool isLastRetryAttempt, Guid notificationId);
1212
}
1313

1414
public class Ado : NotificationsBase, IAdo {
1515
public Ado(ILogTracer logTracer, IOnefuzzContext context) : base(logTracer, context) {
1616
}
1717

18-
public async Async.Task NotifyAdo(AdoTemplate config, Container container, string filename, IReport reportable, bool isLastRetryAttempt, Guid notificationId) {
18+
public async Async.Task<OneFuzzResultVoid> NotifyAdo(AdoTemplate config, Container container, IReport reportable, bool isLastRetryAttempt, Guid notificationId) {
19+
var filename = reportable.FileName();
1920
if (reportable is RegressionReport) {
2021
_logTracer.Info($"ado integration does not support regression report. container:{container:Tag:Container} filename:{filename:Tag:Filename}");
21-
return;
22+
return OneFuzzResultVoid.Ok;
2223
}
2324

2425
var report = (Report)reportable;
@@ -44,8 +45,11 @@ public async Async.Task NotifyAdo(AdoTemplate config, Container container, strin
4445
} else {
4546
_logTracer.WithTags(notificationInfo).Exception(e, $"Failed to process ado notification");
4647
await LogFailedNotification(report, e, notificationId);
48+
return OneFuzzResultVoid.Error(ErrorCode.NOTIFICATION_FAILURE,
49+
$"Failed to process ado notification : exception: {e}");
4750
}
4851
}
52+
return OneFuzzResultVoid.Ok;
4953
}
5054

5155
private static bool IsTransient(Exception e) {
@@ -205,7 +209,7 @@ public async IAsyncEnumerable<WorkItem> ExistingWorkItems((string, string)[] not
205209
}
206210
}
207211

208-
var query = "select [System.Id] from WorkItems";
212+
var query = "select [System.Id] from WorkItems order by [System.Id]";
209213
if (parts != null && parts.Any()) {
210214
query += " where " + string.Join(" AND ", parts);
211215
}
@@ -331,47 +335,42 @@ private async Async.Task<WorkItem> CreateNew() {
331335
}
332336

333337
public async Async.Task Process((string, string)[] notificationInfo) {
334-
var matchingWorkItems = await ExistingWorkItems(notificationInfo).ToListAsync();
335-
336-
var nonDuplicateWorkItems = matchingWorkItems
337-
.Where(wi => !IsADODuplicateWorkItem(wi))
338-
.ToList();
339-
340-
if (nonDuplicateWorkItems.Count > 1) {
341-
var nonDuplicateWorkItemIds = nonDuplicateWorkItems.Select(wi => wi.Id);
342-
var matchingWorkItemIds = matchingWorkItems.Select(wi => wi.Id);
343-
344-
var extraTags = new List<(string, string)> {
345-
("NonDuplicateWorkItemIds", JsonSerializer.Serialize(nonDuplicateWorkItemIds)),
346-
("MatchingWorkItemIds", JsonSerializer.Serialize(matchingWorkItemIds))
347-
};
348-
extraTags.AddRange(notificationInfo);
349-
350-
_logTracer.WithTags(extraTags).Info($"Found more than 1 matching, non-duplicate work item");
351-
foreach (var workItem in nonDuplicateWorkItems) {
352-
_ = await UpdateExisting(workItem, notificationInfo);
338+
var updated = false;
339+
WorkItem? oldestWorkItem = null;
340+
await foreach (var workItem in ExistingWorkItems(notificationInfo)) {
341+
// work items are ordered by id, so the oldest one is the first one
342+
oldestWorkItem ??= workItem;
343+
_logTracer.WithTags(new List<(string, string)> { ("MatchingWorkItemIds", $"{workItem.Id}") }).Info($"Found matching work item");
344+
if (IsADODuplicateWorkItem(workItem)) {
345+
continue;
353346
}
354-
} else if (nonDuplicateWorkItems.Count == 1) {
355-
_ = await UpdateExisting(nonDuplicateWorkItems.Single(), notificationInfo);
356-
} else if (matchingWorkItems.Any()) {
357-
// We have matching work items but all are duplicates
358-
_logTracer.WithTags(notificationInfo).Info($"All matching work items were duplicates, re-opening the oldest one");
359-
var oldestWorkItem = matchingWorkItems.OrderBy(wi => wi.Id).First();
360-
var stateChanged = await UpdateExisting(oldestWorkItem, notificationInfo);
361-
if (stateChanged) {
362-
// add a comment if we re-opened the bug
363-
_ = await _client.AddCommentAsync(
364-
new CommentCreate() {
365-
Text = "This work item was re-opened because OneFuzz could only find related work items that are marked as duplicate."
366-
},
367-
_project,
368-
(int)oldestWorkItem.Id!);
347+
_logTracer.WithTags(new List<(string, string)> { ("NonDuplicateWorkItemId", $"{workItem.Id}") }).Info($"Found matching non-duplicate work item");
348+
_ = await UpdateExisting(workItem, notificationInfo);
349+
updated = true;
350+
}
351+
352+
if (!updated) {
353+
if (oldestWorkItem != null) {
354+
// We have matching work items but all are duplicates
355+
_logTracer.WithTags(notificationInfo)
356+
.Info($"All matching work items were duplicates, re-opening the oldest one");
357+
var stateChanged = await UpdateExisting(oldestWorkItem, notificationInfo);
358+
if (stateChanged) {
359+
// add a comment if we re-opened the bug
360+
_ = await _client.AddCommentAsync(
361+
new CommentCreate() {
362+
Text =
363+
"This work item was re-opened because OneFuzz could only find related work items that are marked as duplicate."
364+
},
365+
_project,
366+
(int)oldestWorkItem.Id!);
367+
}
368+
} else {
369+
// We never saw a work item like this before, it must be new
370+
var entry = await CreateNew();
371+
var adoEventType = "AdoNewItem";
372+
_logTracer.WithTags(notificationInfo).Event($"{adoEventType} {entry.Id:Tag:WorkItemId}");
369373
}
370-
} else {
371-
// We never saw a work item like this before, it must be new
372-
var entry = await CreateNew();
373-
var adoEventType = "AdoNewItem";
374-
_logTracer.WithTags(notificationInfo).Event($"{adoEventType} {entry.Id:Tag:WorkItemId}");
375374
}
376375
}
377376

0 commit comments

Comments
 (0)