{"action":"created","comment":{"url":"https://api.github.com/repos/Azure/device-simulation-dotnet/pulls/comments/225325944","pull_request_review_id":164907631,"id":225325944,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIyNTMyNTk0NA==","diff_hunk":"@@ -9,271 +10,426 @@\n using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency;\n using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;\n using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;\n+using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime;\n+using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceConnection;\n+using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceProperties;\n+using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceState;\n+using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceTelemetry;\n+using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.SimulationThreads;\n using static Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models.Simulation;\n \n namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent\n {\n     public interface ISimulationAgent\n     {\n-        Task RunAsync();\n-        Task AddDeviceAsync(string name, string modelId);\n+        Task StartAsync();\n+        Task AddDeviceAsync(string simulationId, string name, string modelId);\n         Task DeleteDevicesAsync(List<string> ids);\n         void Stop();\n     }\n \n     public class Agent : ISimulationAgent\n     {\n-        private const int CHECK_INTERVAL_MSECS = 10000;\n+        // Wait a few seconds after checking if there are new simulations\n+        // or new devices, to avoid overloading the database with queries.\n+        private const int PAUSE_AFTER_CHECK_MSECS = 20000;\n+        private const int SHUTDOWN_WAIT_INTERVAL_MSECS = 1000;\n+        private const int SHUTDOWN_WAIT_BEFORE_STOPPING_THREADS_MSECS = 3000;\n         private const int DIAGNOSTICS_POLLING_FREQUENCY_DAYS = 1;\n \n+        // How often (minimum) to log simulation statistics\n+        private const int STATS_INTERVAL_MSECS = 15000;\n+\n+        private readonly ISimulationConcurrencyConfig simulationConcurrencyConfig;\n         private readonly ILogger log;\n         private readonly IDiagnosticsLogger logDiagnostics;\n         private readonly ISimulations simulations;\n-        private readonly ISimulationRunner runner;\n-        private readonly IRateLimiting rateReporter;\n-        private readonly IDeviceModels deviceModels;\n-        private readonly IDevices devices;\n+        private readonly IFactory factory;\n         private DateTimeOffset lastPolledTime;\n-        private Simulation simulation;\n         private bool running;\n \n+        // The thread responsible for updating devices/sensors state\n+        private Thread devicesStateThread;\n+        private IDeviceStateTask devicesStateTask;\n+\n+        // The thread responsible for connecting all the devices to Azure IoT Hub\n+        private Thread devicesConnectionThread;\n+        private IDeviceConnectionTask devicesConnectionTask;\n+\n+        // The thread responsible for sending device property updates to Azure IoT Hub\n+        private Thread devicesPropertiesThread;\n+        private IUpdatePropertiesTask devicesPropertiesTask;\n+\n+        // Array of threads used to send telemetry\n+        private Thread[] devicesTelemetryThreads;\n+        private List<IDeviceTelemetryTask> devicesTelemetryTasks;\n+\n+        // List of simulation managers, one for each simulation\n+        private readonly ConcurrentDictionary<string, ISimulationManager> simulationManagers;\n+\n+        // List of all the actors managing the devices state, indexed by Simulation ID + Device ID (string concat)\n+        private readonly ConcurrentDictionary<string, IDeviceStateActor> deviceStateActors;\n+\n+        // Contains all the actors responsible to connect the devices, indexed by Simulation ID + Device ID (string concat)\n+        private readonly ConcurrentDictionary<string, IDeviceConnectionActor> deviceConnectionActors;\n+\n+        // Contains all the actors sending telemetry, indexed by Simulation ID + Device ID (string concat)\n+        private readonly ConcurrentDictionary<string, IDeviceTelemetryActor> deviceTelemetryActors;\n+\n+        // Contains all the actors sending device property updates to Azure IoT Hub, indexed by Simulation ID + Device ID (string concat)\n+        private readonly ConcurrentDictionary<string, IDevicePropertiesActor> devicePropertiesActors;\n+\n+        // Flag signaling whether the simulation is starting (to reduce blocked threads)\n+        private bool startingOrStopping;\n+\n+        // Used to stop the threads\n+        private CancellationTokenSource runningToken;\n+        private long lastStatsTime;\n+\n         public Agent(\n-            ILogger logger,\n-            IDiagnosticsLogger diagnosticsLogger,\n+            ISimulationConcurrencyConfig simulationConcurrencyConfig,\n             ISimulations simulations,\n-            ISimulationRunner runner,\n-            IRateLimiting rateReporter,\n-            IDeviceModels deviceModels,\n-            IDevices devices)\n+            IFactory factory,\n+            ILogger logger,\n+            IDiagnosticsLogger diagnosticsLogger)\n         {\n+            this.simulationConcurrencyConfig = simulationConcurrencyConfig;\n+            this.simulations = simulations;\n+            this.factory = factory;\n             this.log = logger;\n             this.logDiagnostics = diagnosticsLogger;\n-            this.simulations = simulations;\n-            this.runner = runner;\n-            this.rateReporter = rateReporter;\n-            this.deviceModels = deviceModels;\n-            this.devices = devices;\n-            this.running = true;\n+\n+            this.startingOrStopping = false;\n+            this.running = false;\n+            this.runningToken = new CancellationTokenSource();\n             this.lastPolledTime = DateTimeOffset.UtcNow;\n+            this.lastStatsTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();\n+\n+            this.simulationManagers = new ConcurrentDictionary<string, ISimulationManager>();\n+            this.deviceStateActors = new ConcurrentDictionary<string, IDeviceStateActor>();\n+            this.deviceConnectionActors = new ConcurrentDictionary<string, IDeviceConnectionActor>();\n+            this.deviceTelemetryActors = new ConcurrentDictionary<string, IDeviceTelemetryActor>();\n+            this.devicePropertiesActors = new ConcurrentDictionary<string, IDevicePropertiesActor>();\n         }\n \n-        public async Task RunAsync()\n+        public Task StartAsync()\n         {\n-            this.log.Info(\"Simulation Agent running\");\n-\n-            // Keep running, checking if the simulation changes\n-            while (this.running)\n+            if (this.startingOrStopping || this.running)\n             {\n-                var oldSimulation = this.simulation;\n+                this.log.Error(\"The simulation agent can be started only once\");\n+                return Task.CompletedTask;\n+            }\n \n-                this.SendSolutionHeartbeatAsync();\n+            this.running = false;\n+            this.startingOrStopping = true;\n+            this.runningToken.Cancel();\n+            this.runningToken = new CancellationTokenSource();\n \n-                try\n-                {\n-                    this.log.Debug(\"------ Checking for simulation changes ------\");\n+            this.TryToStartThreads();\n \n-                    var simulationList = await this.simulations.GetListAsync();\n+            this.running = true;\n+            this.startingOrStopping = false;\n \n-                    // currently we support only 1 running simulation so the result should return only 1 item\n-                    var runningSimulation = simulationList.FirstOrDefault(s => s.ShouldBeRunning);\n-                    if (runningSimulation == null)\n-                    {\n-                        this.log.Debug(\"No simulations found that should be running. Nothing to do.\");\n-                    }\n+            return this.RunAsync();\n+        }\n \n-                    this.log.Debug(\"Simulation loaded\", () => new { runningSimulation });\n+        public void Stop()\n+        {\n+            // TODO: why is this needed?\n+            while (this.startingOrStopping)\n+                Thread.Sleep(SHUTDOWN_WAIT_INTERVAL_MSECS);\n \n-                    // if the simulation has been removed from storage & we're running, stop the simulation.\n-                    var id = this.simulation?.Id;\n-                    var prevSimulation = simulationList.FirstOrDefault(s => s.Id == id);\n-                    this.CheckForDeletedSimulation(prevSimulation);\n+            this.startingOrStopping = true;\n+            this.log.Write(\"Stopping simulation agent...\");\n \n-                    // if there's a new simulation and it's different from the current one\n-                    // stop the current one from running & start the new one if it's enabled\n-                    await this.CheckForChangedSimulationAsync(runningSimulation);\n+            this.running = false;\n \n-                    // if there's no simulation running but there's one from storage start it\n-                    await this.CheckForNewSimulationAsync(runningSimulation);\n+            // Signal threads to stop\n+            this.runningToken.Cancel();\n+            this.runningToken = new CancellationTokenSource();\n \n-                    // if the current simulation was asked to stop, stop it.\n-                    await this.CheckForStopOrStartToSimulationAsync();\n-                }\n-                catch (Exception e)\n-                {\n-                    this.log.Error(\"Failure reading and starting simulation from storage.\", e);\n-                    this.simulation = oldSimulation;\n-                }\n+            // Allow some time to pass before trying to stop threads\n+            Thread.Sleep(SHUTDOWN_WAIT_BEFORE_STOPPING_THREADS_MSECS);\n+            this.TryToStopThreads();\n+        }\n \n-                if (this.simulation != null && this.simulation.ShouldBeRunning)\n+        private async Task RunAsync()\n+        {\n+            try\n+            {\n+                while (this.running)\n                 {\n-                    this.log.Debug(\"------ Current simulation being run ------\");\n-                    foreach (var model in this.simulation.DeviceModels)\n-                    {\n-                        this.log.Debug(\"Device model\", () => new { model });\n-                    }\n-                }\n+                    this.SendSolutionHeartbeatAsync();\n+\n+                    this.log.Debug(\"Starting simulation agent loop\",\n+                        () => new { SimulationsCount = this.simulationManagers.Count });\n+\n+                    // Get the list of active simulations. Active simulations are already partitioned.\n+                    IList<Simulation> activeSimulations = (await this.simulations.GetListAsync())\n+                        .Where(x => x.IsActiveNow).ToList();\n+                    this.log.Debug(\"Active simulations loaded\", () => new { activeSimulations.Count });\n \n-                Thread.Sleep(CHECK_INTERVAL_MSECS);\n+                    await this.CreateSimulationManagersAsync(activeSimulations);\n+                    await this.RunSimulationManagersMaintenanceAsync();\n+                    this.StopInactiveSimulations(activeSimulations);\n+\n+                    Thread.Sleep(PAUSE_AFTER_CHECK_MSECS);\n+                }\n+            }\n+            catch (Exception e)\n+            {\n+                this.log.Error(\"A critical error occurred in the simulation agent\", e);\n+                this.Stop();\n             }\n         }\n \n-        public void Stop()\n+        // TODO: Implement support for adding devices to a running simulation.\n+        //       This functionality is needed for Remote Monitoring, but the\n+        //       initial implementation of large-scale device simulation\n+        //       does not support this because we do not have a design for\n+        //       how to modify existing partitions at runtime. Implementation\n+        //       of this feature is pending a design for this.\n+        public async Task AddDeviceAsync(string simulationid, string deviceId, string modelId)\n         {\n-            this.simulation = null;\n-            this.running = false;\n-            this.runner.Stop();\n+            return;\n         }\n \n-        public async Task AddDeviceAsync(string deviceId, string modelId)\n+        // TODO: Implement support for removing devices from a running simulation.\n+        //       This functionality is needed for Remote Monitoring, but the\n+        //       initial implementation of large-scale device simulation\n+        //       does not support this because we do not have a design for\n+        //       how to modify existing partitions at runtime. Implementation\n+        //       of this feature is pending a design for this.\n+        public async Task DeleteDevicesAsync(List<string> ids)\n         {\n-            if (this.simulation != null && this.IsDeviceModelIdValidAsync(modelId))\n-            {\n-                try\n-                {\n-                    await this.AddDeviceToSimulationRecordAsync(this.simulation, deviceId, modelId);\n+            return;\n+        }\n \n-                    if (this.running)\n-                    {\n-                        this.log.Info(\"Add device to running simulation\");\n-                        await this.runner.AddDeviceAsync(deviceId, modelId);\n-                    }\n-                    else\n-                    {\n-                        this.log.Info(\"Add device to IoT Hub\");\n-                        await this.simulations.AddDeviceAsync(deviceId);\n-                    }\n-                }\n-                catch (Exception e)\n+        private void StopInactiveSimulations(IList<Simulation> activeSimulations)\n+        {\n+            // Get a list of all managers that are not active in storage\n+            var activeIds = activeSimulations.Select(simulation => simulation.Id).ToList();\n+            var managersToStop = this.simulationManagers.Where(x => !activeIds.Contains(x.Key)).ToList();\n+\n+            foreach (var manager in managersToStop)\n+            {\n+                manager.Value.TearDown();\n+                if (!this.simulationManagers.TryRemove(manager.Key, out _))\n                 {\n-                    this.log.Debug(\"Error while adding new device\", () => new { e });\n-                    throw new Exception(\"Error while adding a new device\");\n+                    this.log.Error(\"Unable to remove simulation manager from the list of managers\",\n+                        () => new { SimulationId = manager.Key });\n                 }\n             }\n         }\n \n-        public async Task DeleteDevicesAsync(List<string> ids)\n+        private async Task CreateSimulationManagersAsync(IEnumerable<Simulation> activeSimulations)\n         {\n-            this.log.Info(\"Update simulation\");\n+            // Skip simulations not ready or already with a manager\n+            var activeSimulationlist = activeSimulations\n+                .Where(x => x.ShouldBeRunning && !this.simulationManagers.ContainsKey(x.Id));\n \n-            try\n+            foreach (var simulation in activeSimulationlist)\n             {\n-                if (this.simulation != null)\n+                try\n                 {\n-                    await this.DeleteDevicesFromSimulationRecordAsync(this.simulation, ids);\n+                    var manager = this.factory.Resolve<ISimulationManager>();\n+                    await manager.InitAsync(\n+                        simulation,\n+                        this.deviceStateActors,\n+                        this.deviceConnectionActors,\n+                        this.deviceTelemetryActors,\n+                        this.devicePropertiesActors);\n \n-                    if (this.running)\n-                    {\n-                        this.log.Info(\"Deleting devices from running simulation\");\n-                        this.runner.DeleteDevices(ids);\n-                    }\n-                    else\n-                    {\n-                        this.log.Info(\"Deleting devices from hub\");\n-                        await this.devices.DeleteListAsync(ids);\n-                    }\n+                    this.simulationManagers[simulation.Id] = manager;\n+\n+                    this.log.Info(\"New simulation manager created\", () => new { SimulationId = simulation.Id });\n+                }\n+                catch (Exception e)\n+                {\n+                    this.log.Error(\"Failed to create simulation manager, will retry\", () => new { simulation.Id, e });\n                 }\n-            }\n-            catch (Exception e)\n-            {\n-                this.log.Debug(\"Error while deleting new device\", () => new { e });\n-                throw new Exception(\"Error while deleting a new device\");\n             }\n         }\n \n-        private void CheckForDeletedSimulation(Simulation newSimulation)\n+        private async Task RunSimulationManagersMaintenanceAsync()\n         {\n-            if (newSimulation == null && this.simulation != null)\n+            var printStats = false;\n+            var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();\n+            if (now - this.lastStatsTime > STATS_INTERVAL_MSECS)\n             {\n-                this.runner.Stop();\n-\n-                this.simulation = null;\n-                this.log.Debug(\"The current simulation is no longer in storage...\");\n+                printStats = true;\n+                this.lastStatsTime = now;\n             }\n-        }\n \n-        private async Task CheckForChangedSimulationAsync(Simulation newSimulation)\n-        {\n-            if (newSimulation != null && this.simulation != null &&\n-                newSimulation.Modified != this.simulation.Modified)\n+            // TODO: determine if these can be run in parallel\n+            foreach (var manager in this.simulationManagers)\n             {\n-                this.log.Debug(\"The simulation has been modified, stopping the current \" +\n-                               \"simulation and starting the new one if enabled\");\n-                this.runner.Stop();\n+                await TryToAsync(manager.Value.HoldAssignedPartitionsAsync(),\n+                    e => this.log.Error(\"An unexpected error occurred while renewing partition locks\", e));\n+\n+                await TryToAsync(manager.Value.AssignNewPartitionsAsync(),\n+                    e => this.log.Error(\"An unexpected error occurred while assigning new partitions\", e));\n \n-                this.simulation = newSimulation;\n+                await TryToAsync(manager.Value.HandleAssignedPartitionChangesAsync(),\n+                    e => this.log.Error(\"An unexpected error occurred while handling partition changes\", e));\n \n-                if (this.simulation.ShouldBeRunning)\n+                await TryToAsync(manager.Value.UpdateThrottlingLimitsAsync(),\n+                    e => this.log.Error(\"An unexpected error occurred while updating the throttling limits\", e));\n+\n+                if (printStats) manager.Value.PrintStats();\n+            }\n+\n+            async Task TryToAsync(Task task, Action<Exception> onException)\n+            {\n+                try\n                 {\n-                    this.log.Debug(\"------ Starting simulation ------\", () => new { this.simulation });\n-                    await this.runner.StartAsync(this.simulation);\n-                    this.log.Debug(\"------ Simulation started ------\", () => new { this.simulation });\n+                    await task;\n+                }\n+                catch (Exception e)\n+                {\n+                    onException.Invoke(e);\n                 }\n             }\n         }\n \n-        private void SendSolutionHeartbeatAsync()\n+        private void TryToStartThreads()\n         {\n-            DateTimeOffset now = DateTimeOffset.UtcNow;\n-            TimeSpan duration = now - this.lastPolledTime;\n+            this.devicesStateTask = this.factory.Resolve<IDeviceStateTask>();\n+            this.devicesStateThread = new Thread(\n+                () => this.devicesStateTask.Run(this.deviceStateActors, this.runningToken.Token));\n+\n+            this.devicesConnectionTask = this.factory.Resolve<IDeviceConnectionTask>();\n+            this.devicesConnectionThread = new Thread(\n+                () => this.devicesConnectionTask.RunAsync(\n+                    this.simulationManagers,\n+                    this.deviceConnectionActors,\n+                    this.runningToken.Token));\n+\n+            this.devicesPropertiesTask = this.factory.Resolve<IUpdatePropertiesTask>();\n+            this.devicesPropertiesThread = new Thread(\n+                () => this.devicesPropertiesTask.RunAsync(\n+                    this.simulationManagers,\n+                    this.devicePropertiesActors,\n+                    this.runningToken.Token));\n+\n+            // State\n+            try\n+            {\n+                this.devicesStateThread.Start();\n+            }\n+            catch (Exception e)\n+            {\n+                this.log.Error(\"Unable to start the device-state thread\", e);\n+                throw new Exception(\"Unable to start the device-state thread\", e);\n+            }\n \n-            // Send heartbeat every 24 hours\n-            if (duration.Days >= DIAGNOSTICS_POLLING_FREQUENCY_DAYS)\n+            // Connection\n+            try\n             {\n-                this.lastPolledTime = now;\n-                this.logDiagnostics.LogServiceHeartbeat();\n+                this.devicesConnectionThread.Start();\n+            }\n+            catch (Exception e)\n+            {\n+                this.log.Error(\"Unable to start the device-connection thread\", e);\n+                throw new Exception(\"Unable to start the device-connection thread\", e);","path":"SimulationAgent/Agent.cs","position":465,"original_position":466,"commit_id":"c190a7a5e3cec00e0e90b50d3d1b2bde6b9de47c","original_commit_id":"da9f9e8e59875fe1df6379fe82cd746d0692bbd7","user":{"login":"peterfelts","id":5828480,"node_id":"MDQ6VXNlcjU4Mjg0ODA=","avatar_url":"https://avatars0.githubusercontent.com/u/5828480?v=4","gravatar_id":"","url":"https://api.github.com/users/peterfelts","html_url":"https://github.com/peterfelts","followers_url":"https://api.github.com/users/peterfelts/followers","following_url":"https://api.github.com/users/peterfelts/following{/other_user}","gists_url":"https://api.github.com/users/peterfelts/gists{/gist_id}","starred_url":"https://api.github.com/users/peterfelts/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/peterfelts/subscriptions","organizations_url":"https://api.github.com/users/peterfelts/orgs","repos_url":"https://api.github.com/users/peterfelts/repos","events_url":"https://api.github.com/users/peterfelts/events{/privacy}","received_events_url":"https://api.github.com/users/peterfelts/received_events","type":"User","site_admin":false},"body":"Fixed. ","created_at":"2018-10-15T21:39:48Z","updated_at":"2018-10-15T21:39:48Z","html_url":"https://github.com/Azure/device-simulation-dotnet/pull/270#discussion_r225325944","pull_request_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/pulls/270","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/Azure/device-simulation-dotnet/pulls/comments/225325944"},"html":{"href":"https://github.com/Azure/device-simulation-dotnet/pull/270#discussion_r225325944"},"pull_request":{"href":"https://api.github.com/repos/Azure/device-simulation-dotnet/pulls/270"}},"in_reply_to_id":224247403},"pull_request":{"url":"https://api.github.com/repos/Azure/device-simulation-dotnet/pulls/270","id":220815468,"node_id":"MDExOlB1bGxSZXF1ZXN0MjIwODE1NDY4","html_url":"https://github.com/Azure/device-simulation-dotnet/pull/270","diff_url":"https://github.com/Azure/device-simulation-dotnet/pull/270.diff","patch_url":"https://github.com/Azure/device-simulation-dotnet/pull/270.patch","issue_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/issues/270","number":270,"state":"open","locked":false,"title":"Integrate SimulationManager","user":{"login":"peterfelts","id":5828480,"node_id":"MDQ6VXNlcjU4Mjg0ODA=","avatar_url":"https://avatars0.githubusercontent.com/u/5828480?v=4","gravatar_id":"","url":"https://api.github.com/users/peterfelts","html_url":"https://github.com/peterfelts","followers_url":"https://api.github.com/users/peterfelts/followers","following_url":"https://api.github.com/users/peterfelts/following{/other_user}","gists_url":"https://api.github.com/users/peterfelts/gists{/gist_id}","starred_url":"https://api.github.com/users/peterfelts/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/peterfelts/subscriptions","organizations_url":"https://api.github.com/users/peterfelts/orgs","repos_url":"https://api.github.com/users/peterfelts/repos","events_url":"https://api.github.com/users/peterfelts/events{/privacy}","received_events_url":"https://api.github.com/users/peterfelts/received_events","type":"User","site_admin":false},"body":"# Description and Motivation <!-- Info & Context so we can review your pull request -->\r\nThis change will integrate the SimulationManager and significantly refactor the SimulationAgent. The SimulationManager will be responsible for, you guessed it, managing individual simulations. There will be one SimulationManager per simulation. With this change, distributed simulations should work. \r\n\r\nI recommend starting reviewing this PR by looking at the simulation agent (SimulationAgent/Agent.cs). The agent has been significantly refactored though, so perhaps you should review it as a completely new file.\r\n\r\n# Change type <!-- [x] in all the boxes that apply -->\r\n\r\n- [ ] Bug fix\r\n- [x] New feature\r\n- [ ] Enhancement\r\n- [ ] Breaking change (breaks backward compatibility)\r\n\r\n**Checklist:**\r\n\r\n- [x] All tests passed\r\n- [x] The code follows the code style and conventions of this project\r\n- [x] The change requires a change to the documentation\r\n- [x] I have updated the documentation accordingly\n\n<!-- Reviewable:start -->\n---\nThis change is [<img src=\"https://reviewable.io/review_button.svg\" height=\"34\" align=\"absmiddle\" alt=\"Reviewable\"/>](https://reviewable.io/reviews/azure/device-simulation-dotnet/270)\n<!-- Reviewable:end -->\n","created_at":"2018-10-05T20:27:00Z","updated_at":"2018-10-15T21:39:48Z","closed_at":null,"merged_at":null,"merge_commit_sha":"30eec0676360c72476851544d2b4a408beeb1a78","assignee":null,"assignees":[],"requested_reviewers":[{"login":"timlaverty","id":5084013,"node_id":"MDQ6VXNlcjUwODQwMTM=","avatar_url":"https://avatars2.githubusercontent.com/u/5084013?v=4","gravatar_id":"","url":"https://api.github.com/users/timlaverty","html_url":"https://github.com/timlaverty","followers_url":"https://api.github.com/users/timlaverty/followers","following_url":"https://api.github.com/users/timlaverty/following{/other_user}","gists_url":"https://api.github.com/users/timlaverty/gists{/gist_id}","starred_url":"https://api.github.com/users/timlaverty/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/timlaverty/subscriptions","organizations_url":"https://api.github.com/users/timlaverty/orgs","repos_url":"https://api.github.com/users/timlaverty/repos","events_url":"https://api.github.com/users/timlaverty/events{/privacy}","received_events_url":"https://api.github.com/users/timlaverty/received_events","type":"User","site_admin":false},{"login":"avanikp","id":32693971,"node_id":"MDQ6VXNlcjMyNjkzOTcx","avatar_url":"https://avatars1.githubusercontent.com/u/32693971?v=4","gravatar_id":"","url":"https://api.github.com/users/avanikp","html_url":"https://github.com/avanikp","followers_url":"https://api.github.com/users/avanikp/followers","following_url":"https://api.github.com/users/avanikp/following{/other_user}","gists_url":"https://api.github.com/users/avanikp/gists{/gist_id}","starred_url":"https://api.github.com/users/avanikp/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/avanikp/subscriptions","organizations_url":"https://api.github.com/users/avanikp/orgs","repos_url":"https://api.github.com/users/avanikp/repos","events_url":"https://api.github.com/users/avanikp/events{/privacy}","received_events_url":"https://api.github.com/users/avanikp/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/pulls/270/commits","review_comments_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/pulls/270/comments","review_comment_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/pulls/comments{/number}","comments_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/issues/270/comments","statuses_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/statuses/c190a7a5e3cec00e0e90b50d3d1b2bde6b9de47c","head":{"label":"Azure:pfelts/integrate-simulation-manager","ref":"pfelts/integrate-simulation-manager","sha":"c190a7a5e3cec00e0e90b50d3d1b2bde6b9de47c","user":{"login":"Azure","id":6844498,"node_id":"MDEyOk9yZ2FuaXphdGlvbjY4NDQ0OTg=","avatar_url":"https://avatars0.githubusercontent.com/u/6844498?v=4","gravatar_id":"","url":"https://api.github.com/users/Azure","html_url":"https://github.com/Azure","followers_url":"https://api.github.com/users/Azure/followers","following_url":"https://api.github.com/users/Azure/following{/other_user}","gists_url":"https://api.github.com/users/Azure/gists{/gist_id}","starred_url":"https://api.github.com/users/Azure/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Azure/subscriptions","organizations_url":"https://api.github.com/users/Azure/orgs","repos_url":"https://api.github.com/users/Azure/repos","events_url":"https://api.github.com/users/Azure/events{/privacy}","received_events_url":"https://api.github.com/users/Azure/received_events","type":"Organization","site_admin":false},"repo":{"id":89741887,"node_id":"MDEwOlJlcG9zaXRvcnk4OTc0MTg4Nw==","name":"device-simulation-dotnet","full_name":"Azure/device-simulation-dotnet","private":false,"owner":{"login":"Azure","id":6844498,"node_id":"MDEyOk9yZ2FuaXphdGlvbjY4NDQ0OTg=","avatar_url":"https://avatars0.githubusercontent.com/u/6844498?v=4","gravatar_id":"","url":"https://api.github.com/users/Azure","html_url":"https://github.com/Azure","followers_url":"https://api.github.com/users/Azure/followers","following_url":"https://api.github.com/users/Azure/following{/other_user}","gists_url":"https://api.github.com/users/Azure/gists{/gist_id}","starred_url":"https://api.github.com/users/Azure/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Azure/subscriptions","organizations_url":"https://api.github.com/users/Azure/orgs","repos_url":"https://api.github.com/users/Azure/repos","events_url":"https://api.github.com/users/Azure/events{/privacy}","received_events_url":"https://api.github.com/users/Azure/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/Azure/device-simulation-dotnet","description":"IoT Simulation service","fork":false,"url":"https://api.github.com/repos/Azure/device-simulation-dotnet","forks_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/forks","keys_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/teams","hooks_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/hooks","issue_events_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/issues/events{/number}","events_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/events","assignees_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/assignees{/user}","branches_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/branches{/branch}","tags_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/tags","blobs_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/git/refs{/sha}","trees_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/statuses/{sha}","languages_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/languages","stargazers_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/stargazers","contributors_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/contributors","subscribers_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/subscribers","subscription_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/subscription","commits_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/commits{/sha}","git_commits_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/git/commits{/sha}","comments_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/comments{/number}","issue_comment_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/issues/comments{/number}","contents_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/contents/{+path}","compare_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/merges","archive_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/downloads","issues_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/issues{/number}","pulls_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/pulls{/number}","milestones_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/milestones{/number}","notifications_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/labels{/name}","releases_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/releases{/id}","deployments_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/deployments","created_at":"2017-04-28T20:20:08Z","updated_at":"2018-10-12T23:49:32Z","pushed_at":"2018-10-15T18:30:31Z","git_url":"git://github.com/Azure/device-simulation-dotnet.git","ssh_url":"git@github.com:Azure/device-simulation-dotnet.git","clone_url":"https://github.com/Azure/device-simulation-dotnet.git","svn_url":"https://github.com/Azure/device-simulation-dotnet","homepage":"","size":2177,"stargazers_count":32,"watchers_count":32,"language":"C#","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":40,"mirror_url":null,"archived":false,"open_issues_count":80,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":40,"open_issues":80,"watchers":32,"default_branch":"master"}},"base":{"label":"Azure:master","ref":"master","sha":"e4f1c79e694cfbc72a85eecc8ee5dafcc00c5aca","user":{"login":"Azure","id":6844498,"node_id":"MDEyOk9yZ2FuaXphdGlvbjY4NDQ0OTg=","avatar_url":"https://avatars0.githubusercontent.com/u/6844498?v=4","gravatar_id":"","url":"https://api.github.com/users/Azure","html_url":"https://github.com/Azure","followers_url":"https://api.github.com/users/Azure/followers","following_url":"https://api.github.com/users/Azure/following{/other_user}","gists_url":"https://api.github.com/users/Azure/gists{/gist_id}","starred_url":"https://api.github.com/users/Azure/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Azure/subscriptions","organizations_url":"https://api.github.com/users/Azure/orgs","repos_url":"https://api.github.com/users/Azure/repos","events_url":"https://api.github.com/users/Azure/events{/privacy}","received_events_url":"https://api.github.com/users/Azure/received_events","type":"Organization","site_admin":false},"repo":{"id":89741887,"node_id":"MDEwOlJlcG9zaXRvcnk4OTc0MTg4Nw==","name":"device-simulation-dotnet","full_name":"Azure/device-simulation-dotnet","private":false,"owner":{"login":"Azure","id":6844498,"node_id":"MDEyOk9yZ2FuaXphdGlvbjY4NDQ0OTg=","avatar_url":"https://avatars0.githubusercontent.com/u/6844498?v=4","gravatar_id":"","url":"https://api.github.com/users/Azure","html_url":"https://github.com/Azure","followers_url":"https://api.github.com/users/Azure/followers","following_url":"https://api.github.com/users/Azure/following{/other_user}","gists_url":"https://api.github.com/users/Azure/gists{/gist_id}","starred_url":"https://api.github.com/users/Azure/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Azure/subscriptions","organizations_url":"https://api.github.com/users/Azure/orgs","repos_url":"https://api.github.com/users/Azure/repos","events_url":"https://api.github.com/users/Azure/events{/privacy}","received_events_url":"https://api.github.com/users/Azure/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/Azure/device-simulation-dotnet","description":"IoT Simulation service","fork":false,"url":"https://api.github.com/repos/Azure/device-simulation-dotnet","forks_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/forks","keys_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/teams","hooks_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/hooks","issue_events_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/issues/events{/number}","events_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/events","assignees_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/assignees{/user}","branches_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/branches{/branch}","tags_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/tags","blobs_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/git/refs{/sha}","trees_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/statuses/{sha}","languages_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/languages","stargazers_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/stargazers","contributors_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/contributors","subscribers_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/subscribers","subscription_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/subscription","commits_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/commits{/sha}","git_commits_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/git/commits{/sha}","comments_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/comments{/number}","issue_comment_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/issues/comments{/number}","contents_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/contents/{+path}","compare_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/merges","archive_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/downloads","issues_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/issues{/number}","pulls_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/pulls{/number}","milestones_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/milestones{/number}","notifications_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/labels{/name}","releases_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/releases{/id}","deployments_url":"https://api.github.com/repos/Azure/device-simulation-dotnet/deployments","created_at":"2017-04-28T20:20:08Z","updated_at":"2018-10-12T23:49:32Z","pushed_at":"2018-10-15T18:30:31Z","git_url":"git://github.com/Azure/device-simulation-dotnet.git","ssh_url":"git@github.com:Azure/device-simulation-dotnet.git","clone_url":"https://github.com/Azure/device-simulation-dotnet.git","svn_url":"https://github.com/Azure/device-simulation-dotnet","homepage":"","size":2177,"stargazers_count":32,"watchers_count":32,"language":"C#","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":40,"mirror_url":null,"archived":false,"open_issues_count":80,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":40,"open_issues":80,"watchers":32,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/Azure/device-simulation-dotnet/pulls/270"},"html":{"href":"https://github.com/Azure/device-simulation-dotnet/pull/270"},"issue":{"href":"https://api.github.com/repos/Azure/device-simulation-dotnet/issues/270"},"comments":{"href":"https://api.github.com/repos/Azure/device-simulation-dotnet/issues/270/comments"},"review_comments":{"href":"https://api.github.com/repos/Azure/device-simulation-dotnet/pulls/270/comments"},"review_comment":{"href":"https://api.github.com/repos/Azure/device-simulation-dotnet/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/Azure/device-simulation-dotnet/pulls/270/commits"},"statuses":{"href":"https://api.github.com/repos/Azure/device-simulation-dotnet/statuses/c190a7a5e3cec00e0e90b50d3d1b2bde6b9de47c"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"peterfelts"}}