/// <summary> /// This is the main entry point for your service replica. /// This method executes when this replica of your service becomes primary and has write status. /// </summary> /// <param name="cancellationToken">Canceled when Service Fabric needs to shut down this service replica.</param> protected override async Task RunAsync(CancellationToken cancellationToken) { // Get the IoT Hub connection string from the Settings.xml config file // from a configuration package named "Config" string iotHubConnectionString = this.Context.CodePackageActivationContext .GetConfigurationPackageObject("Config") .Settings .Sections["IoTHubConfigInformation"] .Parameters["ConnectionString"] .Value; string iotHubProcessOnlyFutureEvents = this.Context.CodePackageActivationContext .GetConfigurationPackageObject("Config") .Settings .Sections["IoTHubConfigInformation"] .Parameters["ProcessOnlyFutureEvents"] .Value.ToLower(); ServiceEventSource.Current.ServiceMessage(this.Context, $"RouterService - {ServiceUniqueId} - RunAsync - Starting service - Process Only Future Events[{iotHubProcessOnlyFutureEvents}] - IoTHub Connection String[{iotHubConnectionString}]"); // These Reliable Dictionaries are used to keep track of our position in IoT Hub. // If this service fails over, this will allow it to pick up where it left off in the event stream. IReliableDictionary <string, string> offsetDictionary = await this.StateManager.GetOrAddAsync <IReliableDictionary <string, string> >(OffsetDictionaryName); IReliableDictionary <string, long> epochDictionary = await this.StateManager.GetOrAddAsync <IReliableDictionary <string, long> >(EpochDictionaryName); // Each partition of this service corresponds to a partition in IoT Hub. // IoT Hub partitions are numbered 0..n-1, up to n = 32. // This service needs to use an identical partitioning scheme. // The low key of every partition corresponds to an IoT Hub partition. Int64RangePartitionInformation partitionInfo = (Int64RangePartitionInformation)this.Partition.PartitionInfo; long servicePartitionKey = partitionInfo.LowKey; EventHubReceiver eventHubReceiver = null; MessagingFactory messagingFactory = null; try { // HttpClient is designed as a shared object. // A single instance should be used throughout the lifetime of RunAsync. using (HttpClient httpClient = new HttpClient(new HttpServiceClientHandler())) { int offsetIteration = 0; bool IsConnected = false; while (true) { cancellationToken.ThrowIfCancellationRequested(); if (!IsConnected) { // Get an EventHubReceiver and the MessagingFactory used to create it. // The EventHubReceiver is used to get events from IoT Hub. // The MessagingFactory is just saved for later so it can be closed before RunAsync exits. Tuple <EventHubReceiver, MessagingFactory> iotHubInfo = await this.ConnectToIoTHubAsync(iotHubConnectionString, servicePartitionKey, epochDictionary, offsetDictionary, iotHubProcessOnlyFutureEvents); eventHubReceiver = iotHubInfo.Item1; messagingFactory = iotHubInfo.Item2; IsConnected = true; } Uri postUrl = null; try { // It's important to set a low wait time here in lieu of a cancellation token // so that this doesn't block RunAsync from exiting when Service Fabric needs it to complete. // ReceiveAsync is a long-poll operation, so the timeout should not be too low, // yet not too high to block RunAsync from exiting within a few seconds. using (EventData eventData = await eventHubReceiver.ReceiveAsync(TimeSpan.FromSeconds(5))) { if (eventData == null) { ServiceEventSource.Current.ServiceMessage(this.Context, $"RouterService - {ServiceUniqueId} - RunAsync - No event data available on hub '{eventHubReceiver.Name}'"); await Task.Delay(global::Iot.Common.Names.IoTHubRetryWaitIntervalsInMills); continue; } else { ServiceEventSource.Current.ServiceMessage(this.Context, $"RouterService - {ServiceUniqueId} - RunAsync - Received event data from hub '{eventHubReceiver.Name}' - Enqueued Time[{eventData.EnqueuedTimeUtc}] - Partition '{eventData.PartitionKey}' Sequence # '{eventData.SequenceNumber}'"); } string targetSite = (string)eventData.Properties[global::Iot.Common.Names.EventKeyFieldTargetSite]; string deviceId = (string)eventData.Properties[global::Iot.Common.Names.EventKeyFieldDeviceId]; // This is the named service instance of the target site data service that the event should be sent to. // The targetSite id is part of the named service instance name. // The incoming device data stream specifie which target site the data belongs to. string prefix = global::Iot.Common.Names.InsightApplicationNamePrefix; string serviceName = global::Iot.Common.Names.InsightDataServiceName; Uri targetSiteServiceName = new Uri($"{prefix}/{targetSite}/{serviceName}"); long targetSiteServicePartitionKey = FnvHash.Hash(deviceId); ServiceEventSource.Current.ServiceMessage(this.Context, $"RouterService - {ServiceUniqueId} - RunAsync - About to post data to Insight Data Service from device '{deviceId}' to target site '{targetSite}' - partitionKey '{targetSiteServicePartitionKey}' - Target Service Name '{targetSiteServiceName}'"); // The target site data service exposes an HTTP API. // For incoming device events, the URL is /api/events/{deviceId} // This sets up a URL and sends a POST request with the device JSON payload. postUrl = new HttpServiceUriBuilder() .SetServiceName(targetSiteServiceName) .SetPartitionKey(targetSiteServicePartitionKey) .SetServicePathAndQuery($"/api/events/{deviceId}") .Build(); ServiceEventSource.Current.ServiceMessage(this.Context, $"RouterService - {ServiceUniqueId} - RunAsync - Ready to post data to Insight Data Service from device '{deviceId}' to taget site '{targetSite}' - partitionKey '{targetSiteServicePartitionKey}' - Target Service Name '{targetSiteServiceName}' - url '{postUrl.PathAndQuery}'"); // The device stream payload isn't deserialized and buffered in memory here. // Instead, we just can just hook the incoming stream from Iot Hub right into the HTTP request stream. using (Stream eventStream = eventData.GetBodyStream()) { using (StreamContent postContent = new StreamContent(eventStream)) { postContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); HttpResponseMessage response = await httpClient.PostAsync(postUrl, postContent, cancellationToken); if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) { // This service expects the receiving target site service to return HTTP 400 if the device message was malformed. // In this example, the message is simply logged. // Your application should handle all possible error status codes from the receiving service // and treat the message as a "poison" message. // Message processing should be allowed to continue after a poison message is detected. string responseContent = await response.Content.ReadAsStringAsync(); ServiceEventSource.Current.ServiceMessage( this.Context, $"RouterService - {ServiceUniqueId} - RunAsync - Insight service '{targetSiteServiceName}' returned HTTP 400 due to a bad device message from device '{deviceId}'. Error message: '{responseContent}'"); } ServiceEventSource.Current.ServiceMessage( this.Context, $"RouterService - {ServiceUniqueId} - RunAsync - Sent event data to Insight service '{targetSiteServiceName}' with partition key '{targetSiteServicePartitionKey}'. Result: {response.StatusCode.ToString()}"); } } // Save the current Iot Hub data stream offset. // This will allow the service to pick up from its current location if it fails over. // Duplicate device messages may still be sent to the the target site service // if this service fails over after the message is sent but before the offset is saved. if (++offsetIteration % OffsetInterval == 0) { ServiceEventSource.Current.ServiceMessage( this.Context, $"RouterService - {ServiceUniqueId} - RunAsync - Saving offset {eventData.Offset}"); using (ITransaction tx = this.StateManager.CreateTransaction()) { await offsetDictionary.SetAsync(tx, "offset", eventData.Offset); await tx.CommitAsync(); } offsetIteration = 0; } } } catch (Microsoft.ServiceBus.Messaging.ReceiverDisconnectedException rde) { // transient error. Retry. ServiceEventSource.Current.ServiceMessage(this.Context, $"RouterService - {ServiceUniqueId} - RunAsync - Receiver Disconnected Exception in RunAsync: {rde.ToString()}"); IsConnected = false; } catch (TimeoutException te) { // transient error. Retry. ServiceEventSource.Current.ServiceMessage(this.Context, $"RouterService - {ServiceUniqueId} - RunAsync - TimeoutException in RunAsync: {te.ToString()}"); } catch (FabricTransientException fte) { // transient error. Retry. ServiceEventSource.Current.ServiceMessage(this.Context, $"RouterService - {ServiceUniqueId} - RunAsync - FabricTransientException in RunAsync: {fte.ToString()}"); } catch (FabricNotPrimaryException fnpe) { ServiceEventSource.Current.ServiceMessage(this.Context, $"RouterService - {ServiceUniqueId} - RunAsync - FabricNotPrimaryException Exception - Message=[{fnpe}]"); // not primary any more, time to quit. return; } catch (Exception ex) { IsConnected = false; string url = postUrl == null ? "Url undefined" : postUrl.ToString(); //ServiceEventSource.Current.ServiceMessage(this.Context, $"RouterService - {ServiceUniqueId} - RunAsync - General Exception Url=[{url}]- Message=[{ex}] - Inner Exception=[{ex.InnerException.Message ?? "ex.InnerException is null"}] Call Stack=[{ex.StackTrace ?? "ex.StackTrace is null"}] - Stack trace of inner exception=[{ex.InnerException.StackTrace ?? "ex.InnerException.StackTrace is null"}]"); ServiceEventSource.Current.ServiceMessage(this.Context, $"RouterService - {ServiceUniqueId} - RunAsync - General Exception Message[{ex.Message}] for url[{url}]"); } } } } finally { if (messagingFactory != null) { await messagingFactory.CloseAsync(); } } }