Skip to content

Commit

Permalink
Merge pull request #5 from Accurx/spike/cjk/2023-03-27-updates
Browse files Browse the repository at this point in the history
Update .NET task
  • Loading branch information
CharlieAccuRx authored Apr 18, 2023
2 parents 44139b6 + a9cdf85 commit 950a1c1
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 67 deletions.
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,24 @@ Please answer the following questions in `questions.md`.
- How long did you spend on the exercise?
- What would you add if you had more time and how?

When you're finished, please download your solution (in GitHub, at the route of your repository, click Code -> Download Zip) and using the link in your invite email, submit your zipped solution.
When you're finished, please download your solution including your completed `questions.md` file (in GitHub, at the route of your repository, click Code -> Download Zip) and using the link in your invite email, submit your zipped solution.

Thanks for your time, we hope you enjoy the exercise and please do get in touch if you have any questions!

## Task

To release software to many hundreds of thousands of clinicians multiple times a week, our component for downloading updates needs to be reliable in the challenging networking conditions clinicians can face: intermittent internet disconnection and slow internet speeds. We would like you to update the skeleton project in this repo to provide a way for clinicians to download our software in multiple situations.

In this task, performing a normal GET request on a file won't be reliable for two reasons. Firstly, we need to be able to recover from internet disconnections. Secondly, we need to not have to start from scratch every time, with intermittent internet disconnection and slow internet, it's unlikely we'll be able to download the whole file in one go. Luckily, some CDNs support downloading partial content so if we can get part of the way through, we can resume from this point. If the URL does not support partial content then we attempt to just download the whole file.
In this task, performing a normal GET request on a file won't be reliable for two reasons. Firstly, we need to be able to recover from internet disconnections. Secondly, we need to not have to start from scratch every time; with intermittent internet disconnection and slow internet, it's unlikely we'll be able to download the whole file in one go. Luckily, some CDNs support downloading partial content so if we can get part of the way through, we can resume from this point. If the URL does not support partial content then we attempt to download the whole file.

Your solution should meet the following core requirements:
- Download the installer even when internet disconnections occur (we use 2 minutes as a disconnection time benchmark for this)
- Download the installer, even when internet disconnections occur (we use 2 minutes as a disconnection time benchmark for this)
- Implement partial downloading so that we don’t need to start from scratch every time, if the CDN supports this
- Implement downloading the file in one go, if the CDN does not support partial downloading
- Recover from failures and not exit until the file has been successfully downloaded
- Check the integrity of the file after downloading and delete the file if this check fails. You can use the Content-MD5 for this: https://www.oreilly.com/library/view/http-the-definitive/1565925092/re17.html
- Report progress to the user throughout the download
- Add the ability to cancel so the user can stop any in progress downloads
- Add the ability to cancel the download


We are looking for you to demonstrate:
Expand All @@ -50,7 +50,7 @@ If you feel that modifying the skeleton project would create a better solution t

### .NET

There is already a ```IWebSystemCalls.cs``` and corresponding implementation which allows you to get the HTTP headers for a URL, download the whole content, or download the partial content. All these calls return an ```HttpResponseMessage``` object which contains properties for headers and the content.
There is already a ```IWebSystemCalls.cs``` and corresponding implementation which allows you to get the HTTP headers for a URL, download the whole content, or download partial content. All these calls return an ```HttpResponseMessage``` object which contains properties for headers and the content.

As in the example here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges, the HTTP header "Accept Ranges" will be set to "Bytes" if the CDN supports partial content.

Expand All @@ -66,11 +66,12 @@ A test project is included (with JUnit added though can be swapped for an altern

## Tips

- Take time to read through the question and description. There's guidance in there that can be helpful to approaching the problem.
- The code doesn't need to be beautiful but it needs to be readable.
- Take the time to read through the task and description. There's guidance in there that can be helpful to approaching the problem.
- The code doesn't need to be perfect but it needs to be readable.
- Try writing down some example input and outputs on paper.
- Try a brute force approach and then optimise the code.
- Add some comments to your code if you think it will be helpful to share your thought process to someone assessing it.
- You can throttle your internet connection using NetLimiter or similar.
- You can simulate internet disconnections through disconnecting wifi/ethernet.
- Different behaviours occur after following different periods of disconnection, two seconds and two minutes are sweet spots for exercising key failure modes.
- Usage of NuGet packages/third party libraries is fine, however do bear in mind that overusing these limits your ability to show off!
15 changes: 6 additions & 9 deletions dot-net/ReliableDownloader/FileDownloader.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ReliableDownloader;

public class FileDownloader : IFileDownloader
{
public Task<bool> DownloadFile(string contentFileUrl, string localFilePath, Action<FileProgress> onProgressChanged)
{
throw new NotImplementedException();
}

public void CancelDownloads()
{
throw new NotImplementedException();
}
public Task<bool> TryDownloadFile(
string contentFileUrl,
string localFilePath,
Action<FileProgress> onProgressChanged,
CancellationToken cancellationToken) => throw new NotImplementedException();
}
20 changes: 5 additions & 15 deletions dot-net/ReliableDownloader/FileProgress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,8 @@

namespace ReliableDownloader;

public class FileProgress
{
public FileProgress(long? totalFileSize, long totalBytesDownloaded, double? progressPercent, TimeSpan? estimatedRemaining)
{
TotalFileSize = totalFileSize;
TotalBytesDownloaded = totalBytesDownloaded;
ProgressPercent = progressPercent;
EstimatedRemaining = estimatedRemaining;
}

public long? TotalFileSize { get; }
public long TotalBytesDownloaded { get; }
public double? ProgressPercent { get; }
public TimeSpan? EstimatedRemaining { get; }
}
public record FileProgress(
long? TotalFileSize,
long TotalBytesDownloaded,
double? ProgressPercent,
TimeSpan? EstimatedRemaining);
23 changes: 12 additions & 11 deletions dot-net/ReliableDownloader/IFileDownloader.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ReliableDownloader;

public interface IFileDownloader
{
/// <summary>Downloads a file, trying to use reliable downloading if possible</summary>
/// <param name="contentFileUrl">The url which the file is hosted at</param>
/// <param name="localFilePath">The local file path to save the file to</param>
/// <param name="onProgressChanged">An action to call which prints progress</param>
/// <returns>True or false, depending on if download completes and writes to file system okay</returns>
Task<bool> DownloadFile(string contentFileUrl, string localFilePath, Action<FileProgress> onProgressChanged);
/// <summary>
/// Cancels any in progress downloads
/// </summary>
void CancelDownloads();
/// <summary>Attempts to download a file and write it to the file system.</summary>
/// <param name="contentFileUrl">The URL of the file to download.</param>
/// <param name="localFilePath">The file path to persist the downloaded file to.</param>
/// <param name="onProgressChanged">An action that is invoked with the latest download progress.</param>
/// <param name="cancellationToken">A cancellation token to cancel the download.</param>
/// <returns>True if the download completes and writes to the file system successfully, otherwise false.</returns>
Task<bool> TryDownloadFile(
string contentFileUrl,
string localFilePath,
Action<FileProgress> onProgressChanged,
CancellationToken cancellationToken);
}
25 changes: 17 additions & 8 deletions dot-net/ReliableDownloader/IWebSystemCalls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,26 @@ namespace ReliableDownloader;

public interface IWebSystemCalls
{
/// <summary>Does an HTTP HEAD REST call to just get the headers for a URL</summary>
/// <summary>
/// Makes an HTTP HEAD request to get the response headers for a URL.
/// </summary>
/// <param name="url">The URL to make the request to.</param>
/// <param name="token">A cancellation token to allow request cancellation.</param>
Task<HttpResponseMessage> GetHeadersAsync(string url, CancellationToken token);

/// <summary>
/// Does a simple HTTP GET to download content from a URL
/// Makes an HTTP GET request to download content from a URL.
/// </summary>
Task<HttpResponseMessage> DownloadContent(string url, CancellationToken token);
/// <param name="url">The URL to download content from.</param>
/// <param name="token">A cancellation token to allow request cancellation.</param>
Task<HttpResponseMessage> DownloadContentAsync(string url, CancellationToken token);

/// <summary>
/// Does a HTTP GET but with a range specified to download partial content (if supported)
/// Makes an HTTP GET request with a byte range specified to allow downloading partial content, if supported.
/// </summary>
/// <param name="from">From value, in bytes</param>
/// <param name="to">From value, in bytes</param>
/// <returns></returns>
Task<HttpResponseMessage> DownloadPartialContent(string url, long from, long to, CancellationToken token);
/// <param name="url">The URL to download content from.</param>
/// <param name="from">The position (in bytes) of the file to start sending data.</param>
/// <param name="to">The position (in bytes) of the file to stop sending data.</param>
/// <param name="token">A cancellation token to allow request cancellation.</param>
Task<HttpResponseMessage> DownloadPartialContentAsync(string url, long from, long to, CancellationToken token);
}
14 changes: 10 additions & 4 deletions dot-net/ReliableDownloader/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ReliableDownloader;

namespace ReliableDownloader;

Expand All @@ -9,9 +10,14 @@ internal class Program
public static async Task Main(string[] args)
{
// If this url 404's, you can get a live one from https://installer.demo.accurx.com/chain/latest.json.
var exampleUrl = "https://installer.demo.accurx.com/chain/3.182.57641.0/accuRx.Installer.Local.msi";
var exampleFilePath = "C:/Users/[USER]/myfirstdownload.msi";
var exampleUrl = "https://installer.demo.accurx.com/chain/4.22.50587.0/accuRx.Installer.Local.msi";
var exampleFilePath = Path.Combine(Directory.GetCurrentDirectory(), "myfirstdownload.msi");
var fileDownloader = new FileDownloader();
await fileDownloader.DownloadFile(exampleUrl, exampleFilePath, progress => { Console.WriteLine($"Percent progress is {progress.ProgressPercent}"); });
var didDownloadSuccessfully = await fileDownloader.TryDownloadFile(
exampleUrl,
exampleFilePath,
progress => Console.WriteLine($"Percent progress is {progress.ProgressPercent}"),
CancellationToken.None);
Console.WriteLine($"File download ended! Success: {didDownloadSuccessfully}");
}
}
23 changes: 10 additions & 13 deletions dot-net/ReliableDownloader/WebSystemCalls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,24 @@ namespace ReliableDownloader;

