Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2138 Introduce ASP.NET rate limiting #2188

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from

Conversation

yjorayev
Copy link

@yjorayev yjorayev commented Nov 6, 2024

New Feature

Closes #2138

Discussion thread

Proposed Changes

  • Includes changes to use .Net middleware
  • Middleware type for rate limiting can be specified in ocelot.json. It defaults to Ocelot's existing rate limiter if not specified
  • Also added some Unit Tests, Integration test and a sample

Documentation block

@raman-m raman-m added feature A new feature Rate Limiting Ocelot feature: Rate Limiting labels Nov 6, 2024
Copy link
Member

@raman-m raman-m left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for PR creation!

Please move current test/Ocelot.IntegrationTests/RateLimitingTests to Ocelot.AcceptanceTests project, we already have a suitable class: ClientRateLimitingTests. However, these tests only cover the Ocelot feature "Rate Limit by Client's Header." Since you are proposing a new feature, a new testing class should be created.

P.S. I recommend (and require) inheriting from Steps, which offers many useful testing helpers, so there's no need to reinvent the wheel.

test/Ocelot.IntegrationTests/RateLimitingTests.cs Outdated Show resolved Hide resolved
@raman-m raman-m changed the title Users/yjorayev/dotnet ratelimit Introduce ASP.NET rate limiting Nov 6, 2024
@raman-m raman-m changed the title Introduce ASP.NET rate limiting #2138 Introduce ASP.NET rate limiting Nov 6, 2024
@raman-m
Copy link
Member

raman-m commented Nov 6, 2024

Yhlas, thank you once again. Here's the current status: I will request an official code review after transitioning tests to acceptance testing. Please remember to draft the preliminary docs.

Regarding the delivery vision, I cannot include it in the current Autumn'24 milestone due to anticipated multiple development rounds. Post-release, our team intends to upgrade Ocelot to .NET 9. It appears your feature may be incorporated into the release alongside this upgrade. Therefore, I suggest testing the PR code with the current .NET 9 RC. Is that okay? Consequently, we will target two frameworks in the project files: net8.0 and net9.0

@yjorayev
Copy link
Author

yjorayev commented Nov 7, 2024

... I suggest testing the PR code with the current .NET 9 RC. Is that okay? Consequently, we will target two frameworks in the project files: net8.0 and net9.0

I targeted Ocelot project and a sample to net 9.0-rc. No issue on manual testing.

I will start working on docs for this in a few days. Thanks!

@yjorayev yjorayev requested a review from raman-m November 7, 2024 06:36
@raman-m raman-m requested review from RaynaldM and ggnaegi November 8, 2024 10:32
Copy link
Member

@raman-m raman-m left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can enhance the quality of the PR: my suggestions are outlined below 👇
FYI, I will revisit this PR once it has been reviewed by a team member as well.

P.S. Keep in mind Line-Ending Gotchas aka EOL Fun. I saw some files have fake changes because of changed line endings.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you use it? Is this built-in Visual Studio feature to call APIs?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Little "play" icon shows near each request definition that can be clicked to send http request. Below screenshot is from Rider, but VS has this feature too.

Screenshot

Docs for .http

Copy link
Member

@raman-m raman-m Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this is Rider IDE! Thanks for the link!
I will learn the same feature for dev productivity in Visual Studio...

samples/RateLimiter/Program.cs Show resolved Hide resolved
src/Ocelot/Configuration/File/FileRateLimitRule.cs Outdated Show resolved Hide resolved
src/Ocelot/Configuration/RateLimitMiddlewareType.cs Outdated Show resolved Hide resolved
public static IServiceCollection AddRateLimiting(this IServiceCollection services) => services
.AddSingleton<IRateLimiting, RateLimiting.RateLimiting>()
.AddSingleton<IRateLimitStorage, MemoryCacheRateLimitStorage>();
public static IServiceCollection AddRateLimiting(this IServiceCollection services, IConfiguration configurationRoot)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interface Breaking Change❗

Can old Ocelot applications compile successfully with this change?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add a new method instead of overriding existing one. I assumed this method is for internal purposes, but now I realize that it was a wrong assumption given that this method is public.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decision hinges on the ultimate logic! Perhaps separating the internal extension methods is not essential.
I advise to revisit this once all the logic has been developed.

@@ -103,7 +103,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo
Services.TryAddSingleton<IDownstreamRouteProviderFactory, DownstreamRouteProviderFactory>();
Services.TryAddSingleton<IHttpResponder, HttpContextResponder>();
Services.TryAddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>();
Services.AddRateLimiting(); // Feature: Rate Limiting
Services.AddRateLimiting(configurationRoot); // Feature: Rate Limiting
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, it should be acceptable; however, I would advise separating the setup for each rate limiting mode.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separated

Comment on lines 83 to 88
#if NET7_0_OR_GREATER
app.UseWhen(UseDotNetRateLimiter, rateLimitedApp =>
{
rateLimitedApp.UseRateLimiter();
});
#endif
Copy link
Member

@raman-m raman-m Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good, but it can be encapsulated in UseRateLimiting method (line 80), right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@ggnaegi
Copy link
Member

ggnaegi commented Nov 8, 2024

@raman-m That's a great idea, but we are stuck with the .NET rate limiters specs, you can't modify them on the fly, as you could with the current limiters. The rate limiters policies must be known, and therefore configured during startup

@raman-m
Copy link
Member

raman-m commented Nov 9, 2024

but we are stuck with the .NET rate limiters specs, you can't modify them on the fly, as you could with the current limiters.

@ggnaegi, what is your suggestion?
Could you please clarify the problem for the author?
Why not conduct a separate code review yourself?

@raman-m raman-m added the conflicts Feature branch has merge conflicts label Nov 11, 2024
@raman-m
Copy link
Member

raman-m commented Nov 11, 2024

Unfortunately, merge conflicts have arisen due to the recent merging of PR #1307 and its commit da9d6fa into the develop branch. I recommend to perform rebasing.

yjorayev and others added 11 commits November 12, 2024 23:56
…Context` response accessed via `IHttpContextAccessor` (ThreeMammals#1307)

* set rate limiting headers on the proper httpcontext

* fix Retry-After header

* merge fix

* refactor of ClientRateLimitTests

* merge fix

* Fix build after rebasing

* EOL: test/Ocelot.AcceptanceTests/Steps.cs

* Add `RateLimitingSteps`

* code review by @raman-m

* Inject IHttpContextAccessor, not IServiceProvider

* Ocelot's rate-limiting headers have become legacy

* Headers definition life hack

* A good StackOverflow link

---------

Co-authored-by: Jolanta Łukawska <[email protected]>
Co-authored-by: Raman Maksimchuk <[email protected]>
@yjorayev yjorayev force-pushed the users/yjorayev/dotnet-ratelimit branch from 59ef966 to e8987ad Compare November 13, 2024 06:15
@ggnaegi
Copy link
Member

ggnaegi commented Nov 13, 2024

but we are stuck with the .NET rate limiters specs, you can't modify them on the fly, as you could with the current limiters.

@ggnaegi, what is your suggestion? Could you please clarify the problem for the author? Why not conduct a separate code review yourself?

@yjorayev @raman-m
I'am wondering if we could replace all the current rate limiters in the request pipeline, like this (haven't checked), then it would make sense imho:

public class CustomRateLimiterMiddleware
{
    private readonly RequestDelegate _next;
    private readonly TokenBucketRateLimiter _rateLimiter;

    public CustomRateLimiterMiddleware(RequestDelegate next)
    {
        _next = next;
        
        // Create a rate limiter with a custom configuration
        _rateLimiter = new TokenBucketRateLimiter(
            new TokenBucketRateLimiterOptions
            {
                TokenLimit = 10,
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = 5,
                ReplenishmentPeriod = TimeSpan.FromSeconds(1),
                TokensPerPeriod = 1,
                AutoReplenishment = true
            });
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Attempt to lease a token for each request
        var lease = await _rateLimiter.WaitAsync();

        if (lease.IsAcquired)
        {
            try
            {
                await _next(context);
            }
            finally
            {
                lease.Dispose();
            }
        }
        else
        {
            context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        }
    }
}

@yjorayev
Copy link
Author

@ggnaegi, @raman-m I think we can make this work. Couple things:

  1. We will need to be able to get rate limiter policy by name. Simple dictionary might suffice.
  2. If we go this route, we might find ourselves re-implementing(copying?) CombinedAcquire and CombinedWaitAsync methods of RateLimitingMiddleware.

What do you think?

@ggnaegi
Copy link
Member

ggnaegi commented Nov 14, 2024

@ggnaegi, @raman-m I think we can make this work. Couple things:

  1. We will need to be able to get rate limiter policy by name. Simple dictionary might suffice.
  2. If we go this route, we might find ourselves re-implementing(copying?) CombinedAcquire and CombinedWaitAsync methods of RateLimitingMiddleware.

What do you think?

Yes, that's the main concern; we essentially need to reinvent the wheel for the middleware part. I've tried to "dynamize" the policies myself, but I believe it’s not possible. However, at least we can retain Microsoft's RateLimiter implementations and replace the current ones.

I’d go down that path, and if you look at the code, there isn’t much to modify.

@yjorayev @raman-m

@raman-m
Copy link
Member

raman-m commented Nov 14, 2024

@ggnaegi @yjorayev

Hello, guys! What's up?

Has .NET 9 been released already? Did I miss the event? 😄

@yjorayev Feel free to request the 2nd code review if you're ready.

@raman-m raman-m removed the conflicts Feature branch has merge conflicts label Nov 14, 2024
@raman-m
Copy link
Member

raman-m commented Nov 14, 2024

@yjorayev commented on Nov 14

  1. We will need to be able to get rate limiter policy by name. Simple dictionary might suffice.

No objections!

  1. If we go this route, we might find ourselves re-implementing(copying?) CombinedAcquire and CombinedWaitAsync methods of RateLimitingMiddleware.

I don't recommend copying and pasting code from the MS codebase for long-term purposes because the internal implementation can change with .NET version updates. However, it can be acceptable in the short term until we develop a more practical design solution based on public methods.

Let's hack this private method?! 😎 Why not to call it with a help of reflection technology? 🙊

@ThreeMammals ThreeMammals deleted a comment from ggnaegi Nov 15, 2024
@ThreeMammals ThreeMammals deleted a comment from ggnaegi Nov 15, 2024
@yjorayev
Copy link
Author

I don't recommend copying and pasting code from the MS codebase for long-term purposes because the internal implementation can change with .NET version updates. However, it can be acceptable in the short term until we develop a more practical design solution based on public methods.

Let's hack this private method?! 😎 Why not to call it with a help of reflection technology? 🙊

I disagree about calling private methods, because private method signature can(probably will) change even with minor version upgrades.

What are the advantages of calling the private method via reflection in contrast to my original proposal in this PR? I understand that my proposal is also sort of a hack, but it only interacts with public methods and classes which is less likely to change.

@ggnaegi
Copy link
Member

ggnaegi commented Nov 16, 2024

I don't recommend copying and pasting code from the MS codebase for long-term purposes because the internal implementation can change with .NET version updates. However, it can be acceptable in the short term until we develop a more practical design solution based on public methods.

Let's hack this private method?! 😎 Why not to call it with a help of reflection technology? 🙊

I disagree about calling private methods, because private method signature can(probably will) change even with minor version upgrades.

What are the advantages of calling the private method via reflection in contrast to my original proposal in this PR? I understand that my proposal is also sort of a hack, but it only interacts with public methods and classes which is less likely to change.

@yjorayev @raman-m Mmmh, i haven't read this bit of trolling from Raman. My point was: Use the classes FixedWindowRateLimiter, SlidingWindowRateLimiter etc. and avoid our constructs that are less reliable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature A new feature Rate Limiting Ocelot feature: Rate Limiting
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants