From c3894457075c5b8926d235f15e99c6f0ecea2ae8 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Tue, 10 Jun 2025 03:05:11 -0300 Subject: [PATCH 1/3] Repro of a handler type hang Unlike the lambda/anonymous function handler, the class one never resolves and hangs the process. --- src/SampleApp/Sample/Program.cs | 93 ++++++++++++++++----------------- 1 file changed, 46 insertions(+), 47 deletions(-) diff --git a/src/SampleApp/Sample/Program.cs b/src/SampleApp/Sample/Program.cs index 13289fb..7bba6a2 100644 --- a/src/SampleApp/Sample/Program.cs +++ b/src/SampleApp/Sample/Program.cs @@ -42,7 +42,7 @@ throw new InvalidOperationException("Missing required App:Storage connection string.")); builder.Services - .AddWhatsApp, JsonSerializerOptions>(builder.Configuration, ProcessMessagesAsync) + .AddWhatsApp(builder.Configuration) // Matches what we use in ConfigureOpenTelemetry .UseOpenTelemetry(builder.Environment.ApplicationName) .UseLogging() @@ -51,58 +51,57 @@ builder.Build().Run(); -static async IAsyncEnumerable ProcessMessagesAsync( - ILogger logger, - JsonSerializerOptions options, - IEnumerable messages, - [EnumeratorCancellation] CancellationToken cancellationToken) +class ProcessHandler(ILogger logger, JsonSerializerOptions options) : IWhatsAppHandler { - // Avoid warning CS1998 // Async method lacks 'await' operators and will run synchronously - await Task.CompletedTask; + public async IAsyncEnumerable HandleAsync(IEnumerable messages, [EnumeratorCancellation] CancellationToken cancellation = default) + { + // Avoid warning CS1998 // Async method lacks 'await' operators and will run synchronously + await Task.CompletedTask; - var message = messages.Last(); - logger.LogInformation("πŸ’¬ Received message: {Message}", message); + var message = messages.Last(); + logger.LogInformation("πŸ’¬ Received message: {Message}", message); - if (message is ErrorMessage error) - { - // Reengagement error, we need to invite the user. - if (error.Error.Code == 131047) + if (message is ErrorMessage error) { - // Showcases how to use a pre-declared template response to reengage the user. - yield return error.Template("reengagement", "es_AR"); + // Reengagement error, we need to invite the user. + if (error.Error.Code == 131047) + { + // Showcases how to use a pre-declared template response to reengage the user. + yield return error.Template("reengagement", "es_AR"); + } + else + { + logger.LogWarning("⚠️ Unknown error message received: {Error}", message); + } } - else + else if (message is InteractiveMessage interactive) { - logger.LogWarning("⚠️ Unknown error message received: {Error}", message); + logger.LogWarning("πŸ‘€ chose {Button} ({Title})", interactive.Button.Id, interactive.Button.Title); + yield return interactive.Reply($"πŸ‘€ chose: {interactive.Button.Title} ({interactive.Button.Id})"); } - } - else if (message is InteractiveMessage interactive) - { - logger.LogWarning("πŸ‘€ chose {Button} ({Title})", interactive.Button.Id, interactive.Button.Title); - yield return interactive.Reply($"πŸ‘€ chose: {interactive.Button.Title} ({interactive.Button.Id})"); - } - else if (message is ReactionMessage reaction) - { - logger.LogInformation("πŸ‘€ reaction: {Reaction}", reaction.Emoji); - yield return reaction.Reply($"πŸ‘€ reaction: {reaction.Emoji}"); - } - else if (message is StatusMessage status) - { - logger.LogInformation("β˜‘οΈ status: {Status}", status.Status); - } - else if (message is ContentMessage content) - { - yield return content.React("🧠"); + else if (message is ReactionMessage reaction) + { + logger.LogInformation("πŸ‘€ reaction: {Reaction}", reaction.Emoji); + yield return reaction.Reply($"πŸ‘€ reaction: {reaction.Emoji}"); + } + else if (message is StatusMessage status) + { + logger.LogInformation("β˜‘οΈ status: {Status}", status.Status); + } + else if (message is ContentMessage content) + { + yield return content.React("🧠"); - // simulate some hard work at hand, like doing some LLM-stuff :) - //await Task.Delay(2000); - yield return content.Reply( - $"β˜‘οΈ Got your {content.Content.Type}:\r\n{JsonSerializer.Serialize(content, options)}", - new Button("btn_good", "πŸ‘"), - new Button("btn_bad", "πŸ‘Ž")); - } - else if (message is UnsupportedMessage unsupported) - { - logger.LogWarning("⚠️ {Message}", unsupported); + // simulate some hard work at hand, like doing some LLM-stuff :) + //await Task.Delay(2000); + yield return content.Reply( + $"β˜‘οΈ Got your {content.Content.Type}:\r\n{JsonSerializer.Serialize(content, options)}", + new Button("btn_good", "πŸ‘"), + new Button("btn_bad", "πŸ‘Ž")); + } + else if (message is UnsupportedMessage unsupported) + { + logger.LogWarning("⚠️ {Message}", unsupported); + } } -} +} \ No newline at end of file From eb11fc3945f775bfbd3c2a494c990fd0ce43c80d Mon Sep 17 00:00:00 2001 From: Adrian Alonso Date: Tue, 10 Jun 2025 09:46:34 -0300 Subject: [PATCH 2/3] Fixed hang by avoiding registering the business handler with IWhatsAppHandler IWhatsAppHandler is already registered by the builder to the the entire pipeline. So, we should try to resolve the provided concrete handler type and resolve all the dependencies defined in the ctor. --- src/WhatsApp/WhatsAppServiceCollectionExtensions.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/WhatsApp/WhatsAppServiceCollectionExtensions.cs b/src/WhatsApp/WhatsAppServiceCollectionExtensions.cs index d254bd6..244ff70 100644 --- a/src/WhatsApp/WhatsAppServiceCollectionExtensions.cs +++ b/src/WhatsApp/WhatsAppServiceCollectionExtensions.cs @@ -126,9 +126,12 @@ public static WhatsAppHandlerBuilder AddWhatsApp( ServiceLifetime lifetime = ServiceLifetime.Singleton) where THandler : class, IWhatsAppHandler { - collection.Add(new ServiceDescriptor(typeof(IWhatsAppHandler), services => services.GetRequiredService(), lifetime)); + if (collection.FirstOrDefault(x => x.ServiceType == typeof(THandler)) == null) + { + collection.Add(new ServiceDescriptor(typeof(THandler), typeof(THandler), lifetime)); + } - return collection.AddWhatsApp(configuration, services => services.GetRequiredService(), lifetime); + return collection.AddWhatsApp(configuration, services => services.GetRequiredService(), lifetime); } /// From fe44d987bd724e177a2cc56f47f2cafc227c6e34 Mon Sep 17 00:00:00 2001 From: Adrian Alonso Date: Tue, 10 Jun 2025 10:15:25 -0300 Subject: [PATCH 3/3] Moved out the main process handler out of the program.cs --- src/SampleApp/Sample/ProcessHandler.cs | 59 +++++++++++++++++++++++++ src/SampleApp/Sample/Program.cs | 60 +------------------------- 2 files changed, 61 insertions(+), 58 deletions(-) create mode 100644 src/SampleApp/Sample/ProcessHandler.cs diff --git a/src/SampleApp/Sample/ProcessHandler.cs b/src/SampleApp/Sample/ProcessHandler.cs new file mode 100644 index 0000000..43cac76 --- /dev/null +++ b/src/SampleApp/Sample/ProcessHandler.cs @@ -0,0 +1,59 @@ +ο»Ώusing System.Runtime.CompilerServices; +using System.Text.Json; +using Devlooped.WhatsApp; +using Microsoft.Extensions.Logging; + +class ProcessHandler(ILogger logger, JsonSerializerOptions options) : IWhatsAppHandler +{ + public async IAsyncEnumerable HandleAsync(IEnumerable messages, [EnumeratorCancellation] CancellationToken cancellation = default) + { + // Avoid warning CS1998 // Async method lacks 'await' operators and will run synchronously + await Task.CompletedTask; + + var message = messages.Last(); + logger.LogInformation("πŸ’¬ Received message: {Message}", message); + + if (message is ErrorMessage error) + { + // Reengagement error, we need to invite the user. + if (error.Error.Code == 131047) + { + // Showcases how to use a pre-declared template response to reengage the user. + yield return error.Template("reengagement", "es_AR"); + } + else + { + logger.LogWarning("⚠️ Unknown error message received: {Error}", message); + } + } + else if (message is InteractiveMessage interactive) + { + logger.LogWarning("πŸ‘€ chose {Button} ({Title})", interactive.Button.Id, interactive.Button.Title); + yield return interactive.Reply($"πŸ‘€ chose: {interactive.Button.Title} ({interactive.Button.Id})"); + } + else if (message is ReactionMessage reaction) + { + logger.LogInformation("πŸ‘€ reaction: {Reaction}", reaction.Emoji); + yield return reaction.Reply($"πŸ‘€ reaction: {reaction.Emoji}"); + } + else if (message is StatusMessage status) + { + logger.LogInformation("β˜‘οΈ status: {Status}", status.Status); + } + else if (message is ContentMessage content) + { + yield return content.React("🧠"); + + // simulate some hard work at hand, like doing some LLM-stuff :) + //await Task.Delay(2000); + yield return content.Reply( + $"β˜‘οΈ Got your {content.Content.Type}:\r\n{JsonSerializer.Serialize(content, options)}", + new Button("btn_good", "πŸ‘"), + new Button("btn_bad", "πŸ‘Ž")); + } + else if (message is UnsupportedMessage unsupported) + { + logger.LogWarning("⚠️ {Message}", unsupported); + } + } +} \ No newline at end of file diff --git a/src/SampleApp/Sample/Program.cs b/src/SampleApp/Sample/Program.cs index 7bba6a2..fbc19d9 100644 --- a/src/SampleApp/Sample/Program.cs +++ b/src/SampleApp/Sample/Program.cs @@ -1,5 +1,4 @@ -ο»Ώusing System.Runtime.CompilerServices; -using System.Text.Json; +ο»Ώusing System.Text.Json; using System.Text.Json.Serialization; using Devlooped; using Devlooped.WhatsApp; @@ -49,59 +48,4 @@ .UseStorage() .UseConversation(); -builder.Build().Run(); - -class ProcessHandler(ILogger logger, JsonSerializerOptions options) : IWhatsAppHandler -{ - public async IAsyncEnumerable HandleAsync(IEnumerable messages, [EnumeratorCancellation] CancellationToken cancellation = default) - { - // Avoid warning CS1998 // Async method lacks 'await' operators and will run synchronously - await Task.CompletedTask; - - var message = messages.Last(); - logger.LogInformation("πŸ’¬ Received message: {Message}", message); - - if (message is ErrorMessage error) - { - // Reengagement error, we need to invite the user. - if (error.Error.Code == 131047) - { - // Showcases how to use a pre-declared template response to reengage the user. - yield return error.Template("reengagement", "es_AR"); - } - else - { - logger.LogWarning("⚠️ Unknown error message received: {Error}", message); - } - } - else if (message is InteractiveMessage interactive) - { - logger.LogWarning("πŸ‘€ chose {Button} ({Title})", interactive.Button.Id, interactive.Button.Title); - yield return interactive.Reply($"πŸ‘€ chose: {interactive.Button.Title} ({interactive.Button.Id})"); - } - else if (message is ReactionMessage reaction) - { - logger.LogInformation("πŸ‘€ reaction: {Reaction}", reaction.Emoji); - yield return reaction.Reply($"πŸ‘€ reaction: {reaction.Emoji}"); - } - else if (message is StatusMessage status) - { - logger.LogInformation("β˜‘οΈ status: {Status}", status.Status); - } - else if (message is ContentMessage content) - { - yield return content.React("🧠"); - - // simulate some hard work at hand, like doing some LLM-stuff :) - //await Task.Delay(2000); - yield return content.Reply( - $"β˜‘οΈ Got your {content.Content.Type}:\r\n{JsonSerializer.Serialize(content, options)}", - new Button("btn_good", "πŸ‘"), - new Button("btn_bad", "πŸ‘Ž")); - } - else if (message is UnsupportedMessage unsupported) - { - logger.LogWarning("⚠️ {Message}", unsupported); - } - } -} \ No newline at end of file +builder.Build().Run(); \ No newline at end of file