soxtoby / SlackNet

A comprehensive Slack API client for .NET
MIT License
208 stars 65 forks source link

Isolated worker model is expecting HttpRequestData instead of HttpRequest on POST requests for the ISlackRequestHandler #181

Closed Tyler-V closed 9 months ago

Tyler-V commented 9 months ago

https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cfunctionsv2&pivots=programming-language-csharp

    [Function("command")]
    public Task<SlackResult> Command([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest request)
    {
        return _requestHandler.HandleSlashCommandRequest(request, _endpointConfig);
    }
[2023-11-22T15:22:26.484Z] Executing 'Functions.command' (Reason='This function was programmatically called via the host APIs.', Id=7edc51c6-a6ac-4005-96cf-b5697e132882)
[2023-11-22T15:22:26.660Z] Function 'command', Invocation id '7edc51c6-a6ac-4005-96cf-b5697e132882': An exception was thrown by the invocation.
[2023-11-22T15:22:26.661Z] Result: Function 'command', Invocation id '7edc51c6-a6ac-4005-96cf-b5697e132882': An exception was thrown by the invocation.
Exception: Microsoft.Azure.Functions.Worker.FunctionInputConverterException: Error converting 1 input parameters for Function 'command': Could not populate the value for 'request' parameter. Consider updating the parameter with a default value.
[2023-11-22T15:22:26.666Z]    at Microsoft.Azure.Functions.Worker.Context.Features.DefaultFunctionInputBindingFeature.BindFunctionInputAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Context\Features\DefaultFunctionInputBindingFeature.cs:line 88
[2023-11-22T15:22:26.670Z]    at Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionExecutor.ExecuteAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Invocation\DefaultFunctionExecutor.cs:line 42
[2023-11-22T15:22:26.672Z]    at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in D:\a\_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:line 13
[2023-11-22T15:22:26.674Z]    at Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:line 77
Stack:    at Microsoft.Azure.Functions.Worker.Context.Features.DefaultFunctionInputBindingFeature.BindFunctionInputAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Context\Features\DefaultFunctionInputBindingFeature.cs:line 88
[2023-11-22T15:22:26.681Z]    at Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionExecutor.ExecuteAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Invocation\DefaultFunctionExecutor.cs:line 42
[2023-11-22T15:22:26.684Z]    at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in D:\a\_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:line 13
[2023-11-22T15:22:26.687Z]    at Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:line 77.
[2023-11-22T15:22:26.701Z] Executed 'Functions.command' (Failed, Id=7edc51c6-a6ac-4005-96cf-b5697e132882, Duration=234ms)
[2023-11-22T15:22:26.704Z] System.Private.CoreLib: Exception while executing function: Functions.command. System.Private.CoreLib: Result: Failure
Exception: Microsoft.Azure.Functions.Worker.FunctionInputConverterException: Error converting 1 input parameters for Function 'command': Could not populate the value for 'request' parameter. Consider updating the parameter with a default value.
[2023-11-22T15:22:26.707Z]    at Microsoft.Azure.Functions.Worker.Context.Features.DefaultFunctionInputBindingFeature.BindFunctionInputAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Context\Features\DefaultFunctionInputBindingFeature.cs:line 88
[2023-11-22T15:22:26.708Z]    at Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionExecutor.ExecuteAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Invocation\DefaultFunctionExecutor.cs:line 42
[2023-11-22T15:22:26.711Z]    at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in D:\a\_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:line 13
[2023-11-22T15:22:26.715Z]    at Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:line 77
[2023-11-22T15:22:26.716Z]    at Microsoft.Azure.Functions.Worker.Handlers.InvocationHandler.InvokeAsync(InvocationRequest request) in D:\a\_work\1\s\src\DotNetWorker.Grpc\Handlers\InvocationHandler.cs:line 88
Stack:    at Microsoft.Azure.Functions.Worker.Context.Features.DefaultFunctionInputBindingFeature.BindFunctionInputAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Context\Features\DefaultFunctionInputBindingFeature.cs:line 88
[2023-11-22T15:22:26.719Z]    at Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionExecutor.ExecuteAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Invocation\DefaultFunctionExecutor.cs:line 42
[2023-11-22T15:22:26.721Z]    at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in D:\a\_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:line 13
[2023-11-22T15:22:26.725Z]    at Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:line 77
[2023-11-22T15:22:26.728Z]    at Microsoft.Azure.Functions.Worker.Handlers.InvocationHandler.InvokeAsync(InvocationRequest request) in D:\a\_work\1\s\src\DotNetWorker.Grpc\Handlers\InvocationHandler.cs:line 88.

Fix would be to use HttpRequestData which would require changes to the request handler interface,

    [Function("command")]
    public Task<SlackResult> Command([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData request)
    {
        return _requestHandler.HandleSlashCommandRequest(request, _endpointConfig);
    }
soxtoby commented 9 months ago

Hi @Tyler-V. Unfortunately, for the foreseeable future SlackNet won't work with isolated worker Functions (see #144), given the size of the change that would be required. If you want to use an Azure Function, it'll need to be in-process at the moment.

Tyler-V commented 9 months ago

Hi @Tyler-V. Unfortunately, for the foreseeable future SlackNet won't work with isolated worker Functions (see #144), given the size of the change that would be required. If you want to use an Azure Function, it'll need to be in-process at the moment.

Understood - thanks @soxtoby!

Perhaps we can get another branch going in the future would be happy to contribute, started to migrate the SlackRequestHandler just to see what it would take

image

Azure's roadmap for Azure Functions starting with .NET 8 looks like it will be moving away from in-process to only isolated starting in .NET 9

soxtoby commented 9 months ago

Hi @Tyler-V, I took a deeper look at this, and with Microsoft's introduction of ASP.NET integration for isolated worker functions, this turned out to be much more feasible than before. Isolated worker functions are now supported in v0.12.0 with the SlackNet.AzureFunctions package.

Tyler-V commented 9 months ago

Hi @Tyler-V, I took a deeper look at this, and with Microsoft's introduction of ASP.NET integration for isolated worker functions, this turned out to be much more feasible than before. Isolated worker functions are now supported in v0.12.0 with the SlackNet.AzureFunctions package.

Great news that's awesome thanks @soxtoby .net 8 here we come!

When you say

albeit at the cost of dropping support for early responses (responses will be sent after you've finished handling the request, no matter how early you respond)

Does that mean async operations in handlers are going to timeout against Slack more frequently?

We have a SlashCommandResponse that creates a Modal View, handled by a ViewSubmissionResponse, and quite frequently see operation_timeout on the first request of the day, was hoping the isolated worker model and .NET 8 might improve the timeouts we are seeing

janssen-io commented 8 months ago

@soxtoby given that isolated workers are now supported. Are there plans to also support the Microsoft.Azure.Functions.Worker.Http.HttpRequestData object next to the Microsoft.AspNetCore.Http.HttpRequest object?

soxtoby commented 8 months ago

Does that mean async operations in handlers are going to timeout against Slack more frequently?

@Tyler-V If you can't respond quickly enough, then yes. I couldn't find a reliable way to keep handlers running after the request has completed. If what your function is doing is taking too long, then I'd suggest queuing the work with an Azure Queue and picking it up with another function triggered by that queue.

Are there plans to also support the Microsoft.Azure.Functions.Worker.Http.HttpRequestData object next to the Microsoft.AspNetCore.Http.HttpRequest object?

@janssen-io no - what made isolated worker support feasible was the ASP.NET integration.

Tyler-V commented 8 months ago

Does that mean async operations in handlers are going to timeout against Slack more frequently?

@Tyler-V If you can't respond quickly enough, then yes. I couldn't find a reliable way to keep handlers running after the request has completed. If what your function is doing is taking too long, then I'd suggest queuing the work with an Azure Queue and picking it up with another function triggered by that queue.

Are there plans to also support the Microsoft.Azure.Functions.Worker.Http.HttpRequestData object next to the Microsoft.AspNetCore.Http.HttpRequest object?

@janssen-io no - what made isolated worker support feasible was the ASP.NET integration.

@soxtoby in theory, if one function just responds to the Slack challenge and offloads that message SlashCommand command to a Queue Trigger is it still possible to open a Slack Modal from that Queue Function?

await _slack.Views.Open(command.TriggerId, new ModalViewDefinition

mblack-montag commented 8 months ago

@Tyler-V Unfortunately, trigger_id from an interaction has a 3 second expiration. It's strange the timeout is only on the first request of the day. What's happening between the slash command and the Views.Open? If you can't consistently open the view in under 3 seconds then Simon's suggestion of using a queue is a good idea. The slash command could open a basic view with a message like "Waiting for data", take the resulting view id and pass it to the queue where your longer process can run, and then call Views.UpdateByViewId:

await _slack.Views
    .UpdateByViewId(UserRequestModal.GetThirdStageView(JsonConvert.DeserializeObject<OpenOrder>(button.Value)), request.View.Id)
    .ConfigureAwait(false);

Using external selectors in my modal has helped me reduce the time to open a view as well. Just depends on what you're doing.

Tyler-V commented 8 months ago

@Tyler-V Unfortunately, trigger_id from an interaction has a 3 second expiration. It's strange the timeout is only on the first request of the day. What's happening between the slash command and the Views.Open? If you can't consistently open the view in under 3 seconds then Simon's suggestion of using a queue is a good idea. The slash command could open a basic view with a message like "Waiting for data", take the resulting view id and pass it to the queue where your longer process can run, and then call Views.UpdateByViewId:

await _slack.Views
  .UpdateByViewId(UserRequestModal.GetThirdStageView(JsonConvert.DeserializeObject<OpenOrder>(button.Value)), request.View.Id)
  .ConfigureAwait(false);

Using external selectors in my modal has helped me reduce the time to open a view as well. Just depends on what you're doing.

Prepopulating the modal with dynamic values for options, think of a list of projects unique to a specific Slack User

We have to,

  1. Query for the var user = _slack.UserProfile.Get(command.UserId)
  2. Retrieve Projects by Slack user.email (projects are then cached after the first request)
  3. Retrieve if the user has already viewed/submitted the modal and repopulate it with their previous selections

Those async queries piled up between our API, Slack's API, and the mercy of our third-party API where we retrieve projects can be problematic for the first user (before caching) to reply in 3 seconds so we're reworking the solution to Simon's suggestion

I like the idea of presenting something followed by updating the view, thanks for the recommendation

mblack-montag commented 8 months ago

@Tyler-V Unfortunately, trigger_id from an interaction has a 3 second expiration. It's strange the timeout is only on the first request of the day. What's happening between the slash command and the Views.Open? If you can't consistently open the view in under 3 seconds then Simon's suggestion of using a queue is a good idea. The slash command could open a basic view with a message like "Waiting for data", take the resulting view id and pass it to the queue where your longer process can run, and then call Views.UpdateByViewId:

await _slack.Views
    .UpdateByViewId(UserRequestModal.GetThirdStageView(JsonConvert.DeserializeObject<OpenOrder>(button.Value)), request.View.Id)
    .ConfigureAwait(false);

Using external selectors in my modal has helped me reduce the time to open a view as well. Just depends on what you're doing.

Prepopulating the modal with dynamic values for options, think of a list of projects unique to a specific Slack User

We have to,

  1. Query for the var user = _slack.UserProfile.Get(command.UserId)
  2. Retrieve Projects by Slack user.email (projects are then cached after the first request)
  3. Retrieve if the user has already viewed/submitted the modal and repopulate it with their previous selections

Those async queries piled up between our API, Slack's API, and the mercy of our third-party API where we retrieve projects can be problematic for the first user (before caching) to reply in 3 seconds so we're reworking the solution to Simon's suggestion

I like the idea of presenting something followed by updating the view, thanks for the recommendation

That was exactly what I had run into and what Slack's external selectors are meant to solve. I had a modal with 5 or so dropdowns that took too long to populate, plus there were thousands of records for some. With external selectors and SlackNet I was able to create a lookup. On some, the user would type x characters as a starting filter before data was returned.

Here is a link to Slack API docs: https://api.slack.com/reference/block-kit/block-elements#external_select It also works for multi-select.

Here is a quick gist of a handler, modal, and external selector: https://gist.github.com/mblack-montag/347a09df2dd2a11ff8e542ceae234a43