Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions src/GitHub.Api/SimpleApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class SimpleApiClient : ISimpleApiClient
public HostAddress HostAddress { get; }
public UriString OriginalUrl { get; }

readonly GitHubClient client;
readonly IGitHubClient client;
readonly Lazy<IEnterpriseProbeTask> enterpriseProbe;
readonly Lazy<IWikiProbe> wikiProbe;
static readonly SemaphoreSlim sem = new SemaphoreSlim(1);
Expand All @@ -23,7 +23,7 @@ public class SimpleApiClient : ISimpleApiClient
bool? isEnterprise;
bool? hasWiki;

internal SimpleApiClient(HostAddress hostAddress, UriString repoUrl, GitHubClient githubClient,
public SimpleApiClient(HostAddress hostAddress, UriString repoUrl, IGitHubClient githubClient,
Lazy<IEnterpriseProbeTask> enterpriseProbe, Lazy<IWikiProbe> wikiProbe)
{
HostAddress = hostAddress;
Expand Down Expand Up @@ -78,16 +78,12 @@ async Task<Repository> GetRepositoryInternal()

public bool HasWiki()
{
if (hasWiki.HasValue)
return hasWiki.Value;
return false;
return hasWiki.HasValue && hasWiki.Value;
}

public bool IsEnterprise()
{
if (isEnterprise.HasValue)
return isEnterprise.Value;
return false;
return isEnterprise.HasValue && isEnterprise.Value;
}

async Task<bool> HasWikiInternal(Repository repo)
Expand All @@ -108,8 +104,6 @@ async Task<bool> HasWikiInternal(Repository repo)
return false;
#endif
var ret = await probe.ProbeAsync(repo);
if (ret == WikiProbeResult.Failed)
return false;
return (ret == WikiProbeResult.Ok);
}

Expand All @@ -122,8 +116,6 @@ async Task<bool> IsEnterpriseInternal()
return false;
#endif
var ret = await probe.ProbeAsync(HostAddress.WebUri);
if (ret == EnterpriseProbeResult.Failed)
return false;
return (ret == EnterpriseProbeResult.Ok);
}
}
Expand Down
30 changes: 9 additions & 21 deletions src/GitHub.Api/SimpleApiClientFactory.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using GitHub.Models;
using GitHub.Primitives;
using GitHub.Services;
using Octokit;
using System.Collections.Generic;
using System.Collections.Concurrent;

namespace GitHub.Api
{
Expand All @@ -16,7 +17,7 @@ public class SimpleApiClientFactory : ISimpleApiClientFactory
readonly Lazy<IEnterpriseProbeTask> lazyEnterpriseProbe;
readonly Lazy<IWikiProbe> lazyWikiProbe;

static readonly Dictionary<UriString, ISimpleApiClient> cache = new Dictionary<UriString, ISimpleApiClient>();
static readonly ConcurrentDictionary<UriString, ISimpleApiClient> cache = new ConcurrentDictionary<UriString, ISimpleApiClient>();

[ImportingConstructor]
public SimpleApiClientFactory(IProgram program, Lazy<IEnterpriseProbeTask> enterpriseProbe, Lazy<IWikiProbe> wikiProbe)
Expand All @@ -28,29 +29,16 @@ public SimpleApiClientFactory(IProgram program, Lazy<IEnterpriseProbeTask> enter

public ISimpleApiClient Create(UriString repositoryUrl)
{
var contains = cache.ContainsKey(repositoryUrl);
if (contains)
return cache[repositoryUrl];

lock (cache)
{
if (!cache.ContainsKey(repositoryUrl))
{
var hostAddress = HostAddress.Create(repositoryUrl);
var apiBaseUri = hostAddress.ApiUri;
cache.Add(repositoryUrl, new SimpleApiClient(hostAddress, repositoryUrl, new GitHubClient(productHeader, new SimpleCredentialStore(hostAddress), apiBaseUri), lazyEnterpriseProbe, lazyWikiProbe));
}
return cache[repositoryUrl];
}
var hostAddress = HostAddress.Create(repositoryUrl);
return cache.GetOrAdd(repositoryUrl, new SimpleApiClient(hostAddress, repositoryUrl,
new GitHubClient(productHeader, new SimpleCredentialStore(hostAddress), hostAddress.ApiUri),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the above block because we were accessing cache outside of the lock here. In theory, if ClearFormCache is called after we check contains (line 32) but before we execute the return (line 33), we could get an exception here because the item could be removed from the cache.

If we're concerned about performance here, we could consider switching to the ConcurrentDictionary, but I'm not sure this is a hot spot. Thoughts @shana?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm almost certain I did this because Create will never get called at the same time as ClearFromCache, and the lock in Create is not to protect Create from ClearFromCache, but to have only the first instance that comes into Create trigger the creation, and keep everyone else from never even reaching the lock.

When the team explorer home page loads, there's a series of requests that get triggered to go through Create, all at the same time - one for each section and navigation item. If there's already an api client, great, return that, but if there isn't, whoever gets to the lock first gets to create it. Since the section gets created first and there's a slight delay between it and the nav items being created, that means that potentially the nav items never even hit the lock and just return immediately.

I need to confirm this, and maybe change the names on these locks to be more clear about what their role is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like ConcurrentDictionary would be useful for this then.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it might be. I'm looking at it now, and ConcurrentDictionary has this interesting remark on its MSDN page (https://msdn.microsoft.com/en-us/library/ee378677(v=vs.110).aspx):

If you call GetOrAdd simultaneously on different threads, addValueFactory may be called multiple times, but its key/value pair might not be added to the dictionary for every call.

That means that it can potentially create multiple instances of SimpleApiClient and then discard them, returning only the first one created (or the first one inserted, whichever comes, erm, first). Given that the SimpleApiClient ctor doesn't do anything, that should be fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm fine with that behavior because creating them is cheap. Especially if it's discarded. Locks are less cheap.

lazyEnterpriseProbe, lazyWikiProbe));
}

public void ClearFromCache(ISimpleApiClient client)
{
lock (cache)
{
if (cache.ContainsKey(client.OriginalUrl))
cache.Remove(client.OriginalUrl);
}
ISimpleApiClient c;
cache.TryRemove(client.OriginalUrl, out c);
}
}
}
51 changes: 51 additions & 0 deletions src/UnitTests/GitHub.Api/SimpleApiClientFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using GitHub.Api;
using GitHub.Primitives;
using GitHub.Services;
using GitHub.VisualStudio;
using NSubstitute;
using Xunit;

