Add Edge Rate Limiting (ERL) Support - #45

Closed
posborne wants to merge 3 commits into
mainfrom
posborne/erl
Closed

Add Edge Rate Limiting (ERL) Support#45
posborne wants to merge 3 commits into
mainfrom
posborne/erl

Conversation

@posborne

Copy link
Copy Markdown
Member

This is a pretty direct wrapping of the WIT with the addition of The composite EdgeRateLimiter which matches what is provided by the Rust SDK with ERL.

Viceroy mostly has stubs for this functionality at this point in time, so the tests are not particularly substantive but do try to at least put some tracers through the hostcall boundary. There are several xfail tests which could be made to fail properly with viceory changes to do more faithfully match parts of the production impl.

This is a pretty direct wrapping of the WIT with the addition
of The composite `EdgeRateLimiter` which matches what is provided
by the Rust SDK with `ERL`.
Viceroy mostly has stubs for this functionality at this point in
time, so the tests are not particularly substantive but do try
to at least put some tracers through the hostcall boundary. There
are several xfail tests which could be made to fail properly
with viceory changes to do more faithfully match parts of the
production impl.
Comment threadfastly_compute/tests/test_erl.py Outdated
Lints for python 3.14 disallow string quoting types; importing
annotations from __future__ defers type avaluations to avoid this
problem and make the linter happy.

@erikroseerikrose left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good wrapper. Just a couple of nits noticed.

On notes of philosophy, since we were just talking about that…

This one's value is in __contains__ and in the docs. Otherwise, it wouldn't need to exist. I think it's fine to add this, but it's also worth spending an hour or two thinking about whether these (+ all their tests) need to exist in such verbose fashion. Maybe there's a slimmer way where we can "add only the additions".

Thanks, Paul! Enjoy your weekend!

Comment threadfastly_compute/erl.py
from fastly_compute.erl import RateCounter, PenaltyBox

# Basic rate limiting
with RateCounter.open("api-counter") as counter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great examples! :-D

Comment threadfastly_compute/erl.py
:param name: The name of the rate counter
:return: RateCounter instance
:raises ~fastly_compute.exceptions.types.open_error.NotFound: If the rate counter doesn't exist
:raises ~fastly_compute.exceptions.types.open_error.InvalidSyntax: If the name is invalid

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we know what constitutes a valid or too-long name? It'd be great to include those here so the descriptions actually convey some additional information.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I hadn't spelunked XQD on this one as of yet, these ended up being copied from another open but it appears that the validation here is different. The ERL specific validation seems limited, but I think the safer option might be to reference the common base class for open errors (probably as a general course).

The lack of enforcement explicitly for name lengths and the like on some of these might be indicative of an issue as I think this could be abused to cause excessive memory allocations on the host (@dgohman-fastly referred to this in passing today).

I'll look at this angle a bit more next week.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm glad you explained InvalidSyntax, as I couldn't have guessed its meaning in this context! But I wouldn't be at all opposed to adding "This may can also raise any other OpenError" or similar. That is, when it comes down to it, the spec enforced by the WIT.

Comment threadfastly_compute/erl.py

:param entry: Identifier for the client (e.g., IP address)
:param delta: Amount to increment the counter by
:param window: Time window in seconds for rate calculation. The host validates

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Booyah: thank you for including the units for all of these. That's a question I had while reading the WIT. We should backport that stuff to the WIT.

Comment threadfastly_compute/erl.py
def get_name(self) -> str:
"""Return the name of this penalty box.

:return: The name of the penalty box

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A couple of places we have a :return: that just restates the first line of the docstring. I think we can save people's time by deleting them.

Comment threadfastly_compute/erl.py
Combines a :class:`RateCounter` and :class:`PenaltyBox` into a single
interface for simplified rate limiting operations.

:param rate_counter: Rate counter to use for counting

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We needn't repeat what's in __init__().

}

@on_viceroy
def rate_counter_open(cls, name):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wouldn't say no to deleting this one and saving 3s on CI, as the next one does a superset of this. Just note in its docstring that it also tests opening. (However, if the retval should actually be used, disregard this ¶.)

The return value is unused; are you just smoketesting? If get_name() works under Viceroy, we ought to assert something about it.

"""Increment a counter and return None (no error)."""
with RateCounter.open(counter_name) as counter:
counter.increment(entry, delta)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need to explicitly return anything here, and there's no advantage in asserting that it returns None, as is done in test_increment(). If there were an error, it would raise an exception.

is_limited = self.rate_counter_check_rate(
"test-counter", "test-penalty", "192.168.1.1", 1, 10, 100, 300
)
assert is_limited is False # Viceroy stub returns False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the good comments about how Viceroy's stubs behave!

"""EdgeRateLimiter convenience wrapper tests."""

VICEROY_CONFIG = {
"local_server": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All 3 classes use similar fixtures. If we ever get sick of waiting for 3 different wasms to build, we could merge the classes or, fancier but more work, add some automatic test fixture coalescing. Back in the Django days, I had to write that, but I think pytest is smart enough to do it itself if we model it right.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll try to reduce these; initially I had hoped there might have been a bit more I could do with configuration to aid testing using Viceroy but that wasn't the case (I considered building that out but decided to avoid that trail for now).

"""Add entry to penalty box."""
with PenaltyBox.open(penalty_name) as penalty:
penalty.add(entry, ttl)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, the return None and matching assert can go.

Comment threadfastly_compute/erl.py
self.close()


class PenaltyBox:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I just realized this whole class can be replaced with…

classPenaltyBox(wit_erl.PenaltyBox):
def__contains__(self, entry: str) ->bool:
"""Check if entry is in the penalty box using the 'in' operator. :param entry: Identifier to check :return: True if the entry is blocked, False otherwise :raises ~fastly_compute.exceptions.types.error.InvalidArgument: If parameters are invalid :raises ~fastly_compute.exceptions.types.error.GenericError: If an unexpected error occurs Example:: with PenaltyBox.open("blocklist") as penalty: if "192.168.1.1" in penalty: return Response("Blocked", status=403) """returnself.has(entry)

…plus some Sphinx to add nicer docstrings to the other routines. The type hints in the stubs are even good. The only functional difference is that erl.PenaltyBox will have a has(), which isn't even a bad thing. I feel silly for not noticing this earlier.

Subclassing saves a bunch of indirection and makes the things we're actually adding stand out.

Comment threadfastly_compute/erl.py
from wit_world.imports import erl as wit_erl


class RateCounter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

After looking at PenaltyBox first, I realize this one is nothing but docs and can be turned into a bunch of RST.

@posborne

Copy link
Copy Markdown
MemberAuthor

Closing this for now but keeping the changes around; ERL from this base was included as part of the #84 changes, more heavily leaning on the generated code layer based on some of the discussion here.

I'm going to keep the branch around for now in case we abandon the approach proposed there.

@posborneposborne closed this Jun 1, 2026
Sign up for freeto 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

@posborne@erikrose
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add Edge Rate Limiting (ERL) Support - #45

Closed
posborne wants to merge 3 commits into
mainfrom
posborne/erl
Closed

Add Edge Rate Limiting (ERL) Support#45
posborne wants to merge 3 commits into
mainfrom
posborne/erl

Conversation

@posborne

Copy link
Copy Markdown
Member

This is a pretty direct wrapping of the WIT with the addition of The composite EdgeRateLimiter which matches what is provided by the Rust SDK with ERL.

Viceroy mostly has stubs for this functionality at this point in time, so the tests are not particularly substantive but do try to at least put some tracers through the hostcall boundary. There are several xfail tests which could be made to fail properly with viceory changes to do more faithfully match parts of the production impl.

This is a pretty direct wrapping of the WIT with the addition
of The composite `EdgeRateLimiter` which matches what is provided
by the Rust SDK with `ERL`.
Viceroy mostly has stubs for this functionality at this point in
time, so the tests are not particularly substantive but do try
to at least put some tracers through the hostcall boundary. There
are several xfail tests which could be made to fail properly
with viceory changes to do more faithfully match parts of the
production impl.
Comment threadfastly_compute/tests/test_erl.py Outdated
Lints for python 3.14 disallow string quoting types; importing
annotations from __future__ defers type avaluations to avoid this
problem and make the linter happy.

@erikroseerikrose left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good wrapper. Just a couple of nits noticed.

On notes of philosophy, since we were just talking about that…

This one's value is in __contains__ and in the docs. Otherwise, it wouldn't need to exist. I think it's fine to add this, but it's also worth spending an hour or two thinking about whether these (+ all their tests) need to exist in such verbose fashion. Maybe there's a slimmer way where we can "add only the additions".

Thanks, Paul! Enjoy your weekend!

Comment threadfastly_compute/erl.py
from fastly_compute.erl import RateCounter, PenaltyBox

# Basic rate limiting
with RateCounter.open("api-counter") as counter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great examples! :-D

Comment threadfastly_compute/erl.py
:param name: The name of the rate counter
:return: RateCounter instance
:raises ~fastly_compute.exceptions.types.open_error.NotFound: If the rate counter doesn't exist
:raises ~fastly_compute.exceptions.types.open_error.InvalidSyntax: If the name is invalid

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we know what constitutes a valid or too-long name? It'd be great to include those here so the descriptions actually convey some additional information.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I hadn't spelunked XQD on this one as of yet, these ended up being copied from another open but it appears that the validation here is different. The ERL specific validation seems limited, but I think the safer option might be to reference the common base class for open errors (probably as a general course).

The lack of enforcement explicitly for name lengths and the like on some of these might be indicative of an issue as I think this could be abused to cause excessive memory allocations on the host (@dgohman-fastly referred to this in passing today).

I'll look at this angle a bit more next week.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm glad you explained InvalidSyntax, as I couldn't have guessed its meaning in this context! But I wouldn't be at all opposed to adding "This may can also raise any other OpenError" or similar. That is, when it comes down to it, the spec enforced by the WIT.

Comment threadfastly_compute/erl.py

:param entry: Identifier for the client (e.g., IP address)
:param delta: Amount to increment the counter by
:param window: Time window in seconds for rate calculation. The host validates

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Booyah: thank you for including the units for all of these. That's a question I had while reading the WIT. We should backport that stuff to the WIT.

Comment threadfastly_compute/erl.py
def get_name(self) -> str:
"""Return the name of this penalty box.

:return: The name of the penalty box

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A couple of places we have a :return: that just restates the first line of the docstring. I think we can save people's time by deleting them.

Comment threadfastly_compute/erl.py
Combines a :class:`RateCounter` and :class:`PenaltyBox` into a single
interface for simplified rate limiting operations.

:param rate_counter: Rate counter to use for counting

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We needn't repeat what's in __init__().

}

@on_viceroy
def rate_counter_open(cls, name):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wouldn't say no to deleting this one and saving 3s on CI, as the next one does a superset of this. Just note in its docstring that it also tests opening. (However, if the retval should actually be used, disregard this ¶.)

The return value is unused; are you just smoketesting? If get_name() works under Viceroy, we ought to assert something about it.

"""Increment a counter and return None (no error)."""
with RateCounter.open(counter_name) as counter:
counter.increment(entry, delta)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need to explicitly return anything here, and there's no advantage in asserting that it returns None, as is done in test_increment(). If there were an error, it would raise an exception.

is_limited = self.rate_counter_check_rate(
"test-counter", "test-penalty", "192.168.1.1", 1, 10, 100, 300
)
assert is_limited is False # Viceroy stub returns False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the good comments about how Viceroy's stubs behave!

"""EdgeRateLimiter convenience wrapper tests."""

VICEROY_CONFIG = {
"local_server": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All 3 classes use similar fixtures. If we ever get sick of waiting for 3 different wasms to build, we could merge the classes or, fancier but more work, add some automatic test fixture coalescing. Back in the Django days, I had to write that, but I think pytest is smart enough to do it itself if we model it right.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll try to reduce these; initially I had hoped there might have been a bit more I could do with configuration to aid testing using Viceroy but that wasn't the case (I considered building that out but decided to avoid that trail for now).

"""Add entry to penalty box."""
with PenaltyBox.open(penalty_name) as penalty:
penalty.add(entry, ttl)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, the return None and matching assert can go.

Comment threadfastly_compute/erl.py
self.close()


class PenaltyBox:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I just realized this whole class can be replaced with…

classPenaltyBox(wit_erl.PenaltyBox):
def__contains__(self, entry: str) ->bool:
"""Check if entry is in the penalty box using the 'in' operator. :param entry: Identifier to check :return: True if the entry is blocked, False otherwise :raises ~fastly_compute.exceptions.types.error.InvalidArgument: If parameters are invalid :raises ~fastly_compute.exceptions.types.error.GenericError: If an unexpected error occurs Example:: with PenaltyBox.open("blocklist") as penalty: if "192.168.1.1" in penalty: return Response("Blocked", status=403) """returnself.has(entry)

…plus some Sphinx to add nicer docstrings to the other routines. The type hints in the stubs are even good. The only functional difference is that erl.PenaltyBox will have a has(), which isn't even a bad thing. I feel silly for not noticing this earlier.

Subclassing saves a bunch of indirection and makes the things we're actually adding stand out.

Comment threadfastly_compute/erl.py
from wit_world.imports import erl as wit_erl


class RateCounter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

After looking at PenaltyBox first, I realize this one is nothing but docs and can be turned into a bunch of RST.

@posborne

Copy link
Copy Markdown
MemberAuthor

Closing this for now but keeping the changes around; ERL from this base was included as part of the #84 changes, more heavily leaning on the generated code layer based on some of the discussion here.

I'm going to keep the branch around for now in case we abandon the approach proposed there.

@posborneposborne closed this Jun 1, 2026
Sign up for freeto 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

@posborne@erikrose
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Edge Rate Limiting (ERL) Support - #45

Closed
posborne wants to merge 3 commits into
mainfrom
posborne/erl
Closed

Add Edge Rate Limiting (ERL) Support#45
posborne wants to merge 3 commits into
mainfrom
posborne/erl

Conversation

@posborne

Copy link
Copy Markdown
Member

This is a pretty direct wrapping of the WIT with the addition of The composite EdgeRateLimiter which matches what is provided by the Rust SDK with ERL.

Viceroy mostly has stubs for this functionality at this point in time, so the tests are not particularly substantive but do try to at least put some tracers through the hostcall boundary. There are several xfail tests which could be made to fail properly with viceory changes to do more faithfully match parts of the production impl.

This is a pretty direct wrapping of the WIT with the addition
of The composite `EdgeRateLimiter` which matches what is provided
by the Rust SDK with `ERL`.
Viceroy mostly has stubs for this functionality at this point in
time, so the tests are not particularly substantive but do try
to at least put some tracers through the hostcall boundary. There
are several xfail tests which could be made to fail properly
with viceory changes to do more faithfully match parts of the
production impl.
Comment threadfastly_compute/tests/test_erl.py Outdated
Lints for python 3.14 disallow string quoting types; importing
annotations from __future__ defers type avaluations to avoid this
problem and make the linter happy.

@erikroseerikrose left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good wrapper. Just a couple of nits noticed.

On notes of philosophy, since we were just talking about that…

This one's value is in __contains__ and in the docs. Otherwise, it wouldn't need to exist. I think it's fine to add this, but it's also worth spending an hour or two thinking about whether these (+ all their tests) need to exist in such verbose fashion. Maybe there's a slimmer way where we can "add only the additions".

Thanks, Paul! Enjoy your weekend!

Comment threadfastly_compute/erl.py
from fastly_compute.erl import RateCounter, PenaltyBox

# Basic rate limiting
with RateCounter.open("api-counter") as counter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great examples! :-D

Comment threadfastly_compute/erl.py
:param name: The name of the rate counter
:return: RateCounter instance
:raises ~fastly_compute.exceptions.types.open_error.NotFound: If the rate counter doesn't exist
:raises ~fastly_compute.exceptions.types.open_error.InvalidSyntax: If the name is invalid

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we know what constitutes a valid or too-long name? It'd be great to include those here so the descriptions actually convey some additional information.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I hadn't spelunked XQD on this one as of yet, these ended up being copied from another open but it appears that the validation here is different. The ERL specific validation seems limited, but I think the safer option might be to reference the common base class for open errors (probably as a general course).

The lack of enforcement explicitly for name lengths and the like on some of these might be indicative of an issue as I think this could be abused to cause excessive memory allocations on the host (@dgohman-fastly referred to this in passing today).

I'll look at this angle a bit more next week.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm glad you explained InvalidSyntax, as I couldn't have guessed its meaning in this context! But I wouldn't be at all opposed to adding "This may can also raise any other OpenError" or similar. That is, when it comes down to it, the spec enforced by the WIT.

Comment threadfastly_compute/erl.py

:param entry: Identifier for the client (e.g., IP address)
:param delta: Amount to increment the counter by
:param window: Time window in seconds for rate calculation. The host validates

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Booyah: thank you for including the units for all of these. That's a question I had while reading the WIT. We should backport that stuff to the WIT.

Comment threadfastly_compute/erl.py
def get_name(self) -> str:
"""Return the name of this penalty box.

:return: The name of the penalty box

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A couple of places we have a :return: that just restates the first line of the docstring. I think we can save people's time by deleting them.

Comment threadfastly_compute/erl.py
Combines a :class:`RateCounter` and :class:`PenaltyBox` into a single
interface for simplified rate limiting operations.

:param rate_counter: Rate counter to use for counting

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We needn't repeat what's in __init__().

}

@on_viceroy
def rate_counter_open(cls, name):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wouldn't say no to deleting this one and saving 3s on CI, as the next one does a superset of this. Just note in its docstring that it also tests opening. (However, if the retval should actually be used, disregard this ¶.)

The return value is unused; are you just smoketesting? If get_name() works under Viceroy, we ought to assert something about it.

"""Increment a counter and return None (no error)."""
with RateCounter.open(counter_name) as counter:
counter.increment(entry, delta)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need to explicitly return anything here, and there's no advantage in asserting that it returns None, as is done in test_increment(). If there were an error, it would raise an exception.

is_limited = self.rate_counter_check_rate(
"test-counter", "test-penalty", "192.168.1.1", 1, 10, 100, 300
)
assert is_limited is False # Viceroy stub returns False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the good comments about how Viceroy's stubs behave!

"""EdgeRateLimiter convenience wrapper tests."""

VICEROY_CONFIG = {
"local_server": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All 3 classes use similar fixtures. If we ever get sick of waiting for 3 different wasms to build, we could merge the classes or, fancier but more work, add some automatic test fixture coalescing. Back in the Django days, I had to write that, but I think pytest is smart enough to do it itself if we model it right.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll try to reduce these; initially I had hoped there might have been a bit more I could do with configuration to aid testing using Viceroy but that wasn't the case (I considered building that out but decided to avoid that trail for now).

"""Add entry to penalty box."""
with PenaltyBox.open(penalty_name) as penalty:
penalty.add(entry, ttl)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, the return None and matching assert can go.

Comment threadfastly_compute/erl.py
self.close()


class PenaltyBox:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I just realized this whole class can be replaced with…

classPenaltyBox(wit_erl.PenaltyBox):
def__contains__(self, entry: str) ->bool:
"""Check if entry is in the penalty box using the 'in' operator. :param entry: Identifier to check :return: True if the entry is blocked, False otherwise :raises ~fastly_compute.exceptions.types.error.InvalidArgument: If parameters are invalid :raises ~fastly_compute.exceptions.types.error.GenericError: If an unexpected error occurs Example:: with PenaltyBox.open("blocklist") as penalty: if "192.168.1.1" in penalty: return Response("Blocked", status=403) """returnself.has(entry)

…plus some Sphinx to add nicer docstrings to the other routines. The type hints in the stubs are even good. The only functional difference is that erl.PenaltyBox will have a has(), which isn't even a bad thing. I feel silly for not noticing this earlier.

Subclassing saves a bunch of indirection and makes the things we're actually adding stand out.

Comment threadfastly_compute/erl.py
from wit_world.imports import erl as wit_erl


class RateCounter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

After looking at PenaltyBox first, I realize this one is nothing but docs and can be turned into a bunch of RST.

@posborne

Copy link
Copy Markdown
MemberAuthor

Closing this for now but keeping the changes around; ERL from this base was included as part of the #84 changes, more heavily leaning on the generated code layer based on some of the discussion here.

I'm going to keep the branch around for now in case we abandon the approach proposed there.

@posborneposborne closed this Jun 1, 2026
Sign up for freeto 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

@posborne@erikrose
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Edge Rate Limiting (ERL) Support - #45

Closed
posborne wants to merge 3 commits into
mainfrom
posborne/erl
Closed

Add Edge Rate Limiting (ERL) Support#45
posborne wants to merge 3 commits into
mainfrom
posborne/erl

Conversation

@posborne

Copy link
Copy Markdown
Member

This is a pretty direct wrapping of the WIT with the addition of The composite EdgeRateLimiter which matches what is provided by the Rust SDK with ERL.

Viceroy mostly has stubs for this functionality at this point in time, so the tests are not particularly substantive but do try to at least put some tracers through the hostcall boundary. There are several xfail tests which could be made to fail properly with viceory changes to do more faithfully match parts of the production impl.

This is a pretty direct wrapping of the WIT with the addition
of The composite `EdgeRateLimiter` which matches what is provided
by the Rust SDK with `ERL`.
Viceroy mostly has stubs for this functionality at this point in
time, so the tests are not particularly substantive but do try
to at least put some tracers through the hostcall boundary. There
are several xfail tests which could be made to fail properly
with viceory changes to do more faithfully match parts of the
production impl.
Comment threadfastly_compute/tests/test_erl.py Outdated
Lints for python 3.14 disallow string quoting types; importing
annotations from __future__ defers type avaluations to avoid this
problem and make the linter happy.

@erikroseerikrose left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good wrapper. Just a couple of nits noticed.

On notes of philosophy, since we were just talking about that…

This one's value is in __contains__ and in the docs. Otherwise, it wouldn't need to exist. I think it's fine to add this, but it's also worth spending an hour or two thinking about whether these (+ all their tests) need to exist in such verbose fashion. Maybe there's a slimmer way where we can "add only the additions".

Thanks, Paul! Enjoy your weekend!

Comment threadfastly_compute/erl.py
from fastly_compute.erl import RateCounter, PenaltyBox

# Basic rate limiting
with RateCounter.open("api-counter") as counter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great examples! :-D

Comment threadfastly_compute/erl.py
:param name: The name of the rate counter
:return: RateCounter instance
:raises ~fastly_compute.exceptions.types.open_error.NotFound: If the rate counter doesn't exist
:raises ~fastly_compute.exceptions.types.open_error.InvalidSyntax: If the name is invalid

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we know what constitutes a valid or too-long name? It'd be great to include those here so the descriptions actually convey some additional information.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I hadn't spelunked XQD on this one as of yet, these ended up being copied from another open but it appears that the validation here is different. The ERL specific validation seems limited, but I think the safer option might be to reference the common base class for open errors (probably as a general course).

The lack of enforcement explicitly for name lengths and the like on some of these might be indicative of an issue as I think this could be abused to cause excessive memory allocations on the host (@dgohman-fastly referred to this in passing today).

I'll look at this angle a bit more next week.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm glad you explained InvalidSyntax, as I couldn't have guessed its meaning in this context! But I wouldn't be at all opposed to adding "This may can also raise any other OpenError" or similar. That is, when it comes down to it, the spec enforced by the WIT.

Comment threadfastly_compute/erl.py

:param entry: Identifier for the client (e.g., IP address)
:param delta: Amount to increment the counter by
:param window: Time window in seconds for rate calculation. The host validates

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Booyah: thank you for including the units for all of these. That's a question I had while reading the WIT. We should backport that stuff to the WIT.

Comment threadfastly_compute/erl.py
def get_name(self) -> str:
"""Return the name of this penalty box.

:return: The name of the penalty box

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A couple of places we have a :return: that just restates the first line of the docstring. I think we can save people's time by deleting them.

Comment threadfastly_compute/erl.py
Combines a :class:`RateCounter` and :class:`PenaltyBox` into a single
interface for simplified rate limiting operations.

:param rate_counter: Rate counter to use for counting

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We needn't repeat what's in __init__().

}

@on_viceroy
def rate_counter_open(cls, name):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wouldn't say no to deleting this one and saving 3s on CI, as the next one does a superset of this. Just note in its docstring that it also tests opening. (However, if the retval should actually be used, disregard this ¶.)

The return value is unused; are you just smoketesting? If get_name() works under Viceroy, we ought to assert something about it.

"""Increment a counter and return None (no error)."""
with RateCounter.open(counter_name) as counter:
counter.increment(entry, delta)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need to explicitly return anything here, and there's no advantage in asserting that it returns None, as is done in test_increment(). If there were an error, it would raise an exception.

is_limited = self.rate_counter_check_rate(
"test-counter", "test-penalty", "192.168.1.1", 1, 10, 100, 300
)
assert is_limited is False # Viceroy stub returns False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the good comments about how Viceroy's stubs behave!

"""EdgeRateLimiter convenience wrapper tests."""

VICEROY_CONFIG = {
"local_server": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All 3 classes use similar fixtures. If we ever get sick of waiting for 3 different wasms to build, we could merge the classes or, fancier but more work, add some automatic test fixture coalescing. Back in the Django days, I had to write that, but I think pytest is smart enough to do it itself if we model it right.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll try to reduce these; initially I had hoped there might have been a bit more I could do with configuration to aid testing using Viceroy but that wasn't the case (I considered building that out but decided to avoid that trail for now).

"""Add entry to penalty box."""
with PenaltyBox.open(penalty_name) as penalty:
penalty.add(entry, ttl)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, the return None and matching assert can go.

Comment threadfastly_compute/erl.py
self.close()


class PenaltyBox:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I just realized this whole class can be replaced with…

classPenaltyBox(wit_erl.PenaltyBox):
def__contains__(self, entry: str) ->bool:
"""Check if entry is in the penalty box using the 'in' operator. :param entry: Identifier to check :return: True if the entry is blocked, False otherwise :raises ~fastly_compute.exceptions.types.error.InvalidArgument: If parameters are invalid :raises ~fastly_compute.exceptions.types.error.GenericError: If an unexpected error occurs Example:: with PenaltyBox.open("blocklist") as penalty: if "192.168.1.1" in penalty: return Response("Blocked", status=403) """returnself.has(entry)

…plus some Sphinx to add nicer docstrings to the other routines. The type hints in the stubs are even good. The only functional difference is that erl.PenaltyBox will have a has(), which isn't even a bad thing. I feel silly for not noticing this earlier.

Subclassing saves a bunch of indirection and makes the things we're actually adding stand out.

Comment threadfastly_compute/erl.py
from wit_world.imports import erl as wit_erl


class RateCounter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

After looking at PenaltyBox first, I realize this one is nothing but docs and can be turned into a bunch of RST.

@posborne

Copy link
Copy Markdown
MemberAuthor

Closing this for now but keeping the changes around; ERL from this base was included as part of the #84 changes, more heavily leaning on the generated code layer based on some of the discussion here.

I'm going to keep the branch around for now in case we abandon the approach proposed there.

@posborneposborne closed this Jun 1, 2026
Sign up for freeto 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

@posborne@erikrose
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add Edge Rate Limiting (ERL) Support - #45

Closed
posborne wants to merge 3 commits into
mainfrom
posborne/erl
Closed

Add Edge Rate Limiting (ERL) Support#45
posborne wants to merge 3 commits into
mainfrom
posborne/erl

Conversation

@posborne

Copy link
Copy Markdown
Member

This is a pretty direct wrapping of the WIT with the addition of The composite EdgeRateLimiter which matches what is provided by the Rust SDK with ERL.

Viceroy mostly has stubs for this functionality at this point in time, so the tests are not particularly substantive but do try to at least put some tracers through the hostcall boundary. There are several xfail tests which could be made to fail properly with viceory changes to do more faithfully match parts of the production impl.

This is a pretty direct wrapping of the WIT with the addition
of The composite `EdgeRateLimiter` which matches what is provided
by the Rust SDK with `ERL`.
Viceroy mostly has stubs for this functionality at this point in
time, so the tests are not particularly substantive but do try
to at least put some tracers through the hostcall boundary. There
are several xfail tests which could be made to fail properly
with viceory changes to do more faithfully match parts of the
production impl.
Comment threadfastly_compute/tests/test_erl.py Outdated
Lints for python 3.14 disallow string quoting types; importing
annotations from __future__ defers type avaluations to avoid this
problem and make the linter happy.

@erikroseerikrose left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good wrapper. Just a couple of nits noticed.

On notes of philosophy, since we were just talking about that…

This one's value is in __contains__ and in the docs. Otherwise, it wouldn't need to exist. I think it's fine to add this, but it's also worth spending an hour or two thinking about whether these (+ all their tests) need to exist in such verbose fashion. Maybe there's a slimmer way where we can "add only the additions".

Thanks, Paul! Enjoy your weekend!

Comment threadfastly_compute/erl.py
from fastly_compute.erl import RateCounter, PenaltyBox

# Basic rate limiting
with RateCounter.open("api-counter") as counter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great examples! :-D

Comment threadfastly_compute/erl.py
:param name: The name of the rate counter
:return: RateCounter instance
:raises ~fastly_compute.exceptions.types.open_error.NotFound: If the rate counter doesn't exist
:raises ~fastly_compute.exceptions.types.open_error.InvalidSyntax: If the name is invalid

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we know what constitutes a valid or too-long name? It'd be great to include those here so the descriptions actually convey some additional information.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I hadn't spelunked XQD on this one as of yet, these ended up being copied from another open but it appears that the validation here is different. The ERL specific validation seems limited, but I think the safer option might be to reference the common base class for open errors (probably as a general course).

The lack of enforcement explicitly for name lengths and the like on some of these might be indicative of an issue as I think this could be abused to cause excessive memory allocations on the host (@dgohman-fastly referred to this in passing today).

I'll look at this angle a bit more next week.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm glad you explained InvalidSyntax, as I couldn't have guessed its meaning in this context! But I wouldn't be at all opposed to adding "This may can also raise any other OpenError" or similar. That is, when it comes down to it, the spec enforced by the WIT.

Comment threadfastly_compute/erl.py

:param entry: Identifier for the client (e.g., IP address)
:param delta: Amount to increment the counter by
:param window: Time window in seconds for rate calculation. The host validates

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Booyah: thank you for including the units for all of these. That's a question I had while reading the WIT. We should backport that stuff to the WIT.

Comment threadfastly_compute/erl.py
def get_name(self) -> str:
"""Return the name of this penalty box.

:return: The name of the penalty box

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A couple of places we have a :return: that just restates the first line of the docstring. I think we can save people's time by deleting them.

Comment threadfastly_compute/erl.py
Combines a :class:`RateCounter` and :class:`PenaltyBox` into a single
interface for simplified rate limiting operations.

:param rate_counter: Rate counter to use for counting

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We needn't repeat what's in __init__().

}

@on_viceroy
def rate_counter_open(cls, name):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wouldn't say no to deleting this one and saving 3s on CI, as the next one does a superset of this. Just note in its docstring that it also tests opening. (However, if the retval should actually be used, disregard this ¶.)

The return value is unused; are you just smoketesting? If get_name() works under Viceroy, we ought to assert something about it.

"""Increment a counter and return None (no error)."""
with RateCounter.open(counter_name) as counter:
counter.increment(entry, delta)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need to explicitly return anything here, and there's no advantage in asserting that it returns None, as is done in test_increment(). If there were an error, it would raise an exception.

is_limited = self.rate_counter_check_rate(
"test-counter", "test-penalty", "192.168.1.1", 1, 10, 100, 300
)
assert is_limited is False # Viceroy stub returns False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the good comments about how Viceroy's stubs behave!

"""EdgeRateLimiter convenience wrapper tests."""

VICEROY_CONFIG = {
"local_server": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All 3 classes use similar fixtures. If we ever get sick of waiting for 3 different wasms to build, we could merge the classes or, fancier but more work, add some automatic test fixture coalescing. Back in the Django days, I had to write that, but I think pytest is smart enough to do it itself if we model it right.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll try to reduce these; initially I had hoped there might have been a bit more I could do with configuration to aid testing using Viceroy but that wasn't the case (I considered building that out but decided to avoid that trail for now).

"""Add entry to penalty box."""
with PenaltyBox.open(penalty_name) as penalty:
penalty.add(entry, ttl)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, the return None and matching assert can go.

Comment threadfastly_compute/erl.py
self.close()


class PenaltyBox:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I just realized this whole class can be replaced with…

classPenaltyBox(wit_erl.PenaltyBox):
def__contains__(self, entry: str) ->bool:
"""Check if entry is in the penalty box using the 'in' operator. :param entry: Identifier to check :return: True if the entry is blocked, False otherwise :raises ~fastly_compute.exceptions.types.error.InvalidArgument: If parameters are invalid :raises ~fastly_compute.exceptions.types.error.GenericError: If an unexpected error occurs Example:: with PenaltyBox.open("blocklist") as penalty: if "192.168.1.1" in penalty: return Response("Blocked", status=403) """returnself.has(entry)

…plus some Sphinx to add nicer docstrings to the other routines. The type hints in the stubs are even good. The only functional difference is that erl.PenaltyBox will have a has(), which isn't even a bad thing. I feel silly for not noticing this earlier.

Subclassing saves a bunch of indirection and makes the things we're actually adding stand out.

Comment threadfastly_compute/erl.py
from wit_world.imports import erl as wit_erl


class RateCounter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

After looking at PenaltyBox first, I realize this one is nothing but docs and can be turned into a bunch of RST.

@posborne

Copy link
Copy Markdown
MemberAuthor

Closing this for now but keeping the changes around; ERL from this base was included as part of the #84 changes, more heavily leaning on the generated code layer based on some of the discussion here.

I'm going to keep the branch around for now in case we abandon the approach proposed there.

@posborneposborne closed this Jun 1, 2026
Sign up for freeto 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

@posborne@erikrose
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Edge Rate Limiting (ERL) Support - #45

Closed
posborne wants to merge 3 commits into
mainfrom
posborne/erl
Closed

Add Edge Rate Limiting (ERL) Support#45
posborne wants to merge 3 commits into
mainfrom
posborne/erl

Conversation

@posborne

Copy link
Copy Markdown
Member

This is a pretty direct wrapping of the WIT with the addition of The composite EdgeRateLimiter which matches what is provided by the Rust SDK with ERL.

Viceroy mostly has stubs for this functionality at this point in time, so the tests are not particularly substantive but do try to at least put some tracers through the hostcall boundary. There are several xfail tests which could be made to fail properly with viceory changes to do more faithfully match parts of the production impl.

This is a pretty direct wrapping of the WIT with the addition
of The composite `EdgeRateLimiter` which matches what is provided
by the Rust SDK with `ERL`.
Viceroy mostly has stubs for this functionality at this point in
time, so the tests are not particularly substantive but do try
to at least put some tracers through the hostcall boundary. There
are several xfail tests which could be made to fail properly
with viceory changes to do more faithfully match parts of the
production impl.
Comment threadfastly_compute/tests/test_erl.py Outdated
Lints for python 3.14 disallow string quoting types; importing
annotations from __future__ defers type avaluations to avoid this
problem and make the linter happy.

@erikroseerikrose left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good wrapper. Just a couple of nits noticed.

On notes of philosophy, since we were just talking about that…

This one's value is in __contains__ and in the docs. Otherwise, it wouldn't need to exist. I think it's fine to add this, but it's also worth spending an hour or two thinking about whether these (+ all their tests) need to exist in such verbose fashion. Maybe there's a slimmer way where we can "add only the additions".

Thanks, Paul! Enjoy your weekend!

Comment threadfastly_compute/erl.py
from fastly_compute.erl import RateCounter, PenaltyBox

# Basic rate limiting
with RateCounter.open("api-counter") as counter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great examples! :-D

Comment threadfastly_compute/erl.py
:param name: The name of the rate counter
:return: RateCounter instance
:raises ~fastly_compute.exceptions.types.open_error.NotFound: If the rate counter doesn't exist
:raises ~fastly_compute.exceptions.types.open_error.InvalidSyntax: If the name is invalid

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we know what constitutes a valid or too-long name? It'd be great to include those here so the descriptions actually convey some additional information.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I hadn't spelunked XQD on this one as of yet, these ended up being copied from another open but it appears that the validation here is different. The ERL specific validation seems limited, but I think the safer option might be to reference the common base class for open errors (probably as a general course).

The lack of enforcement explicitly for name lengths and the like on some of these might be indicative of an issue as I think this could be abused to cause excessive memory allocations on the host (@dgohman-fastly referred to this in passing today).

I'll look at this angle a bit more next week.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm glad you explained InvalidSyntax, as I couldn't have guessed its meaning in this context! But I wouldn't be at all opposed to adding "This may can also raise any other OpenError" or similar. That is, when it comes down to it, the spec enforced by the WIT.

Comment threadfastly_compute/erl.py

:param entry: Identifier for the client (e.g., IP address)
:param delta: Amount to increment the counter by
:param window: Time window in seconds for rate calculation. The host validates

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Booyah: thank you for including the units for all of these. That's a question I had while reading the WIT. We should backport that stuff to the WIT.

Comment threadfastly_compute/erl.py
def get_name(self) -> str:
"""Return the name of this penalty box.

:return: The name of the penalty box

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A couple of places we have a :return: that just restates the first line of the docstring. I think we can save people's time by deleting them.

Comment threadfastly_compute/erl.py
Combines a :class:`RateCounter` and :class:`PenaltyBox` into a single
interface for simplified rate limiting operations.

:param rate_counter: Rate counter to use for counting

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We needn't repeat what's in __init__().

}

