-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
feat(utils/sync): Add sync.Once utility functions #13284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Test Results (amd64) - Integration, Bootstrap 5 files 5 suites 2h 39m 37s ⏱️ Results for commit 3e99d09. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is neat! I like the design and the tests make it clear how it works. Having a thread-safe way to ensure something only runs once seems useful and I am surprised to see Golang has a feature that Python does not have 😆
suggestion: I think it'd be worth already using it somewhere in this PR to show a real use case or why it’s needed. That would help demonstrate its value in context.
|
|
||
| def once(fn: Callable[..., T]) -> Callable[..., T | None]: | ||
| """ | ||
| A decorator that ensures a function is over ever executed once. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| A decorator that ensures a function is over ever executed once. | |
| A decorator that ensures a function is only ever executed once. |
| with pytest.raises(ValueError, match="Something went wrong"): | ||
| failing_function() | ||
|
|
||
| assert len(call_count) == 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: Why the different behavior between the decorator and once.do()? (The first one re-raises while the latter does not)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The decorator functions more like Go's sync.OnceFunc, albeit more flexible since it allows an arbitrary function signature.
Basically this means that Once() allows for some functionality to be called once-and-only once, with subsequent calls being a no-op -- useful for fire-and-forget cases.
| If the function raises an exception, `do` considers `fn` as done, where subsequent calls are still no-ops. |
The decorator on the other hand will cache the response of the passed in function, which if it's an error will mean it needs to be re-raised.
localstack/localstack-core/localstack/utils/sync.py
Lines 223 to 225 in 7e703c8
| A decorator that ensures a function is over ever executed once. | |
| The first call to decorated function will permanently set results. |
Lmk if that makes sense!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, perhaps I should rename the decorator to I renamed this to once_func instead to be more consistent with naming 🤔 Wdyt?once_func instead
I've added a companion PR for this #13295 to illustrate how the fixture can be used to ensure idempotent cleanups of a resource when testing 👀 |
Nice once, that's a good use of the new class! |
|
@carole-lavillonniere The |
I misread and thought the companion PR was using directly the class |
Motivation
A repeated pattern across the codebase is ensuring idempotent execution of some function/method, where multiple calls can be expensive, result in exceptions, or have unintende side-affects, making it inappropriate for repeated invocations.
To address the above, this PR proposes a port of the Golang
sync.Oncefunctionality -- introducing some utilities for thread-safe and at-most-once execution.Changes
sync.Onceutility that allows us to atomically execute some functionality once-and-only-once, with subsequent executions being no-ops.sync.oncedecorator that can be used to guard an arbitrary function against multiple executions, with prior results being cached and returned each time the guarded function is run.Example Usage
File creation and teardown.
A file should only ever be created OR removed once. This prevents the need to track internal state (i.e
is_removed: bool) or run someos.exists()functionality prior to each remove operation.Managing container creation and teardown.
Here, we expose multiple ways of starting the redis container
Service(i.e with some config or with default values), but need to ensure that not both operations can be called.