Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Multi-tenancy stale wallet clean up #1692

Merged

Conversation

dbluhm
Copy link
Contributor

@dbluhm dbluhm commented Mar 25, 2022

This PR is intended to supersede #928. Due to significant changes since the original PR was opened, I cherry-picked a commit but had to modify it. History has not been well preserved, unfortunately. Credit to @TimoGlastra for the original work on the ProfileCache especially and for assistance in finding the solution implemented here.

This PR adds a LRU cache for profiles opened by MultitenantManager (corresponding to wallet types indy and askar, but not askar-profile). weakref.finalize is used to ensure the profiles are closed when they fall out of scope. An ordered dictionary of profiles holds a (strong) reference to the profile until it is evicted. If that profile happens to still be in use at the time it is evicted, it will not be closed until other (strong) references to it expire, i.e. after processing of a message or admin request has finished. This exact scenario is unlikely but possible.

In addition to this profile caching mechanism, I also updated a few aspects of the BaseMultitenantManager and its subclasses to make them more consistent with conventions used in ACA-Py.

Also, after being very confused by handling of askar profiles (not to be confused with AskarProfiles) within the AskarProfileMultitenantManager and AskarProfile, I did some light updates to (I hope) improve clarity.

A brief listing of known limitations:

  • As implemented, the capacity of the LRU cache is statically defined as 100.
  • A temporally based system for evicting profiles from the cache is likely better than a max capacity based system.

Update:
I've taken a pass at improving the multitenancy-config argument. It will still accept json (read: this shouldn't be a breaking change) but it will now also accept key=value pairs. I'm still a bit iffy on whether these shouldn't just be their own separate arguments though...

Signed-off-by: Timo Glastra <timo@animo.id>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
remove usage of self._instances in AskarProfileMultitenantManager

add profile_id to AskarProfile init (to clearly differentiate between
        AskarProfiles where default profile is used and alternate
        profiles are used)

base update_wallet only updates records; managers where profiles are
held open are responsible for updating currently open profiles

Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
@dbluhm
Copy link
Contributor Author

dbluhm commented Mar 25, 2022

I'm still in the process of validating the cache behavior so I've opened this as a draft PR but I would appreciate feedback on what's here so far. Due to resourcing constraints, it's not likely that I'll be able to implement a time based cache (I haven't evaluated what that would look like, though, so if it ends up being super easy, maybe lol).

I would be particularly interested to hear feedback from @TimoGlastra and @andrewwhitehead since the two of you are the original participants on #928 but more than happy to get feedback from anyone.

@swcurran
Copy link
Contributor

@PaulWen - this would seem a really good PR to test with the Load Generator. Do you have an easy way to run tests on a PR branch?

@dbluhm
Copy link
Contributor Author

dbluhm commented Mar 25, 2022

Odd how submitting a PR suddenly brings clarity lol; as implemented, if more than 100 (or whatever the capacity is defined as) is actively in use, this can result in rapid opening and closure of wallets and WalletAlreadyOpen errors 🤔 I'm probably not quite on the mark here yet...

@PaulWen
Copy link

PaulWen commented Mar 25, 2022

This AcaPy repo is already integrated as a git submodule into the Load Generator repo to build AcaPy images with a debugger. Therefore, it should not be difficult to checkout any branch and build a docker image in a similar manner.

@dbluhm Could you describe to me what a suitable test flow would look like? Feel free to drop me a PM in Discord.

@codecov-commenter
Copy link

codecov-commenter commented Apr 12, 2022

Codecov Report

Merging #1692 (df75214) into main (27b238a) will decrease coverage by 0.02%.
The diff coverage is 96.35%.

@@            Coverage Diff             @@
##             main    #1692      +/-   ##
==========================================
- Coverage   95.23%   95.21%   -0.03%     
==========================================
  Files         525      526       +1     
  Lines       33056    33143      +87     
==========================================
+ Hits        31482    31558      +76     
- Misses       1574     1585      +11     

Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
@dbluhm
Copy link
Contributor Author

dbluhm commented Apr 12, 2022

I've sorted out most of the issues with this PR. I was having a heck of a time identifying where references to profiles were being held open but I've found the culprits (reponders held references to profiles/contexts and then profiles held references to responders...) and my rudimentary tests behave as expected.

I'll do my bit to cover new lines better for the sake of code coverage but I'm open to ideas for how or even if we should have tests exercising this functionality inside of ACA-Py.

Should have this ready for review soon.

Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
And make tests follow pytest convention

Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
@dbluhm dbluhm marked this pull request as ready for review April 14, 2022 20:00
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
def _finalize(opened: Optional[AskarOpenStore]):
if opened:
LOGGER.debug("Profile finalizer called; closing wallet")
asyncio.get_event_loop().create_task(opened.close())
Copy link
Contributor

Choose a reason for hiding this comment

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

It's nice but not necessary to close the store this way, as the library has its own finalizers. With multiple profile instances potentially referencing the same Store it would probably not be correct to close the store when a single profile is closed, either.

@@ -115,6 +116,18 @@ def inject_or(
async def close(self):
"""Close the profile instance."""

def finalizer(self) -> Optional[finalize]:
Copy link
Contributor

@andrewwhitehead andrewwhitehead Apr 20, 2022

Choose a reason for hiding this comment

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

I think the finalizer can just be set up in the profile's __init__ method when required. We don't have to keep a reference to it, and requiring the caller to initialize it seems error prone. (It could make testing a little harder.)

@dbluhm
Copy link
Contributor Author

dbluhm commented May 3, 2022

Not realizing that Askar already had finalizers and considering @andrewwhitehead's comments regarding requiring the caller to initialize the finalizer being potentially error prone, I've simplified the finalizer additions. Essentially, finalizers for Indy SDK wallet profiles are always created. I don't think there's a scenario where it would be appropriate for the wallet to remain opened after the profile has gone out of scope so I think this is a valid simplification.

In other words, all profiles managed by the profile cache are now assumed to clean up their own resources after falling out of scope and it is up to the profile implementation to ensure this is the case.

"""

def _finalize(opened: Optional[IndyOpenWallet]):
if opened:
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think opened can be None because it receives the original IndyOpenWallet instance. Maybe if opened.handle?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This came as a recommendation from pyright which indicates that IndySdkProfile.opened is IndyOpenWallet | None. I think it derived this from the self.opened = None line in the close method.

Following that same vein, I think it was possible before for the finalizer to be created after close had been called. Now that it's only intended to be called in __init__, I don't think this is a problem any more. I'll fix that.

@@ -15,13 +16,23 @@ class AskarProfileMultitenantManager(BaseMultitenantManager):

DEFAULT_MULTIENANT_WALLET_NAME = "multitenant_sub_wallet"
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we could fix this variable name

@abstractproperty
def open_profiles(self) -> Iterable[Profile]:
"""Return iterator over open profiles."""
...
Copy link
Contributor

Choose a reason for hiding this comment

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

Ellipsis?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was to satisfy pyright more than anything else. Oddly, it seems fine if I do @property and @abstractmethod but I have to add the ellipsis if I use @abstractproperty. I'll remove this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Apparently it's about the same as writing 'pass', I just haven't seen it much before.

@andrewwhitehead
Copy link
Contributor

Added a couple small comments but it looks great otherwise, I'd like to get this merged.

Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
@dbluhm
Copy link
Contributor Author

dbluhm commented Jun 2, 2022

Digging into the unit test failure; it's not behaving quite the same way when I run run_tests_indy locally so a little perplexed. Should have more time to look at this shortly.

@swcurran swcurran merged commit 814cd8d into openwallet-foundation:main Jun 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants