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

Extract Adapters #18

Merged
merged 36 commits into from
Apr 6, 2024
Merged

Extract Adapters #18

merged 36 commits into from
Apr 6, 2024

Conversation

julik
Copy link
Contributor

@julik julik commented Feb 26, 2024

This also adds a Redis adapter and a Memory adapter. The Redis adapter is useful since it can be a continuation of Prorate in a way - but with conditional fillup added in.

The memory store is useful in a different way - it can be used as a reference implementation to test other adapters against.
Also make the adapter configurable on the Pecorino module.

@julik julik changed the title A better extraction for Adapters Extract Adapters Mar 12, 2024
@julik julik requested a review from skatkov March 12, 2024 22:07
@julik julik marked this pull request as ready for review March 12, 2024 22:07
@skatkov
Copy link
Contributor

skatkov commented Mar 13, 2024

@julik would it be possible to break this up into a separate Pull Requests?

@julik
Copy link
Contributor Author

julik commented Mar 13, 2024

It is possible, but how useful is it to do so? And where would the separation between the pull requests be, if there are dependencies between changed components?

@julik
Copy link
Contributor Author

julik commented Mar 13, 2024

Coming to think of it - I can take out the Redis adapter into a separate PR, but not much else 🤔 will that work for you?

lib/pecorino.rb Show resolved Hide resolved
@@ -90,12 +90,14 @@ def accepted?
# the bucket contents will then be capped at this value. So with
# bucket_capacity set to 12 and a `fillup(14)` the bucket will reach the level
# of 12, and will then immediately start leaking again.
def initialize(key:, capacity:, leak_rate: nil, over_time: nil)
# @param adapter[Pecorino::Adapters::BaseAdapter] a compatible adapter
def initialize(key:, capacity:, adapter: Pecorino.adapter, leak_rate: nil, over_time: nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

question: a question not related to this PR, more of a general question for LeakyBucket. I think our previous implementation allowed LeakyBucket to overfill, but you've fixed it, and now it doesn't do that any more.

If value is higher than capacity, it will refuse to fill a bucket.

I can imagine situations, where it would be great if leaky bucket could overfill. For example, if we're implementing a leaky bucket for API rate limiting, you might allow a user to exceed their rate limit by a small margin and then only enforce the limit if they continue to exceed it. This gives a bit of leeway for users who occasionally go over their limits but still protects your system from being overwhelmed by too many requests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A bucket can never overfill because all fillups get clamped to the capacity value. We have 2 methods - one for conditional fillup (where if your fillup "would" make the bucket overflow it gets refused) and one for unconditional fillup - it will only tell you whether the bucket did spillover as a result of your call. Having these two allows us to implement what you are describing, no? Or I am missing something and "Exceeding by a small margin" needs more information.

Copy link
Contributor

Choose a reason for hiding this comment

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

unconditional fillup - it will only tell you whether the bucket did spillover as a result of your call.

This is precisely what I was looking for.

lib/pecorino/adapters/redis_adapter.rb Show resolved Hide resolved
sleep 0.65

# Both the leaky bucket and the block should have expired by now, and `prune` should not raise
adapter.prune
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: we don't assert anything in this test. Is this is intentional? What's the point then to have it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a whole discussion about assert_nothing_raised here https://stackoverflow.com/questions/12499259/why-doesnt-minitestspec-have-a-wont-raise-assertion - basically, Ryan posits that if your call raises something, it means the test will fail anyway (and using "assert_nothing_raised" just inflates your green dot count and gives you fake satisfaction for no reason, which is not to be had on these premises!)

That's why I left out the assertion (I think it comes with the activesupport methods, which we do not import into the test suite just now). But I could add it if you think just "calling the thing" is not telling the reader what we are doing. Or we could stick in a comment?..

Copy link
Contributor

Choose a reason for hiding this comment

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

How do you feel about assert adapter.prune?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can do - we would need the prune method to return a truthy value for this.

lib/pecorino/adapters/memory_adapter.rb Outdated Show resolved Hide resolved
def set_block(key:, block_for:)
end

# Returns the time until which a block for a given key is in effect. If there is no block in
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: it's cool that you're writting these comments, but would it be better to have yard definitions here instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why not both? 😆 good one


* A memory store (good enough if you have just 1 process)
* A PostgreSQL or SQLite database (at the moment there is no MySQL support, we would be delighted if you could add it)
* A Redis instance

Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: might be a good idea to explain how to redefine adapter - in case of redis, as example, adapter will not be inherited from ActiveRecord and requires a manual configuration.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

None of them inherit from ActiveRecord anymore, some just use the ActiveRecord classes as means of passing connection configuration (and to piggyback on escaping)

Copy link
Contributor

Choose a reason for hiding this comment

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

None of them inherit from ActiveRecord anymore

Why are you saying that? there is a default_adapter_from_main_database method still defined.

https://github.com/cheddar-me/pecorino/pull/18/files#diff-3ae95d36dd505f2d90c28bab102aceb7c117caee072c2a5910685a35fb1f777bR49

But it would be great to add a section about how to configure adapter for pecorino and which settings are being configured by default.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is that method indeed (to let users have their adapter picked automatically), but nothing inherits anything there?..

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry for the confusion, indeed wrong use of the word "inherits".

@skatkov
Copy link
Contributor

skatkov commented Mar 13, 2024

@julik checking almost 1000 line of code was a bit overwhelming, but I did my best :)

end

def create_tables(active_record_schema)
active_record_schema.create_table :pecorino_leaky_buckets, id: :uuid do |t|
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: would it be possible not to force :uuid as a primary key on users? Make it configurable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah let's switch to whatever ID structure that is the app defalut 👍

active_record_schema.add_index :pecorino_leaky_buckets, [:key], unique: true
active_record_schema.add_index :pecorino_leaky_buckets, [:may_be_deleted_after]

active_record_schema.create_table :pecorino_blocks, id: :uuid do |t|
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Same suggestion here -- would be great not to force :uuid as a primary key on users and make this configurable.


require_relative "base_adapter"
require "digest"
require "redis"
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be great to surround this require with begin rescue. This would help us to throw a meaningful error, if redis gem is missing in gemfile.

begin
   require "redis"
rescue LoadError
   puts "Please include redis gem in Gemfile"
   exit
end


* A memory store (good enough if you have just 1 process)
* A PostgreSQL or SQLite database (at the moment there is no MySQL support, we would be delighted if you could add it)
* A Redis instance

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry for the confusion, indeed wrong use of the word "inherits".

@julik julik merged commit 65a65af into main Apr 6, 2024
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants