Alternate way do delete users

Continuing the discussion from Implementing a paid content subscription model on a Plone site:

I have found an issue in the site we implemented the subscription model:

currently the site has around 800 users that have been created using the following code:

def create_subscriber(self, code, email, group):
    api.user.create(email=email, username=email, properties=dict(code=code))
    api.user.revoke_roles(email, roles=['Member'])
    api.group.add_user(groupname=group, username=email)

the problem arises when I try to delete one of the users:

def remove_subscriber(self, email):
    with api.env.adopt_roles(['Manager']):
        api.user.delete(username=email)

as it takes a lot of memory and the instance is restarted by memmon.

is this normal, or is a bug?

checking the code in the membership tool, I see the following things are done:

  1. delete members in acl_users
  2. delete member data in portal_memberdata
  3. delete members' home folders including all content items
  4. delete members' local roles

one of this parts is consuming a lot of memory but I have no idea which one.

I think I don't need steps 3 and 4; may I write my own method and skip them to avoid issues with memory consumption?

def remove_subscriber(self, email):
    membership_tool = api.portal.get_tool('portal_membership')
    with api.env.adopt_roles(['Manager']):
        membership_tool.deleteMembers([email], delete_memberareas=False, delete_localroles=False):

Deleting the local roles is what takes up the memory: it goes through the whole site, waking up all objects, which can be a lot if you have a big site.

For one client where we ran into this, we solved it with the following patch.
Note that you may need to tweak the depth option to fit your site.
The numbers mentioned in the docstring are for one specific site.

from Acquisition import aq_base
from Products.CMFCore.utils import _checkPermission
from Products.CMFCore.MembershipTool import MembershipTool

def deleteLocalRoles(self, obj, member_ids, reindex=1, recursive=0,
                     REQUEST=None, depth=3):
    """Delete local roles of specified members.

    This takes far too much memory.  See if we can reduce this.

    We have tried convincing Zope to release memory by using
    savepoints (transaction.savepoint(optimistic=True)).  We tried
    using the catalog to search for only folderish items, and do
    explicit garbage collection.  Nothing helped.

    Now we add a depth on which we search.  This is a fluid depth.
    If the object does not need deletion of local roles and there
    are no interesting local roles at all, we decrease the depth,
    thus eliminating uninteresting folders where the member likely
    has no local roles anywhere.

    Yes, this may fail to delete some local roles.  But at least it
    usually finishes within a few seconds instead of about a minute.
    And it does not consume over 1.5 GB of memory.  So be happy.

    Note: with a depth of 4 you would already crawl about 90 percent
    of the site, so that would hardly help.
    """
    delete = False
    if _checkPermission(ChangeLocalRoles, obj):
        has_local_roles = False
        for user, roles in obj.get_local_roles():
            if user in member_ids:
                delete = True
            if len(roles) == 0:
                # I guess this cannot happen, but let's be safe.
                continue
            elif len(roles) > 1:
                has_local_roles = True
                break
            elif roles[0] == 'Owner':
                # Only one uninteresting role.
                continue
            else:
                has_local_roles = True
                break
        if delete:
            # At least one to-be-deleted role has been found.
            obj.manage_delLocalRoles(userids=member_ids)
        elif not has_local_roles:
            # Nothing deleted at this level, and no interesting local
            # roles for other users.  Decrease search depth.
            depth -= 1
            if depth <= 0:
                # Ignore the rest of this content tree, if any.
                return

    if recursive and hasattr(aq_base(obj), 'contentValues'):
        for subobj in obj.contentValues():
            self.deleteLocalRoles(subobj, member_ids, 0, 1, depth=depth)

    if reindex and hasattr(aq_base(obj), 'reindexObjectSecurity'):
        # reindexObjectSecurity is always recursive
        obj.reindexObjectSecurity()


def patch_membershiptool():
    MembershipTool._orig_deleteLocalRoles = MembershipTool.deleteLocalRoles
    MembershipTool.deleteLocalRoles = deleteLocalRoles


def unpatch_membershiptool():
    MembershipTool.deleteLocalRoles = MembershipTool._orig_deleteLocalRoles
4 Likes

thanks, I ended with this and it seems to be working now:

def remove_subscriber(self, email):
    """Remove subscriber. We can not use `api.user.delete` as, by
    default, it tries to remove member areas and local roles; the
    later consumes a lot of memory and is not necessary for us.
    """
    membership_tool = api.portal.get_tool('portal_membership')
    with api.env.adopt_roles(['Manager']):
        membership_tool.deleteMembers(
            [email], delete_memberareas=False, delete_localroles=False)
1 Like

Plone Foundation Code of Conduct