Skip to content

Commit

Permalink
cache: adds unmanaged groups to be cached and loaded in the identity
Browse files Browse the repository at this point in the history
* closes inveniosoftware/invenio-app-rdm#2186
* fixes display of group names
* updates hooks to invalidate cache on user/role change
* adds identity cache
* adds celery task to clean the identity cache

Co-authored-by: jrcastro2 <[email protected]>
  • Loading branch information
TLGINO and jrcastro2 committed Jun 13, 2023
1 parent 890e3e3 commit 41c1fa0
Show file tree
Hide file tree
Showing 26 changed files with 569 additions and 103 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#
# This file is part of Invenio.
# Copyright (C) 2016-2018 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Update role_id type.
This recipe only contains the upgrade because as it directly depends on invenio-accounts recipe. That recipe is in
charge of deleting all the constraints on the role_id, including foreign keys using the role_id declared in this module.
Therefore, when in order to downgrade we need to split the recipes to be able to first execute the recipe in
invenio-access (f9843093f686) - this will invoke the recipe from invenio-accounts and delete all the FKs - and after
that we can execute the downgrade recipe (37b21951084c).
"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "02cd82910727"
down_revision = (
"f9843093f686",
"37b21951084c",
) # Depends on invenio-access revision id (f9843093f686)
branch_labels = ()
depends_on = None


def upgrade():
"""Upgrade database."""
# Foreign keys are already dropped by invenio-accounts
op.alter_column(
"communities_archivedinvitations",
"group_id",
existing_type=sa.INTEGER(),
type_=sa.String(length=80),
existing_nullable=True,
)
op.create_foreign_key(
op.f("fk_communities_archivedinvitations_group_id_accounts_role"),
"communities_archivedinvitations",
"accounts_role",
["group_id"],
["id"],
ondelete="RESTRICT",
)
op.alter_column(
"communities_members",
"group_id",
existing_type=sa.INTEGER(),
type_=sa.String(length=80),
existing_nullable=True,
)
op.create_foreign_key(
op.f("fk_communities_members_group_id_accounts_role"),
"communities_members",
"accounts_role",
["group_id"],
["id"],
ondelete="RESTRICT",
)


def downgrade():
"""Downgrade database."""
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#
# This file is part of Invenio.
# Copyright (C) 2016-2018 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Update role_id type (downgrade recipe)."""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "37b21951084c"
down_revision = "a3f5a8635cbb"
branch_labels = ()
depends_on = None


def upgrade():
"""Upgrade database."""
pass


def downgrade():
"""Downgrade database."""
op.alter_column(
"communities_members",
"group_id",
existing_type=sa.String(length=80),
type_=sa.INTEGER(),
existing_nullable=True,
postgresql_using="group_id::integer",
)
op.create_foreign_key(
op.f("fk_communities_members_group_id_accounts_role"),
"communities_members",
"accounts_role",
["group_id"],
["id"],
ondelete="RESTRICT",
)
op.alter_column(
"communities_archivedinvitations",
"group_id",
existing_type=sa.String(length=80),
type_=sa.INTEGER(),
existing_nullable=True,
postgresql_using="group_id::integer",
)
op.create_foreign_key(
op.f("fk_communities_archivedinvitations_group_id_accounts_role"),
"communities_archivedinvitations",
"accounts_role",
["group_id"],
["id"],
ondelete="RESTRICT",
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
revision = "fbe746957cfc"
down_revision = "f701a32e6fbe"
branch_labels = ()
depends_on = None
depends_on = (
"a14fa442680f",
"2f63be7b7572",
) # where the accounts_role and request_metadata table are created


def upgrade():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import PropTypes from "prop-types";
import React, { Component } from "react";
import { Image, withCancel } from "react-invenio-forms";
import { Dropdown, List } from "semantic-ui-react";
import _truncate from "lodash/truncate";

export class MembersSearchBar extends Component {
constructor(props) {
Expand Down Expand Up @@ -67,7 +68,10 @@ export class MembersSearchBar extends Component {
<List.Item>
<Image size="mini" src={group.links.avatar} avatar />
<List.Content>
<List.Header as="a">{group.id}</List.Header>
<List.Header as="a">{group.name}</List.Header>
<List.Description>
{_truncate(group.description, { length: 30 })}
</List.Description>
</List.Content>
</List.Item>
</List>
Expand Down Expand Up @@ -105,7 +109,7 @@ export class MembersSearchBar extends Component {
};

if (searchType === "group") {
serializedSelectedMember["name"] = newSelectedMember.id;
serializedSelectedMember["name"] = newSelectedMember.name; // The schema will pass the id if the name is missing
} else {
serializedSelectedMember["name"] = this.serializeMemberName(newSelectedMember);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export class ManagerMembersResultItem extends Component {
<Item.Header className={!result.member.description ? "mt-5" : ""}>
<b className="mr-10">{result.member.name}</b>

{result.member.is_group && (
{result.member.type === "group" && (
<Label className="mr-10">{i18next.t("Group")}</Label>
)}
{result.is_current_user && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class PublicMemberPublicViewResultItem extends Component {
<Item.Content className="ml-10">
<Item.Header className={!result.member.description ? "mt-5" : ""}>
<b>{result.member.name}</b>
{result.member.is_group && (
{result.member.type === "group" && (
<Label className="ml-10">{i18next.t("Group")}</Label>
)}
</Item.Header>
Expand Down
9 changes: 9 additions & 0 deletions invenio_communities/cache/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2023 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Cache implementations."""
49 changes: 49 additions & 0 deletions invenio_communities/cache/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2023 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Abstract simple identity cache definition."""
from abc import ABC, abstractmethod

from flask import current_app
from werkzeug.utils import cached_property


class IdentityCache(ABC):
"""Abstract cache layer."""

def __init__(self, app=None):
"""Initialize the cache."""

@cached_property
def timeout(self):
"""Return default timeout from config."""
return current_app.config["COMMUNITIES_IDENTITIES_CACHE_TIME"]

@abstractmethod
def get(self, key):
"""Return the key value.
:param key: the object's key
"""

@abstractmethod
def set(self, key, value, timeout=None):
"""Cache the object.
:param key: the object's key
:param value: the stored object
:param timeout: the cache timeout in seconds
"""

@abstractmethod
def delete(self, key):
"""Delete the specific key."""

@abstractmethod
def flush(self):
"""Flush the cache."""
72 changes: 72 additions & 0 deletions invenio_communities/cache/redis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2023 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Implements a Redis cache."""
from cachelib import RedisCache
from flask import current_app
from redis import StrictRedis

from invenio_communities.cache.cache import IdentityCache


class IdentityRedisCache(IdentityCache):
"""Redis image cache."""

def __init__(self, app=None):
"""Initialize the cache."""
super().__init__(app=app)
app = app or current_app
redis_url = app.config["COMMUNITIES_IDENTITIES_CACHE_REDIS_URL"]
prefix = app.config.get("COMMUNITIES_IDENTITIES_CACHE_REDIS_PREFIX", "identity")
self.cache = RedisCache(host=StrictRedis.from_url(redis_url), key_prefix=prefix)

def get(self, key):
"""Return the key value.
:param key: the object's key
:return: the stored object
"""
return self.cache.get(key)

def set(self, key, value, timeout=None):
"""Cache the object.
:param key: the object's key
:param value: the stored object
:param timeout: the cache timeout in seconds
"""
timeout = timeout or self.timeout
self.cache.set(key, value, timeout=timeout)

def delete(self, key):
"""Delete the specific key."""
self.cache.delete(key)

def flush(self):
"""Flush the cache."""
self.cache.clear()

def append(self, key, value):
"""Appends a new value to a list.
:param key: the object's key
:param value: the stored list
"""
values_list = self.cache.get(key)
if values_list:
if not isinstance(values_list, list):
raise TypeError(
"Value {value} must be a list but was {type}".format(
value=values_list, type=type(values_list)
)
)
if value not in values_list:
values_list.append(value)
else:
values_list = [value]
self.set(key, values_list)
16 changes: 15 additions & 1 deletion invenio_communities/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,28 @@

from .fixtures.demo import create_fake_community
from .fixtures.tasks import create_demo_community
from .proxies import current_communities
from .proxies import current_communities, current_identities_cache


@click.group()
def communities():
"""Invenio communities commands."""


@click.group()
def identity_cache():
"""Invenio identity cache commands."""


@identity_cache.command("clear")
@with_appcontext
def clear():
"""Clears identity cache."""
click.secho("Clearing identity cache.", fg="green")
current_identities_cache.flush()
click.secho("Identity cache cleared.", fg="green")


@communities.command("demo")
@with_appcontext
def demo():
Expand Down
4 changes: 2 additions & 2 deletions invenio_communities/communities/services/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from marshmallow.exceptions import ValidationError

from ...proxies import current_roles
from ...utils import on_membership_change
from ...utils import on_user_membership_change
from ..records.systemfields.access import VisibilityEnum


Expand Down Expand Up @@ -126,7 +126,7 @@ def create(self, identity, data=None, record=None, **kwargs):
)

# Invalidate the membership cache
on_membership_change(identity=identity)
on_user_membership_change(identity=identity)


class FeaturedCommunityComponent(ServiceComponent):
Expand Down
12 changes: 12 additions & 0 deletions invenio_communities/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,15 @@
COMMUNITIES_ADMINISTRATION_DISABLED = True

COMMUNITIES_ALLOW_RESTRICTED = True

# Cache duration
# 60 seconds * 60 (1 hour) * 24 (1 day)
COMMUNITIES_IDENTITIES_CACHE_TIME = 60 * 60 * 24

# Redis URL Cache for identities
COMMUNITIES_IDENTITIES_CACHE_REDIS_URL = "redis://localhost:6379/4"

# Cache handler
COMMUNITIES_IDENTITIES_CACHE_HANDLER = (
"invenio_communities.cache.redis:IdentityRedisCache"
)
Loading

0 comments on commit 41c1fa0

Please sign in to comment.