Skip to content

Conversation

@gregfurman
Copy link
Contributor

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.Once functionality -- introducing some utilities for thread-safe and at-most-once execution.

Changes

  • Adds a new sync.Once utility that allows us to atomically execute some functionality once-and-only-once, with subsequent executions being no-ops.
  • Adds a sync.once decorator 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 some os.exists() functionality prior to each remove operation.

class Service:
   @sync.once
   def start(self):
      with open("myfile.txt", "x") as file:
         file.write("write the file")
   
   @sync.once
   def close(self):
      os.remove("myfile.txt")

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.

class Service:
    container: Container
    
    def __init__(self):
        self.container = Container("redis:latest")
        self.start_once = sync.Once()
    
   def start_with_defaults(self):
       self.start_once.do(self.container.start)

   def start_from_config(self):
       self.initialize_config_for_redis()
       self.start_once.do(self.container.start)
    
    @sync.once
    def close(self):
        self.container.destroy()

@gregfurman gregfurman added this to the 4.10 milestone Oct 20, 2025
@gregfurman gregfurman self-assigned this Oct 20, 2025
@gregfurman gregfurman added semver: minor Non-breaking changes which can be included in minor releases, but not in patch releases docs: skip Pull request does not require documentation changes notes: skip Pull request does not have to be mentioned in the release notes labels Oct 20, 2025
@github-actions
Copy link

github-actions bot commented Oct 20, 2025

Test Results - Preflight, Unit

22 376 tests  +13   20 626 ✅ +13   16m 0s ⏱️ +14s
     1 suites ± 0    1 750 💤 ± 0 
     1 files   ± 0        0 ❌ ± 0 

Results for commit 3e99d09. ± Comparison against base commit b4c510a.

♻️ This comment has been updated with latest results.

@github-actions
Copy link

github-actions bot commented Oct 20, 2025

Test Results (amd64) - Acceptance

7 tests  ±0   5 ✅ ±0   3m 25s ⏱️ +3s
1 suites ±0   2 💤 ±0 
1 files   ±0   0 ❌ ±0 

Results for commit 3e99d09. ± Comparison against base commit b4c510a.

♻️ This comment has been updated with latest results.

@github-actions
Copy link

github-actions bot commented Oct 20, 2025

Test Results (amd64) - Integration, Bootstrap

    5 files      5 suites   2h 39m 37s ⏱️
5 252 tests 4 737 ✅ 515 💤 0 ❌
5 258 runs  4 737 ✅ 521 💤 0 ❌

Results for commit 3e99d09.

♻️ This comment has been updated with latest results.

@github-actions
Copy link

github-actions bot commented Oct 20, 2025

LocalStack Community integration with Pro

    2 files  ±0      2 suites  ±0   2h 1m 19s ⏱️ -19s
4 878 tests ±0  4 523 ✅ ±0  355 💤 ±0  0 ❌ ±0 
4 880 runs  ±0  4 523 ✅ ±0  357 💤 ±0  0 ❌ ±0 

Results for commit 3e99d09. ± Comparison against base commit b4c510a.

♻️ This comment has been updated with latest results.

Copy link
Contributor

@carole-lavillonniere carole-lavillonniere left a 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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
Copy link
Contributor

@carole-lavillonniere carole-lavillonniere Oct 21, 2025

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)

Copy link
Contributor Author

@gregfurman gregfurman Oct 22, 2025

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.

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!

Copy link
Contributor Author

@gregfurman gregfurman Oct 22, 2025

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 once_func instead to be more consistent with naming 🤔 Wdyt? I renamed this to once_func instead

@gregfurman
Copy link
Contributor Author

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.

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 👀

@carole-lavillonniere
Copy link
Contributor

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.

I've added a companion PR for this #13295 to illustrate how the fixture can be used to ensure idempotent cleanups of a resource

Nice once, that's a good use of the new class!
suggestion: I think it’d be great to also use the decorator somewhere, both to have a concrete example of how it’s meant to be used and to make sure we’re not adding unused code to the repo.

@gregfurman
Copy link
Contributor Author

@carole-lavillonniere The once_func can be used as either a decorator OR as a wrapper function. So don't think the implementation as-is would introduce redundant code 🤔

@carole-lavillonniere
Copy link
Contributor

@carole-lavillonniere The once_func can be used as either a decorator OR as a wrapper function. So don't think the implementation as-is would introduce redundant code 🤔

I misread and thought the companion PR was using directly the class sync.Once but it's actually using once_func so all good 👍

@gregfurman gregfurman merged commit afbdfeb into main Oct 23, 2025
42 checks passed
@gregfurman gregfurman deleted the add/util/sync-once branch October 23, 2025 10:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs: skip Pull request does not require documentation changes notes: skip Pull request does not have to be mentioned in the release notes semver: minor Non-breaking changes which can be included in minor releases, but not in patch releases

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants