Cysharp / MagicOnion

Unified Realtime/API framework for .NET platform and Unity.
MIT License
3.89k stars 433 forks source link

Question: Creating a RetryFilter using ClientFilter #827

Closed devpioh closed 1 month ago

devpioh commented 3 months ago

Hello, I am currently developing a game using MagicOnion. Thank you for creating such an amazing library.

I am currently facing an issue.

I am trying to implement a RetryFilter using the ClientFilter example provided. Additionally, I am trying to apply a timeout to handle this process.

My code is as follows:

public async ValueTask<ResponseContext> SendAsync(RequestContext context, Func<RequestContext, ValueTask<ResponseContext>> next)
{
    int attempt = 0;
    while (attempt < _maxRetries)
    {
        try
        {
            // Set the Deadline in CallOptions.
            context.CallOptions.WithDeadline(DateTime.UtcNow.Add(TimeSpan.FromSeconds(3)));
            return await next(context);
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
        {
            attempt++;
            Debug.LogWarning($"Request timeout occurred. Retry {attempt}/{_maxRetries}");

            if (await _networkManager.ShowRetryPopup())
            {
                await Task.Delay(_delayBetweenRetries);
            }
            else
            {
                throw; // Throw an exception if the user doesn't want to retry
            }
        }
    }

    throw new Exception("All retries failed.");
}

The problematic part is context.CallOptions.WithDeadline(DateTime.UtcNow.Add(TimeSpan.FromSeconds(3))).

Even though I set the deadline, debugging shows that the deadline is not being applied.

Is there something I might be doing wrong?

licentia88 commented 2 months ago

hello, I have this filter that works for me but the way I call the service is

Service.WithDeadline().MyMethod name 
or 
Service.WithCancellationToken().MyMethodName
using MagicOnion.Client;

namespace MagicT.Client.Filters;

/// <summary>
/// Filter for retrying client requests.
/// </summary>
internal sealed class RetryFilter : IClientFilter
{
    private readonly int _maxRetryAttempts;
    private readonly TimeSpan _retryDelay;

    /// <summary>
    /// Initializes a new instance of the <see cref="RetryFilter"/> class.
    /// </summary>
    /// <param name="maxRetryAttempts">The maximum number of retry attempts.</param>
    /// <param name="retryDelay">The delay between retries.</param>
    public RetryFilter(int maxRetryAttempts, TimeSpan retryDelay)
    {
        _maxRetryAttempts = maxRetryAttempts;
        _retryDelay = retryDelay;
    }

    /// <summary>
    /// Sends the request and retries if it fails.
    /// </summary>
    /// <param name="context">The request context.</param>
    /// <param name="next">The next step in the filter pipeline.</param>
    /// <returns>The response context.</returns>
    public async ValueTask<ResponseContext> SendAsync(RequestContext context, Func<RequestContext, ValueTask<ResponseContext>> next)
    {
        var attempt = 0;
        while (true)
        {
            try
            {
                return await next(context);
            }
            catch (Exception ex) when (attempt < _maxRetryAttempts)
            {
                attempt++;
                Console.WriteLine($"Attempt {attempt} failed: {ex.Message}. Retrying in {_retryDelay.TotalSeconds} seconds...");
                await Task.Delay(_retryDelay);
            }
        }
    }
}

I have more examples in this template project if you are interested

https://github.com/licentia88/MagicOnionGenericTemplate

devpioh commented 1 month ago

hello, I have this filter that works for me but the way I call the service is

Service.WithDeadline().MyMethod name 
or 
Service.WithCancellationToken().MyMethodName
using MagicOnion.Client;

namespace MagicT.Client.Filters;

/// <summary>
/// Filter for retrying client requests.
/// </summary>
internal sealed class RetryFilter : IClientFilter
{
    private readonly int _maxRetryAttempts;
    private readonly TimeSpan _retryDelay;

    /// <summary>
    /// Initializes a new instance of the <see cref="RetryFilter"/> class.
    /// </summary>
    /// <param name="maxRetryAttempts">The maximum number of retry attempts.</param>
    /// <param name="retryDelay">The delay between retries.</param>
    public RetryFilter(int maxRetryAttempts, TimeSpan retryDelay)
    {
        _maxRetryAttempts = maxRetryAttempts;
        _retryDelay = retryDelay;
    }

    /// <summary>
    /// Sends the request and retries if it fails.
    /// </summary>
    /// <param name="context">The request context.</param>
    /// <param name="next">The next step in the filter pipeline.</param>
    /// <returns>The response context.</returns>
    public async ValueTask<ResponseContext> SendAsync(RequestContext context, Func<RequestContext, ValueTask<ResponseContext>> next)
    {
        var attempt = 0;
        while (true)
        {
            try
            {
                return await next(context);
            }
            catch (Exception ex) when (attempt < _maxRetryAttempts)
            {
                attempt++;
                Console.WriteLine($"Attempt {attempt} failed: {ex.Message}. Retrying in {_retryDelay.TotalSeconds} seconds...");
                await Task.Delay(_retryDelay);
            }
        }
    }
}

I have more examples in this template project if you are interested

https://github.com/licentia88/MagicOnionGenericTemplate

I apologize for the delayed response, licentia88.

The code you provided was helpful (the template project helped with other implementations), but what I am trying to implement is as follows:

  1. When a timeout occurs, a reconnect popup is shown to the user.
  2. In the reconnect popup, if the user clicks the reconnect button, the previous request continues. If they click the cancel button, the request is terminated.

I would like to implement this using a ClientFilter. Do you have any ideas?

licentia88 commented 1 month ago

I do not understand why your code does not work, from my understanding once ShowRetryPopup is popped the if statement should wait for it until you press ok or cancel right?

assuming it's that way I understand your problem is with calling the the endpoint with a deadline..

I will try this tomorrow and give you more feedback on it.

apart from it, why are you asking for user for confirmation for resending the same request?

in my opinion what you are doing there is not good coding practice(your client filter implementation is not reusable), If I really needed to receive confirmation from user to resend request, i would prefer dealing with it in the blazor side not in the filter

devpioh commented 1 month ago

Ah, I see. I didn't realize that the filter couldn't be reused.

It seems that instead of reusing the filter as you suggested, I need to construct and send a new request.

I was thinking about the reconnection process that is commonly done in mobile games. (For example, showing a popup with a message like "Would you like to reconnect?" and if it fails, ending the process.)

I thought implementing this common reconnection process as a ClientFilter might be convenient.

Thank you for your help, licentia88.