public class SimpleApiClientFactoryTests
{
public class TheCreateMethod
{
[Fact]
public void CreatesNewInstanceOfSimpleApiClient()
{
var program = new Program();
var enterpriseProbe = Substitute.For<IEnterpriseProbeTask>();
var wikiProbe = Substitute.For<IWikiProbe>();
var factory = new SimpleApiClientFactory(
program,
new Lazy<IEnterpriseProbeTask>(() => enterpriseProbe),
new Lazy<IWikiProbe>(() => wikiProbe));

var client = factory.Create("https://github.com/github/visualstudio");

Assert.Equal("https://github.com/github/visualstudio", client.OriginalUrl);
Assert.Equal(HostAddress.GitHubDotComHostAddress, client.HostAddress);
Assert.Same(client, factory.Create("https://github.com/github/visualstudio")); // Tests caching.
}
}

public class TheClearFromCacheMethod
{
[Fact]
public void RemovesClientFromCache()
{
var program = new Program();
var enterpriseProbe = Substitute.For<IEnterpriseProbeTask>();
var wikiProbe = Substitute.For<IWikiProbe>();
var factory = new SimpleApiClientFactory(
program,
new Lazy<IEnterpriseProbeTask>(() => enterpriseProbe),
new Lazy<IWikiProbe>(() => wikiProbe));

var client = factory.Create("https://github.com/github/visualstudio");
factory.ClearFromCache(client);

Assert.NotSame(client, factory.Create("https://github.com/github/visualstudio"));
}
}
}
165 changes: 165 additions & 0 deletions src/UnitTests/GitHub.Api/SimpleApiClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
using System;
using System.Threading.Tasks;
using GitHub.Api;
using GitHub.Primitives;
using GitHub.Services;
using NSubstitute;
using Octokit;
using Xunit;

public class SimpleApiClientTests
{
public class TheGetRepositoryMethod
{
[Fact]
public async Task RetrievesRepositoryFromWeb()
{
var gitHubHost = HostAddress.GitHubDotComHostAddress;
var gitHubClient = Substitute.For<IGitHubClient>();
var repository = new Repository(42);
gitHubClient.Repository.Get("github", "visualstudio").Returns(Task.FromResult(repository));
var enterpriseProbe = Substitute.For<IEnterpriseProbeTask>();
var wikiProbe = Substitute.For<IWikiProbe>();
var client = new SimpleApiClient(
gitHubHost,
"https://github.com/github/visualstudio",
gitHubClient,
new Lazy<IEnterpriseProbeTask>(() => enterpriseProbe),
new Lazy<IWikiProbe>(() => wikiProbe));

var result = await client.GetRepository();

Assert.Equal(42, result.Id);
}

[Fact]
public async Task RetrievesCachedRepositoryForSubsequentCalls()
{
var gitHubHost = HostAddress.GitHubDotComHostAddress;
var gitHubClient = Substitute.For<IGitHubClient>();
var repository = new Repository(42);
gitHubClient.Repository.Get("github", "visualstudio")
.Returns(_ => Task.FromResult(repository), _ => { throw new Exception("Should only be called once."); });
var enterpriseProbe = Substitute.For<IEnterpriseProbeTask>();
var wikiProbe = Substitute.For<IWikiProbe>();
var client = new SimpleApiClient(
gitHubHost,
"https://github.com/github/visualstudio",
gitHubClient,
new Lazy<IEnterpriseProbeTask>(() => enterpriseProbe),
new Lazy<IWikiProbe>(() => wikiProbe));
await client.GetRepository();

var result = await client.GetRepository();

Assert.Equal(42, result.Id);
}
}

public class TheHasWikiMethod
{
[Theory]
[InlineData(WikiProbeResult.Ok, true)]
[InlineData(WikiProbeResult.Failed, false)]
[InlineData(WikiProbeResult.NotFound, false)]
public async Task ReturnsTrueWhenWikiProbeReturnsOk(WikiProbeResult probeResult, bool expected)
{
var gitHubHost = HostAddress.GitHubDotComHostAddress;
var gitHubClient = Substitute.For<IGitHubClient>();
var repository = CreateRepository(42, true);
gitHubClient.Repository.Get("github", "visualstudio").Returns(Task.FromResult(repository));
var enterpriseProbe = Substitute.For<IEnterpriseProbeTask>();
var wikiProbe = Substitute.For<IWikiProbe>();
wikiProbe.ProbeAsync(repository)
.Returns(_ => Task.FromResult(probeResult), _ => { throw new Exception("Only call it once"); });
var client = new SimpleApiClient(
gitHubHost,
"https://github.com/github/visualstudio",
gitHubClient,
new Lazy<IEnterpriseProbeTask>(() => enterpriseProbe),
new Lazy<IWikiProbe>(() => wikiProbe));
await client.GetRepository();

var result = client.HasWiki();

Assert.Equal(expected, result);
Assert.Equal(expected, client.HasWiki());
}

[Fact]
public void ReturnsFalseWhenWeHaveNotRequestedRepository()
{
var gitHubHost = HostAddress.GitHubDotComHostAddress;
var gitHubClient = Substitute.For<IGitHubClient>();
var enterpriseProbe = Substitute.For<IEnterpriseProbeTask>();
var wikiProbe = Substitute.For<IWikiProbe>();
var client = new SimpleApiClient(
gitHubHost,
"https://github.com/github/visualstudio",
gitHubClient,
new Lazy<IEnterpriseProbeTask>(() => enterpriseProbe),
new Lazy<IWikiProbe>(() => wikiProbe));

var result = client.HasWiki();

Assert.False(result);
}
}

public class TheIsEnterpriseMethod
{
[Theory]
[InlineData(EnterpriseProbeResult.Ok, true)]
[InlineData(EnterpriseProbeResult.Failed, false)]
[InlineData(EnterpriseProbeResult.NotFound, false)]
public async Task ReturnsTrueWhenEnterpriseProbeReturnsOk(EnterpriseProbeResult probeResult, bool expected)
{
var gitHubHost = HostAddress.GitHubDotComHostAddress;
var gitHubClient = Substitute.For<IGitHubClient>();
var repository = CreateRepository(42, true);
gitHubClient.Repository.Get("github", "visualstudio").Returns(Task.FromResult(repository));
var enterpriseProbe = Substitute.For<IEnterpriseProbeTask>();
enterpriseProbe.ProbeAsync(Args.Uri)
.Returns(_ => Task.FromResult(probeResult), _ => { throw new Exception("Only call it once"); });
var wikiProbe = Substitute.For<IWikiProbe>();
var client = new SimpleApiClient(
gitHubHost,
"https://github.com/github/visualstudio",
gitHubClient,
new Lazy<IEnterpriseProbeTask>(() => enterpriseProbe),
new Lazy<IWikiProbe>(() => wikiProbe));
await client.GetRepository();

var result = client.IsEnterprise();

Assert.Equal(expected, result);
Assert.Equal(expected, client.IsEnterprise());
}

[Fact]
public void ReturnsFalseWhenWeHaveNotRequestedRepository()
{
var gitHubHost = HostAddress.GitHubDotComHostAddress;
var gitHubClient = Substitute.For<IGitHubClient>();
var enterpriseProbe = Substitute.For<IEnterpriseProbeTask>();
var wikiProbe = Substitute.For<IWikiProbe>();
var client = new SimpleApiClient(
gitHubHost,
"https://github.com/github/visualstudio",
gitHubClient,
new Lazy<IEnterpriseProbeTask>(() => enterpriseProbe),
new Lazy<IWikiProbe>(() => wikiProbe));

var result = client.IsEnterprise();

Assert.False(result);
}
}

private static Repository CreateRepository(int id, bool hasWiki)
{
return new Repository("", "", "", "", "", "", "", id, new User(), "", "", "", "", "", false, false, 0, 0, 0, "",
0, null, DateTimeOffset.Now, DateTimeOffset.Now, new RepositoryPermissions(), new User(), null, null, false,
hasWiki, false);
}
}
2 changes: 2 additions & 0 deletions src/UnitTests/UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Args.cs" />
<Compile Include="GitHub.Api\SimpleApiClientFactoryTests.cs" />
<Compile Include="GitHub.Api\SimpleApiClientTests.cs" />
<Compile Include="GitHub.App\Caches\CredentialCacheTests.cs" />
<Compile Include="GitHub.App\Caches\ImageCacheTests.cs" />
<Compile Include="GitHub.App\Models\ModelServiceTests.cs" />
Expand Down