I have an azure function app that I call from a slack slash command.
Sometimes the function takes a little while to return the data requested, so I made that function return a "Calculating..." message to slack immediately, and run the actual processing on a Task.Run (the request contains a webhook that I post back to when I finally get the data) :
Task.Run(() => opsData.GenerateQuoteCheckMessage(incomingData, context.FunctionAppDirectory, log));
This works mostly fine, except every now and then when people are calling the function from slack, it will return the data twice. So it will show one "Calculating..." message and then 2 results returned from the above function.
BTW, Azure functions start with :
public static async Task
Thanks!
UPDATE : here is the code for the function:
[FunctionName("QuoteCheck")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous)]HttpRequestMessage req, TraceWriter log, ExecutionContext context)
{
var opsHelper = new OpsHelper();
string bodyContent = await req.Content.ReadAsStringAsync();
var parsedBody = HttpUtility.ParseQueryString(bodyContent);
var commandName = parsedBody["command"];
var incomingBrandId = parsedBody["text"];
int.TryParse(incomingBrandId, out var brandId);
var responseUrl = parsedBody["response_url"];
var incomingData = new IncomingSlackRequestModel
{
UserName = parsedBody["user_name"],
ChannelName = parsedBody["channel_name"],
CommandName = commandName,
ResponseUri = new Uri(responseUrl),
BrandId = brandId
};
var opsData = OpsDataFactory.GetOpsData(context.FunctionAppDirectory, environment);
Task.Run(() => opsData.GenerateQuoteCheckMessage(incomingData, context.FunctionAppDirectory, log));
// Generate a "Calculating" response message based on the correct parameters being passed
var calculatingMessage = opsHelper.GenerateCalculatingMessage(incomingData);
// Return calculating message
return req.CreateResponse(HttpStatusCode.OK, calculatingMessage, JsonMediaTypeFormatter.DefaultMediaType);
}
}
And then the GenerateQuoteCheckMessage calculates some data and eventually posts back to slack (Using Rest Sharp) :
var client = new RestClient(responseUri);
var request = new RestRequest(Method.POST);
request.AddParameter("application/json; charset=utf-8", JsonConvert.SerializeObject(outgoingMessage), ParameterType.RequestBody);
client.Execute(request);
Using Kzrystof's suggestion, I added a service bus call in the function that posts to a queue, and added another function that reads off that queue and processes the request, responding to the webhook that slack gives me :
public void DeferProcessingToServiceBus(IncomingSlackRequestModel incomingSlackRequestModel)
{
var serializedModel = JsonConvert.SerializeObject(incomingSlackRequestModel);
var sbConnectionString = ConfigurationManager.AppSettings.Get("SERVICE_BUS_CONNECTION_STRING");
var sbQueueName = ConfigurationManager.AppSettings.Get("OpsNotificationsQueueName");
var client = QueueClient.CreateFromConnectionString(sbConnectionString, sbQueueName);
var brokeredMessage = new BrokeredMessage(serializedModel);
client.Send(brokeredMessage);
}
Related
I'm trying to get a response from a web api(C#), but, in a specific method, the end point address is pointed to the port the front end is running on, but in the others methods it's working as well.
Future<void> onInit() async {
httpClient.baseUrl = await baseUrlGetter();
httpClient.addRequestModifier((Request request) {
request.headers['Accept'] = 'application/json';
request.headers['Content-Type'] = 'application/json';
request.headers['origemAcesso'] = 't2painel';
return request;
});
httpClient.addAuthenticator((Request request) {
var token = _storageService.token;
var headers = {'Authorization': "Bearer $token"};
request.headers.addAll(headers);
return request;
});
super.onInit();
}
Ahead, it'ts the onInit method in API class, the function baseUrlGetter is working, returnig the rigth url of the API.
But, in the method below:
Future<ImageModel> fetchDistribuidorImage() async {
var response = _errorHandler(await get('/api/Imagens/DistribuidorImagem'));
var data = ImageModel.fromJson(response.body);
return data;
}
Take a look to this images (read the images descriptions)
Only in the method "fetchDistribuidorImage" is not correct.
I'm getting this weird issue when using Durable Azure functions to submit messages to azure service bus.
My code is a simple Fan-Out implementation
REST trigger get the number of messages to be submitted and hands that an orchestrator.
Orchestrator stores the calls activity which will create and submit the message to Service bus.
The issue is when I send the REST parameter asking to add 3000 messages, more than 3000 get added.
Worse, it's not the same number either - 3104, 3100, 3286 anything...
See code below:
[FunctionName("Function1_HttpStart")]
//public static async Task<HttpResponseMessage> HttpStart(
public static async Task<IActionResult> HttpStart(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req,
[DurableClient] IDurableOrchestrationClient starter,
ILogger log)
{
String type = req.Query["type"];
if(!long.TryParse(req.Query["count"], out var count))
{
return new ObjectResult($"Parse failed for parameter 'count' ({req.Query["count"]}) to Int.") { StatusCode = 400};
}
var restInputs = new RestInputs()
{ Type = type, Count = count };
// Function input comes from the request content.
string instanceId = await starter.StartNewAsync
("EmailQueueSubmitter_OrchestratorSingleton"
, restInputs);
log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
return starter.CreateCheckStatusResponse(req, instanceId);
}
[FunctionName("EmailQueueSubmitter_OrchestratorSingleton")]
public static async Task<List<string>> EmailQueueSubmitter_OrchestratorSingleton(
[OrchestrationTrigger] IDurableOrchestrationContext context, ILogger log)
{
var outputs = new List<string>();
try
{
var restInputs = context.GetInput<RestInputs>();
var parallelTasks = new List<Task>();
long runBatchLen;
long i_batch, i_iter, batchCount = 0;
for (i_batch = 0; i_batch < restInputs.Count; i_batch++)
{
parallelTasks.Add(context.CallActivityAsync("EmailQueueSubmitter_ActivitySendMessageBatchSingleton", i_batch.ToString()));
log.LogWarning($"Message {i_batch} Added");
}
log.LogWarning($"Awaiting {parallelTasks.Count} tasks");
await Task.WhenAll(parallelTasks);
var doneTaskCount = parallelTasks.Where(t => t.IsCompleted).ToList().Count;
var successTaskCount = parallelTasks.Where(t => t.IsCompletedSuccessfully).ToList().Count;
var faultedTaskCount = parallelTasks.Where(t => t.IsFaulted).ToList().Count;
var exceptionTaskCount = parallelTasks.Where(t => t.Exception != null).ToList().Count;
log.LogWarning($"Done:{doneTaskCount}, Success: {successTaskCount}, Fault:{faultedTaskCount}, Exception:{exceptionTaskCount}");
log.LogWarning($"Achieved completion.");
}
catch (Exception ex)
{
log.LogError(ex.Message);
throw new InvalidOperationException(ex.Message);
}
return outputs;
}
[FunctionName("EmailQueueSubmitter_ActivitySendMessageBatchSingleton")]
public static async Task EmailQueueSubmitter_ActivitySendMessageBatchSingleton([ActivityTrigger] IDurableActivityContext activityContext, ILogger log)
{
log.LogWarning($"Starting Activity.");
var payload = activityContext.GetInput<String>();
await ServiceBus_Sender.SendMessageBatch(payload);
log.LogWarning($"Finished Activity.");
}
public static ServiceBusMessage CreateMessage(String Payload)
{
try
{
var sbMsg = new ServiceBusMessage(Payload)
{
MessageId = Guid.NewGuid().ToString(),
ContentType = "text/plain"
};
//sbMsg.ApplicationProperties.Add("RequestType", "Publish");
return sbMsg;
}
catch (Exception ex)
{
throw new InvalidOperationException(ex.Message, ex);
}
}
Thanks #Camilo Terevinto for the information, I am converting this to an answer so that may help other community members:
As suggested in the comments, to run a duplicate check you could generate a Guid and send it together with the data, and then check that the Guid wasn't handled already before. Hopefully this resolved your issue.
OP Edit: Duplicates check was enabled by changing the service bus queue to be session enabled and have de-duplication turned on. The submitted messages' MessageId was set to be unique in each session. This is the only way I can think of to deal with the at-least-once guarantees...
we are currently developing some automation with the botframework.
At some point in the conversation, we sent some data through a service bus for processing and wait for a response and then want to continue with the conversation. We already implemented the part where we wait for an response entry in the service bus subscription and then we want to send an Activity from type Event to the bot.
We did the same steps with the proactive message as described in other posts.
We are able to recreate the botclient and conversation reference and all, but in the end when we send the activity, we always send it to the user and not to the bot. But this doesn't trigger the "EventActivityPrompt".
The only way where we achieved the desired outcome was when we made a post to api/messages, but this is too complicated for our taste, and we are looking for an easier way over the botClient (or similar technology)
Has anyone some good ideas? :)
ServiceBusReceiver Message Processing:
private static async Task ProcessMessagesAsync(Message message, CancellationToken token)
{
// Process the message.
Console.WriteLine($"Received message: SequenceNumber:{message.SystemProperties.SequenceNumber} Body:{Encoding.UTF8.GetString(message.Body)}");
_logger?.LogInformation("Received message '{id}' with label '{label}' from queue.", message.MessageId, message.Label);
var data = JsonSerializer.Deserialize<BotCarLicensingOrderRpaRequest>(message.Body);
data.AdditionalData.TryGetValue("ServiceUrl", out var serviceUrl);
data.AdditionalData.TryGetValue("ChannelId", out var channelId);
data.AdditionalData.TryGetValue("BotId", out var botId);
data.AdditionalData.TryGetValue("UserId", out var userId);
data.AdditionalData.TryGetValue("ReplyToId", out var replyToId);
var conversationReference = _offTurnConversationService.CreateSyntheticConversationReference(
channelId?.ToString(),
data.ConversationId,
serviceUrl?.ToString());
conversationReference.User = new ChannelAccount()
{
Id = userId?.ToString(),
Role = "user"
};
conversationReference.Bot = new ChannelAccount
{
Id = botId?.ToString(),
Role = "bot"
};
var activity = (Activity)Activity.CreateEventActivity();
activity.Text = "success";
activity.ChannelId = channelId?.ToString();
activity.ServiceUrl = serviceUrl?.ToString();
activity.RelatesTo = conversationReference;
activity.Conversation = new ConversationAccount
{
Id = data.ConversationId
};
activity.ReplyToId = replyToId?.ToString();
activity.ApplyConversationReference(conversationReference, true);
// Complete the message so that it is not received again.
// This can be done only if the subscriptionClient is created in ReceiveMode.PeekLock mode (which is the default).
await _messageReceiver.CompleteAsync(message.SystemProperties.LockToken);
// This "works" but is complicated, as we have to set up a whole HTTP call
await _offTurnConversationService.SendActivityToBotAsync(activity);
// This just sends the Event to the user, no matter how I set up the conversation
// reference regarding From/Recipient
// And it doesn't help in continuing the conversation
await _offTurnConversationService.SendToConversationThroughPipelineAsync(
async (turnContext, cancellationToken) =>
{
await turnContext.SendActivityAsync(activity, cancellationToken: cancellationToken);
},
conversationReference);
// Note: Use the cancellationToken passed as necessary to determine if the subscriptionClient has already been closed.
// If subscriptionClient has already been closed, you can choose to not call CompleteAsync() or AbandonAsync() etc.
// to avoid unnecessary exceptions.
}
OffTurnConversationService:
public ConversationReference CreateSyntheticConversationReference(string channelId, string conversationId, string serviceUrl)
{
ArgumentGuard.NotNull(channelId, nameof(channelId));
ArgumentGuard.NotNull(conversationId, nameof(conversationId));
ArgumentGuard.NotNull(serviceUrl, nameof(serviceUrl));
if (string.IsNullOrEmpty(_botOptions.CurrentValue.BotId))
{
throw new InvalidOperationException("A valid bot id must be configured in your bot options in order to create a synthetic conversation reference.");
}
// WARNING: This implementation works for directline and webchat.
// Changes could be necessary for other channels.
var supportedChannels = new List<string>()
{
Channels.Directline,
Channels.Webchat
};
if (supportedChannels.Any(c => c.Equals(channelId, StringComparison.OrdinalIgnoreCase)))
{
_logger.LogWarning(
"The synthetic conversation reference created for channel {UsedChannel} might not work properly, " +
"because it's not supported and tested. Supported channels are {SupportedChannel}.",
channelId,
string.Join(",", supportedChannels));
}
var conversationReference = new ConversationReference()
{
Conversation = new ConversationAccount()
{
Id = conversationId
},
Bot = new ChannelAccount()
{
Id = _botOptions.CurrentValue.BotId,
Name = _botOptions.CurrentValue.BotId
},
ChannelId = channelId,
ServiceUrl = serviceUrl
};
return conversationReference;
}
public virtual async Task SendActivityToBotAsync(IActivity activity)
{
// Create the new request to POST to the client
var forwardRequest = new HttpRequestMessage()
{
RequestUri = new Uri(_botOptions.CurrentValue.ReplyServiceUrl),
Method = HttpMethod.Post,
};
// Change the host for the request to be the forwarding URL.
forwardRequest.Headers.Host = forwardRequest.RequestUri.Host;
// If the child bot is not running on local mode (no app-id/password),
// we're going send an authentication header.
OAuthResponse authToken = await GetTokenAsync(_botOptions.CurrentValue.MicrosoftAppId, _botOptions.CurrentValue.MicrosoftAppPassword);
forwardRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken.AccessToken);
// Altered activity to JSON content
var json = JsonConvert.SerializeObject(activity);
var content = new StringContent(json, Encoding.UTF8, "application/json");
forwardRequest.Content = content;
using var client = new HttpClient();
var response = await client.SendAsync(forwardRequest);
if (!response.IsSuccessStatusCode)
{
string message = $"Failed to send activity '{activity.Id}' to client bot. {response.ReasonPhrase}";
throw new Exception(message);
}
}
public virtual async Task SendToConversationThroughPipelineAsync(
BotCallbackHandler callback,
ConversationReference conversationReference)
{
ArgumentGuard.NotNull(callback, nameof(callback));
ArgumentGuard.NotNull(conversationReference, nameof(conversationReference));
// Avoiding 401 "Unauthorized" errors
TrustServiceUrl(conversationReference.ServiceUrl);
// Reuse adapter with its pipeline to send responses back to the user (like pro-active messages)
await ((BotAdapter)_botFrameworkHttpAdapter).ContinueConversationAsync(
_botOptions.CurrentValue.MicrosoftAppId,
conversationReference,
callback,
default);
}
There is one application, the API with access to the database and one application that calls the API with RestSharp.
I implemented all async methods of RestSharp to work generic. So GET, POST, DELETE are all working.The only one I can't get to work is the PUT.
First of all this is my controllers PUT:
[HttpPut("{id}")]
public void Put(int id, [FromBody]ApplicationUser value)
{
string p = value.Email;
}
this is my method:
public Task<bool> PutRequestContentAsync<T>(string resource, object id, T resourceObject) where T : new()
{
RestClient client = new RestClient("http://localhost:54008/api/");
RestRequest request = new RestRequest($"{resource}/{{id}}", Method.PUT);
request.AddUrlSegment("id", id);
request.AddObject(resourceObject);
var tcs = new TaskCompletionSource<bool>();
var asyncHandler = client.ExecuteAsync<T>(request, r =>
{
tcs.SetResult(r.ResponseStatus == ResponseStatus.Completed);
});
return tcs.Task;
}
and this is my call in a view (all other calls of GET,... are working fine):
bool putOk = await new RepositoryCall()
.PutRequestContentAsync("Values", 2,
new ApplicationUser {
Email="test#xxxxxxx.de"
}
);
with debugging, the response-status is Completed but the PUT is never called.
Any idea what the problem could be?
So finally I got my answer myself... (sit yesterday 6 hours and no result, today one more hour and it works)
public Task<bool> PutRequestContentAsync<T>(string resource, object id, T resourceObject) where T : new()
{
RestClient client = new RestClient("http://localhost:54008/api/");
RestRequest request = new RestRequest($"{resource}/{{id}}", Method.PUT);
request.AddUrlSegment("id", id);
request.RequestFormat = DataFormat.Json;
request.AddBody(resourceObject);
var tcs = new TaskCompletionSource<bool>();
var asyncHandler = client.ExecuteAsync<T>(request, (response) => {
tcs.SetResult(response.ResponseStatus == ResponseStatus.Completed);
});
return tcs.Task;
}
the trick was to add a RequestFormat and changing AddObject to AddBody :)
I Have a web api that internally makes multiple web api calls and retuns one result at the end. These multiple methods take different time to execute and as a result I'm getting timeout exception in my function.
For ex:
DeleteAccount is my web api method and it calls another web api method DeleteExistingAccount. This DeleteExistingAccount has multiple method calls say method1, method2, method3, method4. As these are executing asynchronously I'm getting Timeout error in my DeleteAccount method.
How can I achieve the method1, method2, method3, method4 to execute synchronously one after the other.
/////This is my web api method
public IHttpActionResult DeleteAccount([FromUri] string acctId, [FromUri] string email = "")
{
if (PicUpHelper.IsValidUserRequest())
{
string baseUrl = System.Configuration.ConfigurationManager.AppSettings["APIURL"].ToString();
string url = string.Format("/api/deleteaccount/{0}", acctId);
var clientRequest = new RestClient(baseUrl);
var request = new RestRequest(url, Method.DELETE);
IRestResponse response = clientRequest.Execute(request);
//// When I look at the response I have the timeout here
var content = response.Content; // raw content
dynamic data = JsonConvert.DeserializeObject(content);
}
}
///My web api method will invoke this web api method
public WebApiResultEx<string, string> DeleteAccount([FromUri] string acctId, [FromUri] string email = "")
{
WebApiResultEx<string, string> oDelAcctRes = new WebApiResultEx<string, string>();
Account oAccountInfo = new Account();
MyDbContext DbCtxt = new MyDbContext();
AccountAPIController oAccountAPIController = new AccountAPIController();
var oAcctInfo = oPlatAccountAPIController.Get(acctId);
if (!oAcctInfo.HasError > 0)
{
oAcctInfo = (Models.Api.Account)(object)oAcctInfo.Data;
//second method
oRes = oAccountAPIController.DeleteAcct(acctId);
}
return oRes;
}
Thanks
Tarak