@on_viceroy
def rate_counter_open(cls, name):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wouldn't say no to deleting this one and saving 3s on CI, as the next one does a superset of this. Just note in its docstring that it also tests opening. (However, if the retval should actually be used, disregard this ¶.)

The return value is unused; are you just smoketesting? If get_name() works under Viceroy, we ought to assert something about it.

"""Increment a counter and return None (no error)."""
with RateCounter.open(counter_name) as counter:
counter.increment(entry, delta)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need to explicitly return anything here, and there's no advantage in asserting that it returns None, as is done in test_increment(). If there were an error, it would raise an exception.

is_limited = self.rate_counter_check_rate(
"test-counter", "test-penalty", "192.168.1.1", 1, 10, 100, 300
)
assert is_limited is False # Viceroy stub returns False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the good comments about how Viceroy's stubs behave!

"""EdgeRateLimiter convenience wrapper tests."""

VICEROY_CONFIG = {
"local_server": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All 3 classes use similar fixtures. If we ever get sick of waiting for 3 different wasms to build, we could merge the classes or, fancier but more work, add some automatic test fixture coalescing. Back in the Django days, I had to write that, but I think pytest is smart enough to do it itself if we model it right.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll try to reduce these; initially I had hoped there might have been a bit more I could do with configuration to aid testing using Viceroy but that wasn't the case (I considered building that out but decided to avoid that trail for now).

"""Add entry to penalty box."""
with PenaltyBox.open(penalty_name) as penalty:
penalty.add(entry, ttl)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, the return None and matching assert can go.

Comment threadfastly_compute/erl.py
self.close()


class PenaltyBox:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I just realized this whole class can be replaced with…

classPenaltyBox(wit_erl.PenaltyBox):
def__contains__(self, entry: str) ->bool:
"""Check if entry is in the penalty box using the 'in' operator. :param entry: Identifier to check :return: True if the entry is blocked, False otherwise :raises ~fastly_compute.exceptions.types.error.InvalidArgument: If parameters are invalid :raises ~fastly_compute.exceptions.types.error.GenericError: If an unexpected error occurs Example:: with PenaltyBox.open("blocklist") as penalty: if "192.168.1.1" in penalty: return Response("Blocked", status=403) """returnself.has(entry)

…plus some Sphinx to add nicer docstrings to the other routines. The type hints in the stubs are even good. The only functional difference is that erl.PenaltyBox will have a has(), which isn't even a bad thing. I feel silly for not noticing this earlier.

Subclassing saves a bunch of indirection and makes the things we're actually adding stand out.

Comment threadfastly_compute/erl.py
from wit_world.imports import erl as wit_erl


class RateCounter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

After looking at PenaltyBox first, I realize this one is nothing but docs and can be turned into a bunch of RST.

@posborne

Copy link
Copy Markdown
MemberAuthor

Closing this for now but keeping the changes around; ERL from this base was included as part of the #84 changes, more heavily leaning on the generated code layer based on some of the discussion here.

I'm going to keep the branch around for now in case we abandon the approach proposed there.

@posborneposborne closed this Jun 1, 2026
Sign up for freeto 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

@posborne@erikrose
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Edge Rate Limiting (ERL) Support - #45

Closed
posborne wants to merge 3 commits into
mainfrom
posborne/erl
Closed

Add Edge Rate Limiting (ERL) Support#45
posborne wants to merge 3 commits into
mainfrom
posborne/erl

Conversation

@posborne

Copy link
Copy Markdown
Member

This is a pretty direct wrapping of the WIT with the addition of The composite EdgeRateLimiter which matches what is provided by the Rust SDK with ERL.

Viceroy mostly has stubs for this functionality at this point in time, so the tests are not particularly substantive but do try to at least put some tracers through the hostcall boundary. There are several xfail tests which could be made to fail properly with viceory changes to do more faithfully match parts of the production impl.

This is a pretty direct wrapping of the WIT with the addition
of The composite `EdgeRateLimiter` which matches what is provided
by the Rust SDK with `ERL`.
Viceroy mostly has stubs for this functionality at this point in
time, so the tests are not particularly substantive but do try
to at least put some tracers through the hostcall boundary. There
are several xfail tests which could be made to fail properly
with viceory changes to do more faithfully match parts of the
production impl.
Comment threadfastly_compute/tests/test_erl.py Outdated
Lints for python 3.14 disallow string quoting types; importing
annotations from __future__ defers type avaluations to avoid this
problem and make the linter happy.

@erikroseerikrose left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good wrapper. Just a couple of nits noticed.

On notes of philosophy, since we were just talking about that…

This one's value is in __contains__ and in the docs. Otherwise, it wouldn't need to exist. I think it's fine to add this, but it's also worth spending an hour or two thinking about whether these (+ all their tests) need to exist in such verbose fashion. Maybe there's a slimmer way where we can "add only the additions".

Thanks, Paul! Enjoy your weekend!

Comment threadfastly_compute/erl.py
from fastly_compute.erl import RateCounter, PenaltyBox

# Basic rate limiting
with RateCounter.open("api-counter") as counter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great examples! :-D

Comment threadfastly_compute/erl.py
:param name: The name of the rate counter
:return: RateCounter instance
:raises ~fastly_compute.exceptions.types.open_error.NotFound: If the rate counter doesn't exist
:raises ~fastly_compute.exceptions.types.open_error.InvalidSyntax: If the name is invalid

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we know what constitutes a valid or too-long name? It'd be great to include those here so the descriptions actually convey some additional information.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I hadn't spelunked XQD on this one as of yet, these ended up being copied from another open but it appears that the validation here is different. The ERL specific validation seems limited, but I think the safer option might be to reference the common base class for open errors (probably as a general course).

The lack of enforcement explicitly for name lengths and the like on some of these might be indicative of an issue as I think this could be abused to cause excessive memory allocations on the host (@dgohman-fastly referred to this in passing today).

I'll look at this angle a bit more next week.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm glad you explained InvalidSyntax, as I couldn't have guessed its meaning in this context! But I wouldn't be at all opposed to adding "This may can also raise any other OpenError" or similar. That is, when it comes down to it, the spec enforced by the WIT.

Comment threadfastly_compute/erl.py

:param entry: Identifier for the client (e.g., IP address)
:param delta: Amount to increment the counter by
:param window: Time window in seconds for rate calculation. The host validates

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Booyah: thank you for including the units for all of these. That's a question I had while reading the WIT. We should backport that stuff to the WIT.

Comment threadfastly_compute/erl.py
def get_name(self) -> str:
"""Return the name of this penalty box.

:return: The name of the penalty box

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A couple of places we have a :return: that just restates the first line of the docstring. I think we can save people's time by deleting them.

Comment threadfastly_compute/erl.py
Combines a :class:`RateCounter` and :class:`PenaltyBox` into a single
interface for simplified rate limiting operations.

:param rate_counter: Rate counter to use for counting

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We needn't repeat what's in __init__().

}

@on_viceroy
def rate_counter_open(cls, name):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wouldn't say no to deleting this one and saving 3s on CI, as the next one does a superset of this. Just note in its docstring that it also tests opening. (However, if the retval should actually be used, disregard this ¶.)

The return value is unused; are you just smoketesting? If get_name() works under Viceroy, we ought to assert something about it.

"""Increment a counter and return None (no error)."""
with RateCounter.open(counter_name) as counter:
counter.increment(entry, delta)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need to explicitly return anything here, and there's no advantage in asserting that it returns None, as is done in test_increment(). If there were an error, it would raise an exception.

is_limited = self.rate_counter_check_rate(
"test-counter", "test-penalty", "192.168.1.1", 1, 10, 100, 300
)
assert is_limited is False # Viceroy stub returns False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the good comments about how Viceroy's stubs behave!

"""EdgeRateLimiter convenience wrapper tests."""

VICEROY_CONFIG = {
"local_server": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All 3 classes use similar fixtures. If we ever get sick of waiting for 3 different wasms to build, we could merge the classes or, fancier but more work, add some automatic test fixture coalescing. Back in the Django days, I had to write that, but I think pytest is smart enough to do it itself if we model it right.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll try to reduce these; initially I had hoped there might have been a bit more I could do with configuration to aid testing using Viceroy but that wasn't the case (I considered building that out but decided to avoid that trail for now).

"""Add entry to penalty box."""
with PenaltyBox.open(penalty_name) as penalty:
penalty.add(entry, ttl)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, the return None and matching assert can go.

Comment threadfastly_compute/erl.py
self.close()


class PenaltyBox:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I just realized this whole class can be replaced with…

classPenaltyBox(wit_erl.PenaltyBox):
def__contains__(self, entry: str) ->bool:
"""Check if entry is in the penalty box using the 'in' operator. :param entry: Identifier to check :return: True if the entry is blocked, False otherwise :raises ~fastly_compute.exceptions.types.error.InvalidArgument: If parameters are invalid :raises ~fastly_compute.exceptions.types.error.GenericError: If an unexpected error occurs Example:: with PenaltyBox.open("blocklist") as penalty: if "192.168.1.1" in penalty: return Response("Blocked", status=403) """returnself.has(entry)

…plus some Sphinx to add nicer docstrings to the other routines. The type hints in the stubs are even good. The only functional difference is that erl.PenaltyBox will have a has(), which isn't even a bad thing. I feel silly for not noticing this earlier.

Subclassing saves a bunch of indirection and makes the things we're actually adding stand out.

Comment threadfastly_compute/erl.py
from wit_world.imports import erl as wit_erl


class RateCounter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

After looking at PenaltyBox first, I realize this one is nothing but docs and can be turned into a bunch of RST.

@posborne

Copy link
Copy Markdown
MemberAuthor

Closing this for now but keeping the changes around; ERL from this base was included as part of the #84 changes, more heavily leaning on the generated code layer based on some of the discussion here.

I'm going to keep the branch around for now in case we abandon the approach proposed there.

@posborneposborne closed this Jun 1, 2026
Sign up for freeto 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

@posborne@erikrose
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add Edge Rate Limiting (ERL) Support - #45

Closed
posborne wants to merge 3 commits into
mainfrom
posborne/erl
Closed

Add Edge Rate Limiting (ERL) Support#45
posborne wants to merge 3 commits into
mainfrom
posborne/erl

Conversation

@posborne

Copy link
Copy Markdown
Member

This is a pretty direct wrapping of the WIT with the addition of The composite EdgeRateLimiter which matches what is provided by the Rust SDK with ERL.

Viceroy mostly has stubs for this functionality at this point in time, so the tests are not particularly substantive but do try to at least put some tracers through the hostcall boundary. There are several xfail tests which could be made to fail properly with viceory changes to do more faithfully match parts of the production impl.

This is a pretty direct wrapping of the WIT with the addition
of The composite `EdgeRateLimiter` which matches what is provided
by the Rust SDK with `ERL`.
Viceroy mostly has stubs for this functionality at this point in
time, so the tests are not particularly substantive but do try
to at least put some tracers through the hostcall boundary. There
are several xfail tests which could be made to fail properly
with viceory changes to do more faithfully match parts of the
production impl.
Comment threadfastly_compute/tests/test_erl.py Outdated
Lints for python 3.14 disallow string quoting types; importing
annotations from __future__ defers type avaluations to avoid this
problem and make the linter happy.

@erikroseerikrose left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good wrapper. Just a couple of nits noticed.

On notes of philosophy, since we were just talking about that…

This one's value is in __contains__ and in the docs. Otherwise, it wouldn't need to exist. I think it's fine to add this, but it's also worth spending an hour or two thinking about whether these (+ all their tests) need to exist in such verbose fashion. Maybe there's a slimmer way where we can "add only the additions".

Thanks, Paul! Enjoy your weekend!

Comment threadfastly_compute/erl.py
from fastly_compute.erl import RateCounter, PenaltyBox

# Basic rate limiting
with RateCounter.open("api-counter") as counter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great examples! :-D

Comment threadfastly_compute/erl.py
:param name: The name of the rate counter
:return: RateCounter instance
:raises ~fastly_compute.exceptions.types.open_error.NotFound: If the rate counter doesn't exist
:raises ~fastly_compute.exceptions.types.open_error.InvalidSyntax: If the name is invalid

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we know what constitutes a valid or too-long name? It'd be great to include those here so the descriptions actually convey some additional information.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I hadn't spelunked XQD on this one as of yet, these ended up being copied from another open but it appears that the validation here is different. The ERL specific validation seems limited, but I think the safer option might be to reference the common base class for open errors (probably as a general course).

The lack of enforcement explicitly for name lengths and the like on some of these might be indicative of an issue as I think this could be abused to cause excessive memory allocations on the host (@dgohman-fastly referred to this in passing today).

I'll look at this angle a bit more next week.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm glad you explained InvalidSyntax, as I couldn't have guessed its meaning in this context! But I wouldn't be at all opposed to adding "This may can also raise any other OpenError" or similar. That is, when it comes down to it, the spec enforced by the WIT.

Comment threadfastly_compute/erl.py

:param entry: Identifier for the client (e.g., IP address)
:param delta: Amount to increment the counter by
:param window: Time window in seconds for rate calculation. The host validates

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Booyah: thank you for including the units for all of these. That's a question I had while reading the WIT. We should backport that stuff to the WIT.

Comment threadfastly_compute/erl.py
def get_name(self) -> str:
"""Return the name of this penalty box.

:return: The name of the penalty box

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A couple of places we have a :return: that just restates the first line of the docstring. I think we can save people's time by deleting them.

Comment threadfastly_compute/erl.py
Combines a :class:`RateCounter` and :class:`PenaltyBox` into a single
interface for simplified rate limiting operations.

:param rate_counter: Rate counter to use for counting

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We needn't repeat what's in __init__().

}

@on_viceroy
def rate_counter_open(cls, name):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wouldn't say no to deleting this one and saving 3s on CI, as the next one does a superset of this. Just note in its docstring that it also tests opening. (However, if the retval should actually be used, disregard this ¶.)

The return value is unused; are you just smoketesting? If get_name() works under Viceroy, we ought to assert something about it.

"""Increment a counter and return None (no error)."""
with RateCounter.open(counter_name) as counter:
counter.increment(entry, delta)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need to explicitly return anything here, and there's no advantage in asserting that it returns None, as is done in test_increment(). If there were an error, it would raise an exception.

is_limited = self.rate_counter_check_rate(
"test-counter", "test-penalty", "192.168.1.1", 1, 10, 100, 300
)
assert is_limited is False # Viceroy stub returns False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the good comments about how Viceroy's stubs behave!

"""EdgeRateLimiter convenience wrapper tests."""

VICEROY_CONFIG = {
"local_server": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All 3 classes use similar fixtures. If we ever get sick of waiting for 3 different wasms to build, we could merge the classes or, fancier but more work, add some automatic test fixture coalescing. Back in the Django days, I had to write that, but I think pytest is smart enough to do it itself if we model it right.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I'll try to reduce these; initially I had hoped there might have been a bit more I could do with configuration to aid testing using Viceroy but that wasn't the case (I considered building that out but decided to avoid that trail for now).

"""Add entry to penalty box."""
with PenaltyBox.open(penalty_name) as penalty:
penalty.add(entry, ttl)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, the return None and matching assert can go.

Comment threadfastly_compute/erl.py
self.close()


class PenaltyBox:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I just realized this whole class can be replaced with…

classPenaltyBox(wit_erl.PenaltyBox):
def__contains__(self, entry: str) ->bool:
"""Check if entry is in the penalty box using the 'in' operator. :param entry: Identifier to check :return: True if the entry is blocked, False otherwise :raises ~fastly_compute.exceptions.types.error.InvalidArgument: If parameters are invalid :raises ~fastly_compute.exceptions.types.error.GenericError: If an unexpected error occurs Example:: with PenaltyBox.open("blocklist") as penalty: if "192.168.1.1" in penalty: return Response("Blocked", status=403) """returnself.has(entry)

…plus some Sphinx to add nicer docstrings to the other routines. The type hints in the stubs are even good. The only functional difference is that erl.PenaltyBox will have a has(), which isn't even a bad thing. I feel silly for not noticing this earlier.

Subclassing saves a bunch of indirection and makes the things we're actually adding stand out.

Comment threadfastly_compute/erl.py
from wit_world.imports import erl as wit_erl


class RateCounter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

After looking at PenaltyBox first, I realize this one is nothing but docs and can be turned into a bunch of RST.

@posborne

Copy link
Copy Markdown
MemberAuthor

Closing this for now but keeping the changes around; ERL from this base was included as part of the #84 changes, more heavily leaning on the generated code layer based on some of the discussion here.

I'm going to keep the branch around for now in case we abandon the approach proposed there.

@posborneposborne closed this Jun 1, 2026
Sign up for freeto 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

@posborne@erikrose