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

Commit b2456d8

Browse files
committed
Merge pull request #118 from github/feature/pr/streaming-cache
Pull request list apis with cache
2 parents a697f91 + 071da8f commit b2456d8

File tree

18 files changed

+835
-48
lines changed

18 files changed

+835
-48
lines changed

src/GitHub.App/Api/ApiClient.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using System.Reactive.Linq;
88
using System.Security.Cryptography;
99
using System.Text;
10-
using GitHub.Authentication;
1110
using GitHub.Primitives;
1211
using NLog;
1312
using NullGuard;
@@ -122,8 +121,6 @@ public IObservable<LicenseMetadata> GetLicenses()
122121

123122
public HostAddress HostAddress { get; }
124123

125-
public ITwoFactorChallengeHandler TwoFactorChallengeHandler { get; private set; }
126-
127124
static string GetSha256Hash(string input)
128125
{
129126
try
@@ -198,5 +195,15 @@ public IObservable<Unit> DeleteApplicationAuthorization(int id, [AllowNull]strin
198195
{
199196
return gitHubClient.Authorization.Delete(id, twoFactorAuthorizationCode);
200197
}
198+
199+
public IObservable<PullRequest> GetPullRequestsForRepository(string owner, string name)
200+
{
201+
return gitHubClient.PullRequest.GetAllForRepository(owner, name,
202+
new PullRequestRequest {
203+
State = ItemState.All,
204+
SortProperty = PullRequestSort.Updated,
205+
SortDirection = SortDirection.Descending
206+
});
207+
}
201208
}
202209
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Globalization;
4+
using System.Linq;
5+
using System.Reactive.Linq;
6+
using Akavache;
7+
using NullGuard;
8+
9+
namespace GitHub.Caches
10+
{
11+
public class CacheIndex
12+
{
13+
public static CacheIndex Create(string key)
14+
{
15+
return new CacheIndex { IndexKey = key };
16+
}
17+
18+
public CacheIndex()
19+
{
20+
Keys = new List<string>();
21+
OldKeys = new List<string>();
22+
}
23+
24+
public IObservable<CacheIndex> AddAndSave(IBlobCache cache, string indexKey, CacheItem item,
25+
DateTimeOffset? absoluteExpiration = null)
26+
{
27+
var k = string.Format(CultureInfo.InvariantCulture, "{0}|{1}", IndexKey, item.Key);
28+
if (!Keys.Contains(k))
29+
Keys.Add(k);
30+
UpdatedAt = DateTimeOffset.UtcNow;
31+
return cache.InsertObject(IndexKey, this, absoluteExpiration)
32+
.Select(x => this);
33+
}
34+
35+
public static IObservable<CacheIndex> AddAndSaveToIndex(IBlobCache cache, string indexKey, CacheItem item,
36+
DateTimeOffset? absoluteExpiration = null)
37+
{
38+
return cache.GetOrCreateObject(indexKey, () => Create(indexKey))
39+
.Do(index =>
40+
{
41+
var k = string.Format(CultureInfo.InvariantCulture, "{0}|{1}", index.IndexKey, item.Key);
42+
if (!index.Keys.Contains(k))
43+
index.Keys.Add(k);
44+
index.UpdatedAt = DateTimeOffset.UtcNow;
45+
})
46+
.SelectMany(index => cache.InsertObject(index.IndexKey, index, absoluteExpiration)
47+
.Select(x => index));
48+
}
49+
50+
public IObservable<CacheIndex> Clear(IBlobCache cache, string indexKey, DateTimeOffset? absoluteExpiration = null)
51+
{
52+
OldKeys = Keys.ToList();
53+
Keys.Clear();
54+
UpdatedAt = DateTimeOffset.UtcNow;
55+
return cache
56+
.InvalidateObject<CacheIndex>(indexKey)
57+
.SelectMany(_ => cache.InsertObject(indexKey, this, absoluteExpiration))
58+
.Select(_ => this);
59+
}
60+
61+
[AllowNull]
62+
public string IndexKey {[return: AllowNull] get; set; }
63+
public List<string> Keys { get; set; }
64+
public DateTimeOffset UpdatedAt { get; set; }
65+
public List<string> OldKeys { get; set; }
66+
}
67+
}

src/GitHub.App/Caches/CacheItem.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.Globalization;
3+
using System.Reactive.Linq;
4+
using Akavache;
5+
using NullGuard;
6+
7+
namespace GitHub.Caches
8+
{
9+
public class CacheItem
10+
{
11+
[AllowNull]
12+
public string Key {[return: AllowNull] get; set; }
13+
public DateTimeOffset Timestamp { get; set; }
14+
15+
public IObservable<T> Save<T>(IBlobCache cache, string key, DateTimeOffset? absoluteExpiration = null)
16+
where T : CacheItem
17+
{
18+
var k = string.Format(CultureInfo.InvariantCulture, "{0}|{1}", key, Key);
19+
return cache
20+
.InvalidateObject<T>(k)
21+
.Select(_ => cache.InsertObject(k, this, absoluteExpiration))
22+
.Select(_ => this as T);
23+
}
24+
}
25+
}

src/GitHub.App/Extensions/AkavacheExtensions.cs

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using System;
2+
using System.Linq;
3+
using System.Reactive;
24
using System.Reactive.Linq;
35
using Akavache;
6+
using GitHub.Caches;
47

58
namespace GitHub.Extensions
69
{
@@ -14,6 +17,7 @@ public static class AkavacheExtensions
1417
/// the stale value will be produced first, followed by the fresh value
1518
/// when the fetch func completes.
1619
/// </summary>
20+
/// <param name="blobCache">The cache to retrieve the object from.</param>
1721
/// <param name="key">The key to look up the cache value with.</param>
1822
/// <param name="fetchFunc">The fetch function.</param>
1923
/// <param name="refreshInterval">
@@ -59,6 +63,7 @@ public static IObservable<T> GetAndRefreshObject<T>(
5963
/// the stale value will be produced first, followed by the fresh value
6064
/// when the fetch func completes.
6165
/// </summary>
66+
/// <param name="blobCache">The cache to retrieve the object from.</param>
6267
/// <param name="key">The key to look up the cache value with.</param>
6368
/// <param name="fetchFunc">The fetch function.</param>
6469
/// <param name="refreshInterval">
@@ -133,10 +138,105 @@ static IObservable<byte[]> GetAndFetchLatestBytes(
133138
.RefCount();
134139
}
135140

141+
/// <summary>
142+
/// This method attempts to return a series of cached value(s) aggregated by
143+
/// a <paramref name="key"/>. In the case of a
144+
/// cache miss the fetchFunc will be used to provide a fresh value which
145+
/// is then returned. In the case of a cache hit where the cache item is
146+
/// considered stale (but not expired) as determined by <paramref name="refreshInterval"/>
147+
/// the stale values will be produced first, followed by the fresh values
148+
/// when the fetch func completes.
149+
/// </summary>
150+
/// <param name="blobCache">The cache to retrieve the object from.</param>
151+
/// <param name="key">The key to look up the cache value with.</param>
152+
/// <param name="fetchFunc">The fetch function.</param>
153+
/// <param name="removedItemsCallback">The callback that receives items that
154+
/// were a part of the cache but not of the list list of values.</param>
155+
/// <param name="refreshInterval">
156+
/// Cache objects with an age exceeding this value will be treated as stale
157+
/// and the fetch function will be invoked to refresh it.
158+
/// </param>
159+
/// <param name="maxCacheDuration">
160+
/// The maximum age of a cache object before the object is treated as
161+
/// expired and unusable. Cache objects older than this will be treated
162+
/// as a cache miss.
163+
/// </param>
164+
public static IObservable<T> GetAndFetchLatestFromIndex<T>(
165+
this IBlobCache blobCache,
166+
string key,
167+
Func<IObservable<T>> fetchFunc,
168+
Action<T> removedItemsCallback,
169+
TimeSpan refreshInterval,
170+
TimeSpan maxCacheDuration)
171+
where T : CacheItem
172+
{
173+
return Observable.Defer(() =>
174+
{
175+
var absoluteExpiration = blobCache.Scheduler.Now + maxCacheDuration;
176+
177+
try
178+
{
179+
return blobCache.GetAndFetchLatestFromIndex(
180+
key,
181+
fetchFunc,
182+
removedItemsCallback,
183+
createdAt => IsExpired(blobCache, createdAt, refreshInterval),
184+
absoluteExpiration);
185+
}
186+
catch (Exception exc)
187+
{
188+
return Observable.Throw<T>(exc);
189+
}
190+
});
191+
}
192+
193+
static IObservable<T> GetAndFetchLatestFromIndex<T>(this IBlobCache This,
194+
string key,
195+
Func<IObservable<T>> fetchFunc,
196+
Action<T> removedItemsCallback,
197+
Func<DateTimeOffset, bool> fetchPredicate = null,
198+
DateTimeOffset? absoluteExpiration = null,
199+
bool shouldInvalidateOnError = false)
200+
where T : CacheItem
201+
{
202+
var fetch = Observable.Defer(() => This.GetOrCreateObject(key, () => CacheIndex.Create(key))
203+
.Select(x => Tuple.Create(x, fetchPredicate == null || !x.Keys.Any() || fetchPredicate(x.UpdatedAt)))
204+
.Where(predicateIsTrue => predicateIsTrue.Item2)
205+
.Select(x => x.Item1)
206+
.SelectMany(index => index.Clear(This, key, absoluteExpiration))
207+
.SelectMany(index =>
208+
{
209+
var fetchObs = fetchFunc().Catch<T, Exception>(ex =>
210+
{
211+
var shouldInvalidate = shouldInvalidateOnError ?
212+
This.InvalidateObject<CacheIndex>(key) :
213+
Observable.Return(Unit.Default);
214+
return shouldInvalidate.SelectMany(__ => Observable.Throw<T>(ex));
215+
});
216+
217+
return fetchObs
218+
.SelectMany(x => x.Save<T>(This, key, absoluteExpiration))
219+
.Do(x => index.AddAndSave(This, key, x, absoluteExpiration))
220+
.Finally(() =>
221+
{
222+
This.GetObjects<T>(index.OldKeys.Except(index.Keys))
223+
.Do(dict => This.InvalidateObjects<T>(dict.Keys))
224+
.SelectMany(dict => dict.Values)
225+
.Do(removedItemsCallback)
226+
.Subscribe();
227+
});
228+
}));
229+
230+
var cache = Observable.Defer(() => This.GetOrCreateObject(key, () => CacheIndex.Create(key))
231+
.SelectMany(index => This.GetObjects<T>(index.Keys))
232+
.SelectMany(dict => dict.Values));
233+
234+
return cache.Merge(fetch).Replay().RefCount();
235+
}
236+
136237
static bool IsExpired(IBlobCache blobCache, DateTimeOffset itemCreatedAt, TimeSpan cacheDuration)
137238
{
138-
var now = blobCache.Scheduler.Now;
139-
var elapsed = now - itemCreatedAt;
239+
var elapsed = blobCache.Scheduler.Now - itemCreatedAt.ToUniversalTime();
140240

141241
return elapsed > cacheDuration;
142242
}

src/GitHub.App/GitHub.App.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,15 @@
123123
<None Include="..\..\script\Key.snk" Condition="$(Buildtype) == 'Internal'">
124124
<Link>Key.snk</Link>
125125
</None>
126+
<Compile Include="Caches\CacheIndex.cs" />
127+
<Compile Include="Caches\CacheItem.cs" />
126128
<Compile Include="Caches\ImageCache.cs" />
127129
<Compile Include="Extensions\AkavacheExtensions.cs" />
128130
<Compile Include="Extensions\EnvironmentExtensions.cs" />
129131
<Compile Include="Extensions\ValidationExtensions.cs" />
130132
<Compile Include="GlobalSuppressions.cs" />
131133
<Compile Include="Infrastructure\LoggingConfiguration.cs" />
134+
<Compile Include="Models\PullRequestModel.cs" />
132135
<Compile Include="Resources.Designer.cs">
133136
<AutoGen>True</AutoGen>
134137
<DesignTime>True</DesignTime>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System;
2+
using System.Globalization;
3+
using GitHub.Primitives;
4+
using GitHub.VisualStudio.Helpers;
5+
using NullGuard;
6+
7+
namespace GitHub.Models
8+
{
9+
public sealed class PullRequestModel : NotificationAwareObject, IPullRequestModel
10+
{
11+
public PullRequestModel(int number, string title, IAccount author, DateTimeOffset createdAt, DateTimeOffset? updatedAt = null)
12+
{
13+
Number = number;
14+
Title = title;
15+
Author = author;
16+
CreatedAt = createdAt;
17+
UpdatedAt = updatedAt ?? CreatedAt;
18+
}
19+
20+
public void CopyFrom(IPullRequestModel other)
21+
{
22+
if (!Equals(other))
23+
throw new ArgumentException("Instance to copy from doesn't match this instance. this:(" + this + ") other:(" + other + ")", nameof(other));
24+
Title = other.Title;
25+
UpdatedAt = other.UpdatedAt;
26+
CommentCount = other.CommentCount;
27+
HasNewComments = other.HasNewComments;
28+
}
29+
30+
public override bool Equals([AllowNull]object obj)
31+
{
32+
if (ReferenceEquals(this, obj))
33+
return true;
34+
var other = obj as PullRequestModel;
35+
return other != null && Number == other.Number;
36+
}
37+
38+
public override int GetHashCode()
39+
{
40+
return Number;
41+
}
42+
43+
bool IEquatable<IPullRequestModel>.Equals([AllowNull]IPullRequestModel other)
44+
{
45+
if (ReferenceEquals(this, other))
46+
return true;
47+
return other != null && Number == other.Number;
48+
}
49+
50+
public int CompareTo([AllowNull]IPullRequestModel other)
51+
{
52+
return other != null ? UpdatedAt.CompareTo(other.UpdatedAt) : 1;
53+
}
54+
55+
public static bool operator >([AllowNull]PullRequestModel lhs, [AllowNull]PullRequestModel rhs)
56+
{
57+
if (ReferenceEquals(lhs, rhs))
58+
return false;
59+
return lhs?.CompareTo(rhs) > 0;
60+
}
61+
62+
public static bool operator <([AllowNull]PullRequestModel lhs, [AllowNull]PullRequestModel rhs)
63+
{
64+
if (ReferenceEquals(lhs, rhs))
65+
return false;
66+
return (object)lhs == null || lhs.CompareTo(rhs) < 0;
67+
}
68+
69+
public static bool operator ==([AllowNull]PullRequestModel lhs, [AllowNull]PullRequestModel rhs)
70+
{
71+
return Equals(lhs, rhs) && ((object)lhs == null || lhs.CompareTo(rhs) == 0);
72+
}
73+
74+
public static bool operator !=([AllowNull]PullRequestModel lhs, [AllowNull]PullRequestModel rhs)
75+
{
76+
return !(lhs == rhs);
77+
}
78+
79+
public int Number { get; }
80+
81+
string title;
82+
public string Title
83+
{
84+
get { return title; }
85+
set { title = value; this.RaisePropertyChange(); }
86+
}
87+
88+
int commentCount;
89+
public int CommentCount
90+
{
91+
get { return commentCount; }
92+
set { commentCount = value; this.RaisePropertyChange(); }
93+
}
94+
95+
bool hasNewComments;
96+
public bool HasNewComments
97+
{
98+
get { return hasNewComments; }
99+
set { hasNewComments = value; this.RaisePropertyChange(); }
100+
}
101+
102+
public DateTimeOffset CreatedAt { get; set; }
103+
public DateTimeOffset UpdatedAt { get; set; }
104+
public IAccount Author { get; set; }
105+
106+
107+
[return: AllowNull] // nullguard thinks a string.Format can return null. sigh.
108+
public override string ToString()
109+
{
110+
return string.Format(CultureInfo.InvariantCulture, "id:{0} title:{1} created:{2:u} updated:{3:u}", Number, Title, CreatedAt, UpdatedAt);
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)