diff --git a/src/Unit-3/lesson5/Completed/ActorPaths.cs b/src/Unit-3/lesson5/Completed/ActorPaths.cs
deleted file mode 100644
index ae491dd9b..000000000
--- a/src/Unit-3/lesson5/Completed/ActorPaths.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using Akka.Actor;
-
-namespace GithubActors
-{
- ///
- /// Static helper class used to define paths to fixed-name actors
- /// (helps eliminate errors when using )
- ///
- public static class ActorPaths
- {
- public static readonly ActorMetaData GithubAuthenticatorActor = new ActorMetaData("authenticator", "akka://GithubActors/user/authenticator");
- public static readonly ActorMetaData MainFormActor = new ActorMetaData("mainform", "akka://GithubActors/user/mainform");
- public static readonly ActorMetaData GithubValidatorActor = new ActorMetaData("validator", "akka://GithubActors/user/validator");
- public static readonly ActorMetaData GithubCommanderActor = new ActorMetaData("commander", "akka://GithubActors/user/commander");
- public static readonly ActorMetaData GithubCoordinatorActor = new ActorMetaData("coordinator", "akka://GithubActors/user/commander/coordinator");
- }
-
- ///
- /// Meta-data class
- ///
- public class ActorMetaData
- {
- public ActorMetaData(string name, string path)
- {
- Name = name;
- Path = path;
- }
-
- public string Name { get; private set; }
-
- public string Path { get; private set; }
- }
-}
diff --git a/src/Unit-3/lesson5/Completed/ActorSystem.fs b/src/Unit-3/lesson5/Completed/ActorSystem.fs
new file mode 100644
index 000000000..f1242afe1
--- /dev/null
+++ b/src/Unit-3/lesson5/Completed/ActorSystem.fs
@@ -0,0 +1,6 @@
+namespace GithubActors
+
+open Akka.FSharp
+
+module ActorSystem =
+ let githubActors = System.create "GithubActors" (Configuration.load())
\ No newline at end of file
diff --git a/src/Unit-3/lesson5/Completed/Actors.fs b/src/Unit-3/lesson5/Completed/Actors.fs
new file mode 100644
index 000000000..458a5c306
--- /dev/null
+++ b/src/Unit-3/lesson5/Completed/Actors.fs
@@ -0,0 +1,462 @@
+namespace GithubActors
+
+open System
+open System.Windows.Forms
+open System.Drawing
+open System.Linq
+open Akka.FSharp
+open Akka.Actor
+open Akka.Routing
+
+[]
+module Actors =
+
+ // make a pipe-friendly version of Akka.NET PipeTo for handling async computations
+ let pipeToWithSender recipient sender asyncComp = pipeTo asyncComp recipient sender
+
+ // Helper functions to check the type of query received
+ let isWorkerMessage (someType: obj) = someType.GetType().IsSubclassOf(typeof)
+
+ let isQueryStarrers (someType: obj) =
+ if isWorkerMessage someType then
+ match someType :?> GithubActorMessage with
+ | QueryStarrers _ -> true
+ | _ -> false
+ else
+ false
+
+ let isQueryStarrer (someType: obj) =
+ if isWorkerMessage someType then
+ match someType :?> GithubActorMessage with
+ | QueryStarrer _ -> true
+ | _ -> false
+ else
+ false
+
+ // Actors
+ let githubAuthenticationActor (statusLabel: Label) (githubAuthForm: Form) (launcherForm: Form) (mailbox: Actor<_>) =
+
+ let cannotAuthenticate reason =
+ statusLabel.ForeColor <- Color.Red
+ statusLabel.Text <- reason
+
+ let showAuthenticatingStatus () =
+ statusLabel.Visible <- true
+ statusLabel.ForeColor <- Color.Orange
+ statusLabel.Text <- "Authenticating..."
+
+ let rec unauthenticated () =
+ actor {
+ let! message = mailbox.Receive ()
+
+ match message with
+ | Authenticate token ->
+ showAuthenticatingStatus ()
+ let client = GithubClientFactory.getUnauthenticatedClient ()
+ client.Credentials <- Octokit.Credentials token
+
+ let continuation (task: System.Threading.Tasks.Task) : AuthenticationMessage =
+ match task.IsFaulted with
+ | true -> AuthenticationFailed
+ | false ->
+ match task.IsCanceled with
+ | true -> AuthenticationCancelled
+ | false ->
+ GithubClientFactory.setOauthToken token
+ AuthenticationSuccess
+
+ client.User.Current().ContinueWith continuation
+ |> Async.AwaitTask
+ |!> mailbox.Self
+
+ return! authenticating ()
+ | _ -> return! unauthenticated ()
+ }
+ and authenticating () =
+ actor {
+ let! message = mailbox.Receive ()
+
+ match message with
+ | AuthenticationFailed ->
+ cannotAuthenticate "Authentication failed."
+ return! unauthenticated ()
+ | AuthenticationCancelled ->
+ cannotAuthenticate "Authentication timed out."
+ return! unauthenticated ()
+ | AuthenticationSuccess ->
+ githubAuthForm.Hide ()
+ launcherForm.Show ()
+ | _ -> return! authenticating ()
+ }
+
+ unauthenticated ()
+
+
+ let mainFormActor (isValidLabel: Label) (createRepoResultsForm) (mailbox: Actor<_>) =
+
+ let updateLabel message isValid =
+ isValidLabel.Text <- message
+ if isValid then isValidLabel.ForeColor <- Color.Green else isValidLabel.ForeColor <- Color.Red
+ mailbox.UnstashAll ()
+
+ let rec ready () =
+ actor {
+ let! message = mailbox.Receive ()
+
+ match message with
+ | ProcessRepo uri ->
+ select "akka://GithubActors/user/validator" mailbox.Context.System
+ let repoResultsForm: Form = createRepoResultsForm repoKey coordinator
+ repoResultsForm.Show ()
+ return! ready ()
+ | _ -> return! ready ()
+ }
+ and busy () =
+ actor {
+ let! message = mailbox.Receive ()
+
+ match message with
+ | ValidRepo _ ->
+ updateLabel "Valid!" true
+ return! ready ()
+ | InvalidRepo (uri, reason) ->
+ updateLabel reason false
+ return! ready ()
+ | UnableToAcceptJob job ->
+ updateLabel (sprintf "%s/%s is a valid repo, but the system cannot accept additional jobs" job.Owner job.Repo) false
+ return! ready ()
+ | AbleToAcceptJob job ->
+ updateLabel (sprintf "%s/%s is a valid repo - starting job!" job.Owner job.Repo) true
+ return! ready ()
+ | LaunchRepoResultsWindow (_, _) ->
+ mailbox.Stash ()
+ return! busy ()
+ | _ -> return! busy ()
+ }
+
+ ready ()
+
+
+ let githubValidatorActor (getGithubClient: unit -> Octokit.GitHubClient) (mailbox: Actor<_>) =
+
+ let splitIntoOwnerAndRepo repoUri =
+ let results = Uri(repoUri, UriKind.Absolute).PathAndQuery.TrimEnd('/').Split('/') |> Array.rev
+ (results.[1], results.[0]) // User, Repo
+
+ let rec processMessage () = actor {
+ let! message = mailbox.Receive ()
+
+ match message with
+ // outright invalid URLs
+ | ValidateRepo uri when uri |> String.IsNullOrEmpty || not (Uri.IsWellFormedUriString(uri, UriKind.Absolute)) ->
+ mailbox.Context.Sender
+ let continuation (task: System.Threading.Tasks.Task) : GithubActorMessage =
+ match task.IsCanceled with
+ | true -> InvalidRepo(uri, "Repo lookup timed out")
+ | false ->
+ match task.IsFaulted with
+ | true -> InvalidRepo(uri, "Not a valid absolute URI")
+ | false -> ValidRepo task.Result
+
+ let (user, repo) = splitIntoOwnerAndRepo uri
+ let githubClient = getGithubClient ()
+
+ githubClient.Repository.Get(user, repo).ContinueWith continuation
+ |> Async.AwaitTask
+ |> pipeToWithSender mailbox.Self mailbox.Context.Sender // send the message back to ourselves but pass the real sender through
+ | InvalidRepo (uri, reason) ->
+ InvalidRepo(uri, reason) |> mailbox.Context.Sender.Forward
+ | ValidRepo repo ->
+ mailbox.Context.ActorSelection("akka://GithubActors/user/commander")
+ mailbox.Context.ActorSelection("akka://GithubActors/user/mainform")
+ mailbox.Context.ActorSelection("akka://GithubActors/user/mainform") return! processMessage ()
+
+ return! processMessage ()
+ }
+
+ processMessage ()
+
+ let githubWorkerActor (mailbox: Actor<_>) =
+
+ let githubClient = lazy (GithubClientFactory.getClient ())
+
+ let rec processMessage () = actor {
+ let! message = mailbox.Receive ()
+
+ match message with
+ | RetryableQuery query when isQueryStarrer query.Query || isQueryStarrers query.Query ->
+ match query.Query :?> GithubActorMessage with
+ | QueryStarrer login ->
+ let sender = mailbox.Context.Sender
+
+ let continuation (task: System.Threading.Tasks.Task>) : GithubActorMessage =
+ if task.IsFaulted || task.IsCanceled then
+ RetryableQuery(nextTry query)
+ else
+ StarredReposForUser(login, task.Result)
+
+ githubClient.Value.Activity.Starring.GetAllForUser(login).ContinueWith continuation
+ |> Async.AwaitTask
+ |!> sender
+ | QueryStarrers repoKey ->
+ let sender = mailbox.Context.Sender
+
+ let continuation (task: System.Threading.Tasks.Task>) : GithubActorMessage =
+ if task.IsFaulted || task.IsCanceled then
+ RetryableQuery(nextTry query)
+ else
+ task.Result |> Seq.toArray |> UsersToQuery // returns the list of users
+
+ githubClient.Value.Activity.Starring.GetAllStargazers(repoKey.Owner, repoKey.Repo).ContinueWith continuation
+ |> Async.AwaitTask
+ |!> sender
+ | _ -> () // never reached
+ | _ -> ()
+
+ return! processMessage ()
+ }
+
+ processMessage ()
+
+
+ let githubCoordinatorActor (mailbox: Actor<_>) =
+
+ let startWorking repoKey (scheduler: IScheduler) =
+ {
+ ReceivedInitialUsers = false
+ CurrentRepo = repoKey
+ Subscribers = System.Collections.Generic.HashSet ()
+ SimilarRepos = System.Collections.Generic.Dictionary ()
+ GithubProgressStats = getDefaultStats ()
+ PublishTimer = new Cancelable (scheduler)
+ }
+
+ // pre-start
+ let githubWorker = spawnOpt mailbox.Context "worker" githubWorkerActor [ SpawnOption.Router(RoundRobinPool(10)) ]
+
+ let rec waiting () =
+ actor {
+ let! message = mailbox.Receive ()
+
+ match message with
+ | CanAcceptJob repoKey ->
+ mailbox.Context.Sender
+ githubWorker return! waiting ()
+
+ return! waiting ()
+ }
+ and working (settings: WorkerSettings) =
+ actor {
+ let! message = mailbox.Receive ()
+
+ match message with
+ // received a downloaded user back from the github worker
+ | StarredReposForUser (login, repos) ->
+ repos
+ |> Seq.iter (fun repo ->
+ if not <| settings.SimilarRepos.ContainsKey repo.HtmlUrl then
+ settings.SimilarRepos.[repo.HtmlUrl] <- { SimilarRepo.Repo = repo; SharedStarrers = 1 }
+ else
+ settings.SimilarRepos.[repo.HtmlUrl] <- increaseSharedStarrers settings.SimilarRepos.[repo.HtmlUrl]
+ )
+
+ return! working {settings with GithubProgressStats = userQueriesFinished settings.GithubProgressStats 1 }
+ | PublishUpdate ->
+ // Check to see if the job has fully completed
+ match settings.ReceivedInitialUsers && settings.GithubProgressStats.IsFinished with
+ | true ->
+ let finishStats = finish settings.GithubProgressStats
+
+ // All repos minus forks of the current one
+ let sortedSimilarRepos =
+ settings.SimilarRepos.Values
+ |> Seq.filter (fun repo -> repo.Repo.Name <> settings.CurrentRepo.Repo)
+ |> Seq.sortBy (fun repo -> -repo.SharedStarrers)
+
+ // Update progress (both repos and users)
+ settings.Subscribers
+ |> Seq.iter (fun subscriber ->
+ subscriber
+ settings.Subscribers
+ |> Seq.iter (fun subscriber -> subscriber
+ // queue all the jobs
+ users |> Seq.iter (fun user -> githubWorker
+ mailbox.Context.Sender
+ // this is our first subscriber, which means we need to turn publishing on
+ if settings.Subscribers.Count = 0 then
+ mailbox.Context.System.Scheduler.ScheduleTellRepeatedly(
+ TimeSpan.FromMilliseconds 100., TimeSpan.FromMilliseconds 30.,
+ mailbox.Self, PublishUpdate, mailbox.Self, settings.PublishTimer)
+ settings.Subscribers.Add subscriber |> ignore
+
+ // query failed, but can be retried
+ | RetryableQuery query when query.CanRetry ->
+ githubWorker
+ settings.Subscribers
+ |> Seq.iter (fun subscriber -> subscriber
+ return! working {settings with GithubProgressStats = incrementFailures settings.GithubProgressStats 1 }
+ | _ -> return! working settings
+
+ return! working settings
+ }
+
+ waiting ()
+
+
+ let githubCommanderActor (mailbox: Actor<_>) =
+
+ let timeout = Nullable(TimeSpan.FromSeconds 3.)
+ mailbox.Context.SetReceiveTimeout timeout
+ mailbox.Context.SetReceiveTimeout (Nullable())
+
+ // pre-start
+ let coordinator = spawnOpt mailbox.Context "coordinator" githubCoordinatorActor [ SpawnOption.Router(FromConfig.Instance) ]
+
+ // post-stop, kill off the old coordinator so we can recreate it from scratch
+ mailbox.Defer (fun _ -> coordinator
+ match githubMessage with
+ | CanAcceptJob repoKey ->
+ coordinator Async.RunSynchronously
+
+ mailbox.Context.SetReceiveTimeout (Nullable(TimeSpan.FromSeconds 3.))
+ return! asking mailbox.Context.Sender (routees.Members.Count ())
+ | _ -> return! ready canAcceptJobSender pendingJobReplies
+ | _ -> return! ready canAcceptJobSender pendingJobReplies
+ }
+ // pass around the actor that sent the CanAcceptJob message as well as the current number of pending jobs
+ and asking canAcceptJobSender pendingJobReplies =
+ actor {
+ let! message = mailbox.Receive ()
+
+ match box message with
+ | :? ReceiveTimeout as timeout ->
+ canAcceptJobSender
+ match githubMessage with
+ | CanAcceptJob repoKey ->
+ mailbox.Stash ()
+ return! asking canAcceptJobSender pendingJobReplies
+ | UnableToAcceptJob repoKey ->
+ let currentPendingJobReplies = pendingJobReplies - 1
+ if currentPendingJobReplies = 0 then
+ canAcceptJobSender
+ canAcceptJobSender return! asking canAcceptJobSender pendingJobReplies
+ | _ -> return! asking canAcceptJobSender pendingJobReplies
+ }
+
+ ready null 0
+
+
+ let repoResultsActor (usersGrid: DataGridView) (statusLabel: ToolStripStatusLabel) (progressBar: ToolStripProgressBar) (mailbox: Actor<_>) =
+ let startProgress stats =
+ progressBar.Minimum <- 0
+ progressBar.Step <- 1
+ progressBar.Maximum <- stats.ExpectedUsers
+ progressBar.Value <- stats.UsersThusFar
+ progressBar.Visible <- true
+ statusLabel.Visible <- true
+
+ let displayProgress stats =
+ statusLabel.Text <- sprintf "%i out of %i users (%i failures) [%A elapsed]" stats.UsersThusFar stats.ExpectedUsers stats.QueryFailures stats.Elapsed
+
+ let stopProgress repo =
+ progressBar.Visible <- true
+ progressBar.ForeColor <- Color.Red
+ progressBar.Maximum <- 1
+ progressBar.Value <- 1
+ statusLabel.Visible <- true
+ statusLabel.Text <- sprintf "Failed to gather date for GitHub repository %s / %s" repo.Owner repo.Repo
+
+ let displayRepo similarRepo =
+ let repo = similarRepo.Repo
+ let row = new DataGridViewRow()
+ row.CreateCells usersGrid
+ row.Cells.[0].Value <- repo.Owner.Login
+ row.Cells.[1].Value <- repo.Owner.Name
+ row.Cells.[2].Value <- repo.Owner.HtmlUrl
+ row.Cells.[3].Value <- similarRepo.SharedStarrers
+ row.Cells.[4].Value <- repo.OpenIssuesCount
+ row.Cells.[5].Value <- repo.StargazersCount
+ row.Cells.[6].Value <- repo.ForksCount
+ usersGrid.Rows.Add row |> ignore
+
+ let mutable hasSetProgress = false
+ let rec processMessage () = actor {
+ let! message = mailbox.Receive ()
+
+ match message with
+ | GithubProgressStats stats -> // progress update
+ if not hasSetProgress && stats.ExpectedUsers > 0 then
+ startProgress stats
+ hasSetProgress <- true
+ displayProgress stats
+ progressBar.Value <- stats.UsersThusFar + stats.QueryFailures
+ | SimilarRepos repos -> // user update
+ repos |> Seq.iter displayRepo
+ | JobFailed repoKey -> // critical failure, like not being able to connect to Github
+ stopProgress repoKey
+ | _ -> ()
+
+ return! processMessage ()
+ }
+
+ processMessage ()
\ No newline at end of file
diff --git a/src/Unit-3/lesson5/Completed/Actors/GithubAuthenticationActor.cs b/src/Unit-3/lesson5/Completed/Actors/GithubAuthenticationActor.cs
deleted file mode 100644
index 8c2523ce9..000000000
--- a/src/Unit-3/lesson5/Completed/Actors/GithubAuthenticationActor.cs
+++ /dev/null
@@ -1,87 +0,0 @@
-using System.Drawing;
-using Akka.Actor;
-using Octokit;
-using Label = System.Windows.Forms.Label;
-
-namespace GithubActors.Actors
-{
- public class GithubAuthenticationActor : ReceiveActor
- {
- #region Messages
-
- public class Authenticate
- {
- public Authenticate(string oAuthToken)
- {
- OAuthToken = oAuthToken;
- }
-
- public string OAuthToken { get; private set; }
- }
-
- public class AuthenticationFailed { }
-
- public class AuthenticationCancelled { }
-
- public class AuthenticationSuccess { }
-
- #endregion
-
- private readonly Label _statusLabel;
- private readonly GithubAuth _form;
-
- public GithubAuthenticationActor(Label statusLabel, GithubAuth form)
- {
- _statusLabel = statusLabel;
- _form = form;
- Unauthenticated();
- }
-
- private void Unauthenticated()
- {
- Receive(auth =>
- {
- //need a client to test our credentials with
- var client = GithubClientFactory.GetUnauthenticatedClient();
- GithubClientFactory.OAuthToken = auth.OAuthToken;
- client.Credentials = new Credentials(auth.OAuthToken);
- BecomeAuthenticating();
- client.User.Current().ContinueWith