Skip to content

Commit 5ffc662

Browse files
dkurepapremun
andauthored
Group subscription updates (#4422)
<!-- Link the GitHub or AzDO issue this pull request is associated with. Please copy and paste the full URL rather than using the dotnet/arcade-services# syntax --> #4336 This PR makes the service group dependency updates by keeping track of the commit that was in the latest subscription trigger. For example, let's say we trigger a subscription (we'll call it trigger 1) that has an already existing PR, but that PR can't be updated. We set a reminder to try again later. Now let's say we trigger the subscription again (trigger 2), again, but the PR is still not updatable. This PR makes it so that the update caused by the trigger 1 doesn't get processed, because a newer update from trigger 2 should be processed next --------- Co-authored-by: Přemek Vysoký <[email protected]>
1 parent 7795021 commit 5ffc662

13 files changed

+186
-37
lines changed

src/ProductConstructionService/ProductConstructionService.DependencyFlow/IPullRequestUpdater.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@ Task<bool> CheckPullRequestAsync(
1212
PullRequestCheck pullRequestCheck);
1313

1414
Task<bool> ProcessPendingUpdatesAsync(
15-
SubscriptionUpdateWorkItem update);
15+
SubscriptionUpdateWorkItem update,
16+
bool forceApply);
1617

1718
Task<bool> UpdateAssetsAsync(
1819
Guid subscriptionId,
1920
SubscriptionType type,
2021
int buildId,
2122
string sourceRepo,
2223
string sourceSha,
23-
List<Asset> assets);
24+
List<Asset> assets,
25+
bool forceApply);
2426

2527
PullRequestUpdaterId Id { get; }
2628
}

src/ProductConstructionService/ProductConstructionService.DependencyFlow/InProgressPullRequest.cs

+3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ public class InProgressPullRequest : DependencyFlowWorkItem, IPullRequest
5353

5454
[DataMember]
5555
public InProgressPullRequestState MergeState { get; set; }
56+
57+
[DataMember]
58+
public Dictionary<Guid, int> NextBuildsToProcess { get; set; } = [];
5659
}
5760

5861
public enum InProgressPullRequestState

src/ProductConstructionService/ProductConstructionService.DependencyFlow/PullRequestUpdater.cs

+42-16
Original file line numberDiff line numberDiff line change
@@ -115,32 +115,37 @@ public async Task<bool> UpdateAssetsAsync(
115115
int buildId,
116116
string sourceRepo,
117117
string sourceSha,
118-
List<Asset> assets)
118+
List<Asset> assets,
119+
bool forceApply)
119120
{
120-
return await ProcessPendingUpdatesAsync(new()
121-
{
122-
UpdaterId = Id.ToString(),
123-
SubscriptionId = subscriptionId,
124-
SubscriptionType = type,
125-
BuildId = buildId,
126-
SourceSha = sourceSha,
127-
SourceRepo = sourceRepo,
128-
Assets = assets,
129-
IsCoherencyUpdate = false,
130-
});
121+
return await ProcessPendingUpdatesAsync(
122+
new()
123+
{
124+
UpdaterId = Id.ToString(),
125+
SubscriptionId = subscriptionId,
126+
SubscriptionType = type,
127+
BuildId = buildId,
128+
SourceSha = sourceSha,
129+
SourceRepo = sourceRepo,
130+
Assets = assets,
131+
IsCoherencyUpdate = false,
132+
},
133+
forceApply);
131134
}
132135

133136
/// <summary>
134137
/// Process any pending pull request updates.
135138
/// </summary>
139+
/// <param name="forceApply">If false, we will check if this build is the latest one we have queued. If it's not we will skip this update.</param>
136140
/// <returns>
137141
/// True if updates have been applied; <see langword="false" /> otherwise.
138142
/// </returns>
139-
public async Task<bool> ProcessPendingUpdatesAsync(SubscriptionUpdateWorkItem update)
143+
public async Task<bool> ProcessPendingUpdatesAsync(SubscriptionUpdateWorkItem update, bool forceApply)
140144
{
141145
_logger.LogInformation("Processing pending updates for subscription {subscriptionId}", update.SubscriptionId);
142146
// Check if we track an on-going PR already
143147
InProgressPullRequest? pr = await _pullRequestState.TryGetStateAsync();
148+
144149
bool isCodeFlow = update.SubscriptionType == SubscriptionType.DependenciesAndSources;
145150

146151
if (pr == null)
@@ -149,6 +154,18 @@ public async Task<bool> ProcessPendingUpdatesAsync(SubscriptionUpdateWorkItem up
149154
}
150155
else
151156
{
157+
if (!forceApply &&
158+
pr.NextBuildsToProcess != null &&
159+
pr.NextBuildsToProcess.TryGetValue(update.SubscriptionId, out int buildId) &&
160+
buildId != update.BuildId)
161+
{
162+
_logger.LogInformation("Skipping update for subscription {subscriptionId} with build {oldBuild} because an update with a newer build {newBuild} has already been queued.",
163+
update.SubscriptionId,
164+
update.BuildId,
165+
pr.NextBuildsToProcess);
166+
return true;
167+
}
168+
152169
var prStatus = await GetPullRequestStatusAsync(pr, isCodeFlow);
153170
switch (prStatus)
154171
{
@@ -161,9 +178,7 @@ public async Task<bool> ProcessPendingUpdatesAsync(SubscriptionUpdateWorkItem up
161178
// If we can update it, we will do it below
162179
break;
163180
case PullRequestStatus.InProgressCannotUpdate:
164-
_logger.LogInformation("PR {url} for subscription {subscriptionId} cannot be updated at this time. Deferring update..", pr.Url, update.SubscriptionId);
165-
await _pullRequestUpdateReminders.SetReminderAsync(update, DefaultReminderDelay, isCodeFlow);
166-
await _pullRequestCheckReminders.UnsetReminderAsync(isCodeFlow);
181+
await ScheduleUpdateForLater(pr, update, isCodeFlow);
167182
return false;
168183
default:
169184
throw new NotImplementedException($"Unknown PR status {prStatus}");
@@ -636,6 +651,7 @@ await AddDependencyFlowEventsAsync(
636651

637652
await darcRemote.UpdatePullRequestAsync(pr.Url, pullRequest);
638653
pr.LastUpdate = DateTime.UtcNow;
654+
pr.NextBuildsToProcess.Remove(update.SubscriptionId);
639655
await SetPullRequestCheckReminder(pr, isCodeFlow: update.SubscriptionType == SubscriptionType.DependenciesAndSources);
640656

641657
_logger.LogInformation("Pull request '{prUrl}' updated", pr.Url);
@@ -866,6 +882,15 @@ private async Task ClearAllStateAsync(bool isCodeFlow, bool clearPendingUpdates)
866882
return alteredUpdates;
867883
}
868884

885+
private async Task ScheduleUpdateForLater(InProgressPullRequest pr, SubscriptionUpdateWorkItem update, bool isCodeFlow)
886+
{
887+
_logger.LogInformation("PR {url} for subscription {subscriptionId} cannot be updated at this time. Deferring update..", pr.Url, update.SubscriptionId);
888+
await _pullRequestUpdateReminders.SetReminderAsync(update, DefaultReminderDelay, isCodeFlow);
889+
await _pullRequestCheckReminders.UnsetReminderAsync(isCodeFlow);
890+
pr.NextBuildsToProcess[update.SubscriptionId] = update.BuildId;
891+
await _pullRequestState.SetAsync(pr);
892+
}
893+
869894
#region Code flow subscriptions
870895

871896
/// <summary>
@@ -1242,6 +1267,7 @@ private async Task<bool> HandlePrUpdateConflictAsync(
12421267

12431268
pr.MergeState = InProgressPullRequestState.Conflict;
12441269
pr.SourceSha = remoteCommit;
1270+
pr.NextBuildsToProcess[update.SubscriptionId] = update.BuildId;
12451271
await _pullRequestState.SetAsync(pr);
12461272
await _pullRequestUpdateReminders.SetReminderAsync(update, DefaultReminderDelay, isCodeFlow: true);
12471273
await _pullRequestCheckReminders.UnsetReminderAsync(isCodeFlow: true);

src/ProductConstructionService/ProductConstructionService.DependencyFlow/SubscriptionTriggerer.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ await pullRequestUpdater.UpdateAssetsAsync(
172172
build.Id,
173173
build.GetRepository(),
174174
build.Commit,
175-
assets);
175+
assets,
176+
forceApply: true);
176177

177178
_logger.LogInformation("Asset update complete for {subscriptionId}", _subscriptionId);
178179
}

src/ProductConstructionService/ProductConstructionService.DependencyFlow/WorkItemProcessors/SubscriptionUpdateProcessor.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public override async Task<bool> ProcessWorkItemAsync(
1515
CancellationToken cancellationToken)
1616
{
1717
var updater = _updaterFactory.CreatePullRequestUpdater(PullRequestUpdaterId.Parse(workItem.UpdaterId));
18-
await updater.ProcessPendingUpdatesAsync(workItem);
18+
await updater.ProcessPendingUpdatesAsync(workItem, forceApply: false);
1919
return true;
2020
}
2121

test/ProductConstructionService.DependencyFlow.Tests/PendingCodeFlowUpdatesTests.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public async Task PendingCodeFlowUpdatesNotUpdatablePr()
2626
using (WithExistingCodeFlowPullRequest(build, canUpdate: false))
2727
{
2828
await WhenProcessPendingUpdatesAsyncIsCalled(build, isCodeFlow: true);
29-
AndShouldHaveInProgressPullRequestState(build);
29+
AndShouldHaveInProgressPullRequestState(build, build.Id);
3030
}
3131
}
3232

@@ -100,6 +100,7 @@ public async Task PendingUpdatesInConflictWithCurrent()
100100
AndShouldNotHavePullRequestCheckReminder();
101101
AndShouldHaveInProgressPullRequestState(
102102
oldBuild,
103+
nextBuildToProcess: newBuild.Id,
103104
overwriteBuildCommit: ConflictPRRemoteSha,
104105
prState: InProgressPullRequestState.Conflict);
105106
}
@@ -125,6 +126,7 @@ public async Task PendingUpdateNotUpdatablePrAlreadyInConflict()
125126
AndShouldNotHavePullRequestCheckReminder();
126127
AndShouldHaveInProgressPullRequestState(
127128
build,
129+
nextBuildToProcess: build.Id,
128130
overwriteBuildCommit: ConflictPRRemoteSha,
129131
prState: InProgressPullRequestState.Conflict);
130132
}

test/ProductConstructionService.DependencyFlow.Tests/PendingUpdatePullRequestUpdaterTests.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ namespace ProductConstructionService.DependencyFlow.Tests;
88

99
internal abstract class PendingUpdatePullRequestUpdaterTests : PullRequestUpdaterTests
1010
{
11-
protected async Task WhenProcessPendingUpdatesAsyncIsCalled(Build forBuild, bool isCodeFlow = false)
11+
protected async Task WhenProcessPendingUpdatesAsyncIsCalled(
12+
Build forBuild,
13+
bool isCodeFlow = false,
14+
bool forceApply = true)
1215
{
1316
await Execute(
1417
async context =>
1518
{
1619
IPullRequestUpdater updater = CreatePullRequestActor(context);
17-
await updater.ProcessPendingUpdatesAsync(CreateSubscriptionUpdate(forBuild, isCodeFlow));
20+
await updater.ProcessPendingUpdatesAsync(CreateSubscriptionUpdate(forBuild, isCodeFlow), forceApply);
1821
});
1922
}
2023

test/ProductConstructionService.DependencyFlow.Tests/PendingUpdatesTests.cs

+76
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,80 @@ public async Task PendingUpdatesUpdatablePr()
5757
AndShouldHavePullRequestCheckReminder();
5858
}
5959
}
60+
61+
[Test]
62+
public async Task PendingUpdatesNotUpdatableGroupingTest()
63+
{
64+
GivenATestChannel();
65+
GivenASubscription(
66+
new SubscriptionPolicy
67+
{
68+
Batchable = true,
69+
UpdateFrequency = UpdateFrequency.EveryBuild
70+
});
71+
Build b1 = GivenANewBuild(true);
72+
Build b2 = GivenANewBuild(true);
73+
b2.Id = 2;
74+
75+
using (WithExistingPullRequest(b1, canUpdate: false))
76+
{
77+
await WhenProcessPendingUpdatesAsyncIsCalled(b2);
78+
79+
ThenShouldHaveInProgressPullRequestState(b1, b2.Id);
80+
ThenShouldHavePendingUpdateState(b2, isCodeFlow: false);
81+
AndShouldNotHavePullRequestCheckReminder();
82+
}
83+
}
84+
85+
[Test]
86+
public async Task PendingUpdatesShouldNotBeProcessedUnlessNewerBuildQueued()
87+
{
88+
GivenATestChannel();
89+
GivenASubscription(
90+
new SubscriptionPolicy
91+
{
92+
Batchable = true,
93+
UpdateFrequency = UpdateFrequency.EveryBuild
94+
});
95+
Build b1 = GivenANewBuild(true);
96+
Build b2 = GivenANewBuild(true);
97+
b2.Id = 2;
98+
Build b3 = GivenANewBuild(true);
99+
b3.Id = 3;
100+
101+
using (WithExistingPullRequest(b1, canUpdate: true, nextBuildToProcess: b3.Id, setupRemoteMock: false))
102+
{
103+
await WhenProcessPendingUpdatesAsyncIsCalled(b2, forceApply: false);
104+
105+
ThenShouldHaveInProgressPullRequestState(b1, b3.Id);
106+
AndShouldHaveNoPendingUpdateState();
107+
AndShouldNotHavePullRequestCheckReminder();
108+
}
109+
}
110+
111+
[Test]
112+
public async Task PendingUpdatesShouldBeProcessedWhenNewestBuildPending()
113+
{
114+
GivenATestChannel();
115+
GivenASubscription(
116+
new SubscriptionPolicy
117+
{
118+
Batchable = true,
119+
UpdateFrequency = UpdateFrequency.EveryBuild
120+
});
121+
Build b1 = GivenANewBuild(true);
122+
Build b2 = GivenANewBuild(true);
123+
b2.Id = 2;
124+
125+
WithRequireNonCoherencyUpdates();
126+
WithNoRequiredCoherencyUpdates();
127+
using (WithExistingPullRequest(b1, canUpdate: true, nextBuildToProcess: b2.Id, setupRemoteMock: true))
128+
{
129+
await WhenProcessPendingUpdatesAsyncIsCalled(b2, forceApply: false);
130+
131+
ThenShouldHaveInProgressPullRequestState(b2);
132+
AndShouldHaveNoPendingUpdateState();
133+
AndShouldHavePullRequestCheckReminder();
134+
}
135+
}
60136
}

0 commit comments

Comments
 (0)