/// <summary> /// Solve the Problem locally in the WebRole/Hub asynchronously using Rx Observable sequences to capture and signal updates to the Client(s) /// </summary> /// <param name="problemToSolve"><see cref="WaterBucket.Domain.Problem"/> to be solved and calculate the results</param> /// <param name="work">The <see cref="WaterBucketWeb.Models.WorkOrder"/> passed from the Client determining how to perform the work of solving the <see cref="WaterBucket.Domain.Problem"/></param> /// <returns>Task resulting in <see cref="WaterBucket.Domain.SolutionResult"/>s for the problem for each <see cref="WaterBucket.Domain.ISolutionStrategy"/> employed</returns> protected async Task<SolutionResult[]> SolveAProblemAsync(Problem problemToSolve, WorkOrder work)//, int delayStart, int workDelay) { if (work.StartDelay > 0) Thread.Sleep(work.StartDelay); ISolutionStrategy smallToBig, bigToSmall; smallToBig = new SmallToBigSingleBucketSolutionStrategy(problemToSolve); bigToSmall = new BigToSmallSingleBucketSolutionStrategy(problemToSolve); var startSmall = from step in problemToSolve.Solve(smallToBig) select step; //startSmall.ToObservable().Subscribe(ProblemActionStep, ProblemActionError, ProblemSolutionCompleted); // Example of doing all of the code on a single line //problemToSolve.Solve(new SmallToBigSingleBucketSolutionStrategy(problemToSolve)).ToObservable().Subscribe(ProblemActionStep, ProblemActionError, () => ProblemSolutionCompleted("Small to Big")); // Capture as primitive value type rather than rely on reference type for use in Tasks - was burned on this with the PublisherAddresses list bool yieldOnAction = work.YieldWeb; // Set up a Task to run the Observable in but don't wait on it yet var smallToBigTask = Task.Run(() => (work.WorkDelay > 0 ? startSmall.ToObservable().Do(_ => Thread.Sleep(work.WorkDelay)) : startSmall.ToObservable()) .Subscribe(step => ProblemActionStep(step, yieldOnAction), ProblemActionError, () => ProblemSolutionCompleted("Small to Big"))) .ContinueWith(t => { if (t.IsCanceled) { } else if (t.IsFaulted) { } else if (t.IsCompleted) { t.Result.Dispose(); SignalSolution(smallToBig.StrategyName, smallToBig.Result); return smallToBig.Result; } return null; }); var startBig = from step in problemToSolve.Solve(bigToSmall) select step; //var startBig = (from step in problemToSolve.Solve(bigToSmall) // select step).ToObservable(); //if (workDelay > 0) //{ // var timer = Observable.Interval(TimeSpan.FromMilliseconds(workDelay)); // startBig = startBig.Zip(timer, (bs, _) => bs); //} // Set up a Task to run the Observable in but don't wait on it yet var bigToSmallTask = Task.Run(() => (work.WorkDelay > 0 ? startBig.ToObservable().Do(_ => Thread.Sleep(work.WorkDelay)) : startBig.ToObservable()) .Subscribe(step => ProblemActionStep(step, yieldOnAction), ProblemActionError, () => ProblemSolutionCompleted("Big to Small"))) .ContinueWith(t => { if (t.IsCanceled) { } else if (t.IsFaulted) { } else if (t.IsCompleted) { t.Result.Dispose(); SignalSolution(bigToSmall.StrategyName, bigToSmall.Result); return smallToBig.Result; } return null; }); // Wait for the Observable execution tasks to complete before finishing return await Task.WhenAll(smallToBigTask, bigToSmallTask); }
// Deprecated message for SignedUpdates with the theory of having a single ZmqSocket subscribe to all // messages for a problem and then separate/parse them by which strategy the message is an update for // and pass that appropriately to the client(s) //protected IEnumerable<SignedProblemUpdate> GetProblemUpdates(ZmqSocket subscriber, string signature) //{ // if (subscriber == null) // throw new ArgumentNullException("subscriber"); // SignedProblemUpdate update; // do // { // update = null; // // This is the blocking version of Receiving from a ZMQ Socket - this is okay since we will wrap this call in Rx // ZmqMessage msg = subscriber.ReceiveMessage(); // // To decouple ProblemUpdate from the Transport Medium, only binary data is passed using byte[] instead of a ZeroMQ.Frame // update = new SignedProblemUpdate(msg.Select(f => f.Buffer).ToArray()); // if (!signature.Equals(update.Signature)) // continue; // if (update.IsAction || update.IsInitial) // yield return update; // } while ((update != null) && !(update.IsCompletion || update.IsError)); // if (update != null) // { // if (update.IsCompletion) // { // yield return update; // } // else if (update.IsError) // { // throw update.GetException<Exception>(); // } // } // yield break; //} /// <summary> /// Solve the Problem using a WorkerRole hosted in Azure PaaS Cloud asynchronously by queuing the problem in an Azure Storage CloudQueue and /// listening on ZmqSockets for update messages from the Worker doing the work as the work is being done. /// <para>Based on a setting by the Client in the WorkOrder optionally use an Rx Observable sequences over the message stream to signal /// updates to the Client(s)</para> /// </summary> /// <param name="problemToSolve"><see cref="WaterBucket.Domain.Problem"/> to be solved and calculate the results</param> /// <param name="work">The <see cref="WaterBucketWeb.Models.WorkOrder"/> passed from the Client determining how to perform the work of solving the <see cref="WaterBucket.Domain.Problem"/></param> /// <returns>Task that can be awaited on for work being completed by the Worker</returns> /// <seealso cref="Microsoft.WindowsAzure.Storage.Queue.CloudQueue"/> /// <seealso cref="ZeroMQ"/> protected async Task SolveProblemWithWorkerAsync(Problem problemToSolve, WorkOrder work)//, int delayStart, int workDelay) { // Calling this here will ensure that it is trying to find workers anytime it needs them // rather than prefetching PublisherAddresses, finding out they're not initialized and // never being able to use workers after that // - fortunately or unfortunately, it doesn't matter in SignalR since Hubs are reconstructed // for every call to the Hub so the result if called in the ctor wouldn't be cached accross calls //InitPublisherAddresses(); //if (!CanUseWorker) //{ // Clients.All.addMsg("Cannot use Worker for calculations, reason: " + NoWorkerReason); // return; //} // Check whether the CloudQueue is for the staging or production environment, which use separate Queues in order to provide fully isolated environments bool isStaging = RoleEnvironmentExt.GetRoleConfigSetting("UseStaging", false); // Get the CloudQueue on which we want to put messages for the WorkerRoles to get their work string storageAccountConStr = !isStaging ? RoleEnvironment.GetConfigurationSettingValue("StorageAccount") : RoleEnvironment.GetConfigurationSettingValue("StagingStorageAccount"); CloudStorageAccount storageAccount = CloudStorageAccount.Parse(storageAccountConStr); CloudQueueClient qClient = storageAccount.CreateCloudQueueClient(); CloudQueue queue = qClient.GetQueueReference(CLOUD_Q_FOR_WORK); queue.CreateIfNotExists(); try { ZmqContext ctx = ZmqContext.Create(); //ZmqSocket socket = ctx.CreateSocket(SocketType.SUB); //socket.Connect(publishAddress); //List<ISolutionStrategy> strategies = new List<ISolutionStrategy>(); //strategies.Add(new BigToSmallSingleBucketSolutionStrategy(problemToSolve)); //strategies.Add(new SmallToBigSingleBucketSolutionStrategy(problemToSolve)); //IObservable<SignedProblemUpdate> combinedUpdates = null; //foreach (var s in strategies) //{ // socket.Subscribe(Encoding.UTF8.GetBytes(s.Signature)); // if (combinedUpdates == null) // { // combinedUpdates = GetProblemUpdates(socket, s.Signature).ToObservable(); // } // else // { // combinedUpdates = combinedUpdates.Union(GetProblemUpdates(socket, s.Signature).ToObservable()); // } //} ISolutionStrategy bigToSmall = new BigToSmallSingleBucketSolutionStrategy(problemToSolve); ISolutionStrategy smallToBig = new SmallToBigSingleBucketSolutionStrategy(problemToSolve); //socket.Subscribe(Encoding.UTF8.GetBytes(bigToSmall.Signature)); //socket.Subscribe(Encoding.UTF8.GetBytes(smallToBig.Signature)); //var combinedUpdates = (from u in GetProblemUpdates(socket, bigToSmall.Signature) // select u) // .Union( // from u in GetProblemUpdates(socket, smallToBig.Signature) // select u); //bool done = false; //using (combinedUpdates.ToObservable().Subscribe( // update => // { // if (update.IsAction || update.IsInitial) // { // ProblemActionStep(update.IntoType<BucketActionStep>().UpdateState); // } // else if (update.IsCompletion) // { // ISolutionStrategy signedStrategy = strategies.FirstOrDefault(s => update.Signature.Equals(s.Signature)); // if (signedStrategy != null) // { // var solnResult = update.IntoType<SolutionResult>().UpdateState; // ProblemSolutionCompleted(signedStrategy.StrategyName); // // Don't need this with the bool done // signedStrategy.RemoteResult(solnResult); // SignalSolution(signedStrategy.StrategyName, solnResult); // } // //if (bigToSmall.Signature.Equals(update.Signature)) // //{ // // SignalSolution(bigToSmall.StrategyName, update.IntoType<SolutionResult>().UpdateState); // //} // //else if (smallToBig.Signature.Equals(update.Signature)) // //{ // // SignalSolution(smallToBig.StrategyName, update.IntoType<SolutionResult>().UpdateState); // //} // } // else if (update.IsError) // { // ProblemActionError(update.GetException<Exception>()); // } // }, // ProblemActionError, // () => { done = true; } // )) //{ // byte[] qMsgBytes = new byte[12]; // Buffer.BlockCopy(BitConverter.GetBytes(problemToSolve.FirstBucket.Capacity), 0, qMsgBytes, 0, 4); // Buffer.BlockCopy(BitConverter.GetBytes(problemToSolve.SecondBucket.Capacity), 0, qMsgBytes, 4, 4); // Buffer.BlockCopy(BitConverter.GetBytes(problemToSolve.GoalWaterVolume), 0, qMsgBytes, 8, 4); // CloudQueueMessage qMsg = new CloudQueueMessage(qMsgBytes); // queue.AddMessage(qMsg); // while (!done) // { // Thread.Sleep(500); // } //} Task b2sWork, s2bWork; // Put the work.YieldWeb value in a local value type which is easier to pass to child Tasks bool yieldOnAction = work.YieldWeb; // The Client does NOT want to use a Rx Observable to wrap the message stream from the ZmqSocket if (!work.ObserverOnZmq) { // Start tasks to listen for Worker updates on their own ZmqSockets per each SolutionStrategy that will be employed to solve the problem and signal Client(s) as they come in b2sWork = Task.Run(() => { try { foreach (var step in GetWorkerUpdates(ctx, bigToSmall, result => SignalSolution(bigToSmall.StrategyName, result))) { ProblemActionStep(step, yieldOnAction); } ProblemSolutionCompleted("Big to Small"); } catch (Exception ex) { ProblemActionError(ex); } }); s2bWork = Task.Run(() => { try { foreach (var step in GetWorkerUpdates(ctx, smallToBig, result => SignalSolution(smallToBig.StrategyName, result))) { ProblemActionStep(step, yieldOnAction); } ProblemSolutionCompleted("Small to Big"); } catch (Exception ex) { ProblemActionError(ex); } }); } else // The Client wants to use a Rx Observable to wrap the message stream from the ZmqSocket { // First create a lazy evaluated Linq Sequence around getting ZmqSocket subscription messages for a specific SolutionStrategy var b2sUpdates = from u in GetWorkerUpdates(ctx, bigToSmall, result => SignalSolution(bigToSmall.StrategyName, result)) select u; // Start a task in which a Rx Observable will wrap the message stream and signal the Client(s) when messages are received b2sWork = Task.Run(() => b2sUpdates.ToObservable() .Subscribe(step => ProblemActionStep(step, yieldOnAction), ProblemActionError, () => ProblemSolutionCompleted("Big to Small"))) .ContinueWith(t => { if (t.IsCanceled) { // TODO: Log this } else if (t.IsFaulted) { // TODO: Log the Exception } else if (t.IsCompleted) { t.Result.Dispose(); } }); // Repeat the above steps for the other SolutionStrategy var s2bUpdates = from u in GetWorkerUpdates(ctx, smallToBig, result => SignalSolution(smallToBig.StrategyName, result)) select u; s2bWork = Task.Run(() => s2bUpdates.ToObservable() .Subscribe(step => ProblemActionStep(step, yieldOnAction), ProblemActionError, () => ProblemSolutionCompleted("Small to Big"))) .ContinueWith(t => { if (t.IsFaulted) { // TODO: Log the Exception } else if (t.IsCanceled) { // TODO: Log this } else if (t.IsCompleted) { t.Result.Dispose(); } }); } // To ensure the Subscriptions occur before any messages are published by the Worker, we could call Thread.Yield here // and we will get back control once those Tasks block on attempting to receive a message //Thread.Yield(); // BUT - we want to see the effects of delays on publishing and the Zmq PUB-SUB messaging so we're not doing that now // Create the message to be put into the Azure Storage CloudQueue that is picked up by the Workers byte[] qMsgBytes = new byte[20]; Buffer.BlockCopy(BitConverter.GetBytes(problemToSolve.FirstBucket.Capacity), 0, qMsgBytes, 0, 4); Buffer.BlockCopy(BitConverter.GetBytes(problemToSolve.SecondBucket.Capacity), 0, qMsgBytes, 4, 4); Buffer.BlockCopy(BitConverter.GetBytes(problemToSolve.GoalWaterVolume), 0, qMsgBytes, 8, 4); Buffer.BlockCopy(BitConverter.GetBytes(work.StartDelay), 0, qMsgBytes, 12, 4); // be able to signal to the worker the WorkDelay or whether it should Yield on each Action (workDelay == 0) or Not to Yield (workDelay < 0) int workDelay = work.WorkDelay > 0 ? work.WorkDelay : work.YieldWorker ? 0 : -1; Buffer.BlockCopy(BitConverter.GetBytes(workDelay), 0, qMsgBytes, 16, 4); CloudQueueMessage qMsg = new CloudQueueMessage(qMsgBytes); queue.AddMessage(qMsg); // Now wait for the Subscription tasks to complete await Task.WhenAll(b2sWork, s2bWork); } catch (Exception ex) { // TODO: Log any uncaught exception throw new Exception("Encountered Exception during WorkerAsync", ex); } }
/// <summary> /// Get a message from the Client to Solve a given problem using the given work order /// </summary> /// <param name="problem"><see cref="WaterBucket.Domain.Problem"/> defining the Bucket and Goal Parameters to use in the calculation</param> /// <param name="work">WorkOrder parameters used during execution of the solution</param> public void Solve(ProblemVM problem, WorkOrder work)//, bool useWorker = false, int delayStart = -1, int workDelay = -1) { if (BroadcastAll) { Clients.All.problemSubmitted(ObservableOnJavaScript); Clients.Others.submission(problem, work.UseWorker); } else { Clients.Caller.problemSubmitted(ObservableOnJavaScript); } // Test Bucket Ranges here instead of at the browser so that updates made on the back end for thresholds are checked without requiring browser refresh bool end = false; int bigBucket = Math.Max(problem.FirstBucketCapacity, problem.SecondBucketCapacity); if (bigBucket > BucketMax) { if (BroadcastAll) { Clients.All.outOfRange(bigBucket, true); } else { Clients.Caller.outOfRange(bigBucket, true); } end = true; } int smallBucket = Math.Min(problem.FirstBucketCapacity, problem.SecondBucketCapacity); if (smallBucket < BucketMin) { if (BroadcastAll) { Clients.All.outOfRange(smallBucket, false); } else { Clients.Caller.outOfRange(smallBucket, false); } end = true; } if (end) return; // Check to see if the Problem exists in the Cache Problem problemo = HttpContext.Current.Cache[problem.CacheKey] as Problem; // If not, create a new Problem based on the parameters to solve and add it to the Cache if (problemo == null) { problemo = new Problem(problem.FirstBucketCapacity, problem.SecondBucketCapacity, problem.GoalWaterVolume); HttpContext.Current.Cache.Insert(problem.CacheKey, problemo); } // Notify Client(s) that the Problem is being started // ActionBindingThreshold tells the client browser whether to use knockout.js observableArray binding for displaying action step updates or // plain old DOM manipulation - ko.observableArray creates a big performance hit bool bindActions = bigBucket <= ActionBindingThreshold; if (BroadcastAll) { Clients.All.startedProblem(problem, bindActions && !work.UseWorker); } else { Clients.Caller.startedProblem(problem, bindActions && !work.UseWorker); } // Check to see if it is a solvable Problem if (!problemo.IsSolvable) { // If not a solvable problem then notify the clients and do not try to solve it if (BroadcastAll) { Clients.All.notSolvable(problem); } else { Clients.Caller.notSolvable(problem); } } else { // Try-Catch didn't work here to solve the problem I was having //try //{ var problemTasks = work.UseWorker ? SolveProblemWithWorkerAsync(problemo, work) : SolveAProblemAsync(problemo, work); // problemTasks.ContinueWith(t => // { // if (t.IsFaulted) // Clients.All.errorInSolve(t.Exception); // }); //} //catch (Exception ex) //{ // Clients.All.errorInSolve(ex); //} } }