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

Commit 55d5ca0

Browse files
authored
Merge pull request #1776 from github/fixes/pr-list-errors-2
Make PR list handle errors more gracefully.
2 parents 79c7077 + 94f98f0 commit 55d5ca0

File tree

7 files changed

+202
-17
lines changed

7 files changed

+202
-17
lines changed

src/GitHub.App/Collections/SequentialListSource.cs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,18 @@ async Task<Page<TModel>> EnsureLoaded(int pageNumber)
146146
}
147147
}
148148

149-
await Task.WhenAny(loading, pageLoaded).ConfigureAwait(false);
149+
var completed = await Task.WhenAny(loading, pageLoaded).ConfigureAwait(false);
150+
151+
if (completed.IsFaulted)
152+
{
153+
throw completed.Exception;
154+
}
150155

151156
if (pageLoaded.IsCompleted)
152157
{
153-
return pages[pageNumber];
158+
// A previous waiting task may have already returned the page. If so, return null.
159+
pages.TryGetValue(pageNumber, out var result);
160+
return result;
154161
}
155162
}
156163

@@ -177,13 +184,18 @@ async Task Load()
177184
{
178185
OnBeginLoading();
179186

180-
while (nextPage <= loadTo && !disposed)
187+
try
188+
{
189+
while (nextPage <= loadTo && !disposed)
190+
{
191+
await LoadNextPage().ConfigureAwait(false);
192+
PageLoaded?.Invoke(this, EventArgs.Empty);
193+
}
194+
}
195+
finally
181196
{
182-
await LoadNextPage().ConfigureAwait(false);
183-
PageLoaded?.Invoke(this, EventArgs.Empty);
197+
OnEndLoading();
184198
}
185-
186-
OnEndLoading();
187199
}
188200

189201
async Task LoadNextPage()

src/GitHub.App/Collections/VirtualizingList.cs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
using System.Collections.Generic;
44
using System.Collections.Specialized;
55
using System.ComponentModel;
6+
using System.IO;
67
using System.Linq;
8+
using System.Reflection;
79
using System.Threading.Tasks;
810
using System.Windows;
911
using System.Windows.Threading;
@@ -139,6 +141,7 @@ object IList.this[int index]
139141

140142
public event NotifyCollectionChangedEventHandler CollectionChanged;
141143
public event PropertyChangedEventHandler PropertyChanged;
144+
public event EventHandler<ErrorEventArgs> InitializationError;
142145

143146
public IEnumerator<T> GetEnumerator()
144147
{
@@ -176,14 +179,22 @@ void LoadCount()
176179
{
177180
countTask.ContinueWith(x =>
178181
{
179-
count = x.Result;
180-
SendReset();
181-
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
182+
if (x.IsFaulted)
183+
{
184+
RaiseInitializationError(x.Exception);
185+
}
186+
else if (!x.IsCanceled)
187+
{
188+
count = x.Result;
189+
SendReset();
190+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
191+
}
182192
}, TaskScheduler.FromCurrentSynchronizationContext());
183193
}
184194
}
185195
catch (Exception ex)
186196
{
197+
RaiseInitializationError(ex);
187198
log.Error(ex, "Error loading virtualizing list count");
188199
}
189200
}
@@ -206,6 +217,25 @@ async void LoadPage(int number)
206217
catch (Exception ex)
207218
{
208219
log.Error(ex, "Error loading virtualizing list page {Number}", number);
220+
pages.Remove(number);
221+
}
222+
}
223+
224+
void RaiseInitializationError(Exception e)
225+
{
226+
if (InitializationError != null)
227+
{
228+
if (e is AggregateException ae)
229+
{
230+
e = ae = ae.Flatten();
231+
232+
if (ae.InnerExceptions.Count == 1)
233+
{
234+
e = ae.InnerException;
235+
}
236+
}
237+
238+
InitializationError(this, new ErrorEventArgs(e));
209239
}
210240
}
211241

src/GitHub.App/ViewModels/GitHubPane/IssueListViewModelBase.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.ComponentModel;
4+
using System.IO;
45
using System.Linq;
56
using System.Reactive;
67
using System.Reactive.Disposables;
@@ -197,6 +198,7 @@ public override Task Refresh()
197198
view.Filter = FilterItem;
198199
Items = items;
199200
ItemsView = view;
201+
Error = null;
200202

201203
dispose.Add(itemSource);
202204
dispose.Add(
@@ -208,6 +210,11 @@ public override Task Refresh()
208210
this.WhenAnyValue(x => x.AuthorFilter.Selected),
209211
(loading, count, _, __, ___) => Tuple.Create(loading, count))
210212
.Subscribe(x => UpdateState(x.Item1, x.Item2)));
213+
dispose.Add(
214+
Observable.FromEventPattern<ErrorEventArgs>(
215+
x => items.InitializationError += x,
216+
x => items.InitializationError -= x)
217+
.Subscribe(x => Error = x.EventArgs.GetException()));
211218
subscription = dispose;
212219

213220
return Task.CompletedTask;

src/GitHub.App/ViewModels/GitHubPane/PullRequestListViewModel.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ protected override IIssueListItemViewModelBase CreateViewModel(PullRequestListIt
122122
return result;
123123
}
124124

125-
protected override Task<Page<PullRequestListItemModel>> LoadPage(string after)
125+
protected override async Task<Page<PullRequestListItemModel>> LoadPage(string after)
126126
{
127127
PullRequestStateEnum[] states;
128128

@@ -139,15 +139,12 @@ protected override Task<Page<PullRequestListItemModel>> LoadPage(string after)
139139
break;
140140
}
141141

142-
var sw = Stopwatch.StartNew();
143-
var result = owner.service.ReadPullRequests(
142+
var result = await owner.service.ReadPullRequests(
144143
HostAddress.Create(owner.RemoteRepository.CloneUrl),
145144
owner.RemoteRepository.Owner,
146145
owner.RemoteRepository.Name,
147146
after,
148-
states);
149-
sw.Stop();
150-
System.Diagnostics.Debug.WriteLine("Read PR page in " + sw.Elapsed);
147+
states).ConfigureAwait(false);
151148
return result;
152149
}
153150
}

src/GitHub.App/ViewModels/UserFilterViewModel.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,13 @@ async Task Load()
120120
while (true)
121121
{
122122
var page = await load(after);
123-
users.AddRange(page.Items.Select(x => new ActorViewModel(x)));
123+
124+
foreach (var item in page.Items)
125+
{
126+
var vm = new ActorViewModel(item);
127+
users.Add(vm);
128+
}
129+
124130
after = page.EndCursor;
125131
if (!page.HasNextPage) break;
126132
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reactive;
5+
using System.Reactive.Linq;
6+
using System.Reactive.Subjects;
7+
using System.Reactive.Threading.Tasks;
8+
using System.Threading.Tasks;
9+
using GitHub.Collections;
10+
using GitHub.Models;
11+
using NUnit.Framework;
12+
13+
namespace GitHub.App.UnitTests.Collections
14+
{
15+
public class SequentialListSourceTests
16+
{
17+
[Test]
18+
public async Task GetCount_Should_Load_First_Page()
19+
{
20+
var target = new TestSource();
21+
22+
Assert.That(target.PagesLoaded, Is.Empty);
23+
24+
var count = await target.GetCount();
25+
26+
Assert.That(count, Is.EqualTo(100));
27+
Assert.That(target.PagesLoaded, Is.EqualTo(new[] { 0 }));
28+
}
29+
30+
[Test]
31+
public async Task GetPage_Should_Load_Pages()
32+
{
33+
var target = new TestSource();
34+
35+
Assert.That(target.PagesLoaded, Is.Empty);
36+
37+
var count = await target.GetPage(3);
38+
39+
Assert.That(target.PagesLoaded, Is.EqualTo(new[] { 0, 1, 2, 3 }));
40+
}
41+
42+
[Test]
43+
public void GetPage_Should_Stop_Loading_Pages_When_LoadPage_Throws()
44+
{
45+
var target = new TestSource(2);
46+
47+
Assert.That(target.PagesLoaded, Is.Empty);
48+
49+
Assert.ThrowsAsync<AggregateException>(() => target.GetPage(3));
50+
Assert.That(target.PagesLoaded, Is.EqualTo(new[] { 0, 1 }));
51+
}
52+
53+
[Test]
54+
public void IsLoading_Should_Be_Set_To_False_When_LoadPage_Throws()
55+
{
56+
var target = new TestSource(2);
57+
58+
Assert.That(target.PagesLoaded, Is.Empty);
59+
60+
Assert.ThrowsAsync<AggregateException>(() => target.GetPage(3));
61+
Assert.That(target.IsLoading, Is.False);
62+
}
63+
64+
[Test]
65+
public async Task Should_Not_Load_Duplicate_Pages()
66+
{
67+
var trigger = new Subject<Unit>();
68+
var target = new TestSource(loadTrigger: trigger);
69+
70+
var task1 = target.GetPage(1);
71+
var task2 = target.GetPage(1);
72+
73+
Assert.That(target.PagesLoaded, Is.Empty);
74+
75+
trigger.OnNext(Unit.Default);
76+
trigger.OnNext(Unit.Default);
77+
78+
await task1;
79+
await task2;
80+
81+
Assert.That(target.PagesLoaded, Is.EqualTo(new[] { 0, 1 }));
82+
}
83+
84+
class TestSource : SequentialListSource<string, string>
85+
{
86+
const int PageCount = 10;
87+
readonly int? throwAtPage;
88+
readonly ISubject<Unit> loadTrigger;
89+
90+
public TestSource(
91+
int? throwAtPage = null,
92+
ISubject<Unit> loadTrigger = null)
93+
{
94+
this.throwAtPage = throwAtPage;
95+
this.loadTrigger = loadTrigger;
96+
}
97+
98+
public override int PageSize => 10;
99+
public List<int> PagesLoaded { get; private set; } = new List<int>();
100+
101+
protected override string CreateViewModel(string model)
102+
{
103+
return model + " loaded";
104+
}
105+
106+
protected override async Task<Page<string>> LoadPage(string after)
107+
{
108+
var page = after != null ? int.Parse(after) : 0;
109+
110+
if (loadTrigger != null)
111+
{
112+
await loadTrigger.Take(1).ToTask().ConfigureAwait(false);
113+
}
114+
115+
if (page == throwAtPage)
116+
{
117+
throw new GitHubLogicException("Thrown.");
118+
}
119+
120+
PagesLoaded.Add(page);
121+
122+
return new Page<string>
123+
{
124+
EndCursor = (page + 1).ToString(),
125+
HasNextPage = page < PageCount,
126+
Items = Enumerable.Range(page * PageSize, PageSize).Select(x => "Item " + x).ToList(),
127+
TotalCount = PageSize * PageCount,
128+
};
129+
}
130+
}
131+
}
132+
}

test/GitHub.App.UnitTests/GitHub.App.UnitTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@
157157
<Compile Include="..\Helpers\TestSharedCache.cs" />
158158
<Compile Include="Args.cs" />
159159
<Compile Include="Caches\ImageCacheTests.cs" />
160+
<Compile Include="Collections\SequentialListSourceTests.cs" />
160161
<Compile Include="Factories\ModelServiceFactoryTests.cs" />
161162
<Compile Include="Models\AccountModelTests.cs" />
162163
<Compile Include="Models\ModelServiceTests.cs" />

0 commit comments

Comments
 (0)