public class WebSystemCalls : IWebSystemCalls
{
private static readonly HttpClient _client = new HttpClient();
private static readonly HttpClient _client = new();

public async Task<HttpResponseMessage> GetHeadersAsync(string url, CancellationToken token)
{
return await _client.SendAsync(new HttpRequestMessage(HttpMethod.Head, url), token).ConfigureAwait(continueOnCapturedContext: false);
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Head, url);
return await _client.SendAsync(httpRequestMessage, token).ConfigureAwait(continueOnCapturedContext: false);
}

public async Task<HttpResponseMessage> DownloadContent(string url, CancellationToken token)
public async Task<HttpResponseMessage> DownloadContentAsync(string url, CancellationToken token)
{
using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url))
{
return await _client.SendAsync(httpRequestMessage, token).ConfigureAwait(continueOnCapturedContext: false);
}
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
return await _client.SendAsync(httpRequestMessage, token).ConfigureAwait(continueOnCapturedContext: false);
}

public async Task<HttpResponseMessage> DownloadPartialContent(string url, long from, long to, CancellationToken token)
public async Task<HttpResponseMessage> DownloadPartialContentAsync(string url, long from, long to, CancellationToken token)
{
using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url))
{
httpRequestMessage.Headers.Range = new RangeHeaderValue(from, to);
return await _client.SendAsync(httpRequestMessage, token).ConfigureAwait(continueOnCapturedContext: false);
}
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
httpRequestMessage.Headers.Range = new RangeHeaderValue(from, to);
return await _client.SendAsync(httpRequestMessage, token).ConfigureAwait(continueOnCapturedContext: false);
}
}

0 comments on commit 950a1c1

Please sign in to comment.