Invalidation of caches and search index

In the general admin settings, the user can find two button to either invalidate the cache or rebuild the search index.

The endpoints are defined in the InvalidationResource class in scm-webapp.

Co-authored-by: René Pfeuffer<rene.pfeuffer@cloudogu.com>
This commit is contained in:
Thomas Zerr
2023-11-02 10:51:32 +01:00
parent 69c165749a
commit 123fc4c3d1
27 changed files with 660 additions and 3 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -9,6 +9,7 @@ Im Bereich Administration kann die SCM-Manager Instanz administriert werden. Von
* [Berechtigungsrollen](roles/)
* [Einstellungen](settings/)
* [Git](git/)
* [Fehlerbehebung](troubleshooting/)
<!--- AppendLinkContentEnd -->
### Information

View File

@@ -0,0 +1,30 @@
---
title: Fehlerbehebung
---
## Caches invalidieren
Um die Performance des SCM-Managers zu verbessern, werden viele Daten zusätzlich als Cache im Arbeitsspeicher gehalten.
Es kann passieren, dass die Daten im Cache nicht invalidiert werden, obwohl sich die zugrundeliegenden Daten geändert
haben. Dies kann zu Fehlern führen, z. B. könnten manche Ansichten versuchen ein Repository zu laden, welches bereits
gelöscht wurde. Um dieses Problem manuell zu lösen, können Administratoren den internen Cache des SCM-Managers
invalidieren. Allerdings kann diese Operation den SCM-Manager für eine Zeit verlangsamen. Dementsprechend sollte diese
Operation nur bedacht genutzt werden.
Die Option zur Invalidierung findet sich in den generellen Einstellungen:
![Screenshot der generellen Einstellungen für die Cache Invalidierung](assets/cache_invalidation.png)
## Suchindex neu aufbauen
Unter hoher Server-Last kann es passieren, dass der Suchindex nicht korrekt invalidiert wird, obwohl sich die
zugrundeliegenden Daten geändert haben. Dementsprechend kann es passieren, dass veraltete Daten gefunden werden. Dies
kann zu Fehlern in der Suchkomponente führen. Um dieses Problem manuell zu lösen, können Administratoren den Suchindex
neu erstellen lassen. Allerdings ist diese Operation zeitaufwändig und könnte den SCM-Manager für eine Zeit
verlangsamen. Dementsprechend sollte diese Operation nur bedacht genutzt werden. Wenn die Probleme bei der Suche nur ein
Repository betrifft, dann sollten Administratoren stattdessen nur den Suchindex für dieses Repository neu aufbauen
lassen. Dies kann in den generellen Einstellungen des Repositories gemacht werden.
Die Option zum Neuaufbau findet sich in den generellen Einstellungen:
![Screenshot der generellen Einstellungen für das erneute Aufbauen des Suchindex](assets/rebuild_index.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -8,6 +8,7 @@ The SCM-Manager instance can be administered in the Administration area. From he
* [Permission Roles](roles/)
* [Settings](settings/)
* [Git](git/)
* [Troubleshooting](troubleshooting/)
### Information
On the information page in the administration area you can find the version of your SCM-Manager instance and helpful links to get in touch with the SCM-Manager support team. If there is a newer version for SCM-Manager, it will be shown with the link to the download section on the official SCM-Manager homepage.

View File

@@ -0,0 +1,28 @@
---
title: Fehlerbehebung
---
## Invalidate Caches
To improve the performance of the SCM-Manager, many data is additionally kept as cache in the main memory. It can happen
that the data in the cache is not invalidated, although the underlying data has changed. This can lead to errors, e.g.
some views could try to load a repository that has already been deleted. To solve this problem manually, administrators
can invalidate the internal cache of the SCM-Manager. However, this operation can slow down the SCM-Manager for a while.
Accordingly, this operation should only be used with caution.
The option for invalidation can be found in the general settings:
![Screenshot of the general settings for cache invalidation](assets/cache_invalidation.png)
## Rebuild Search Index
Under high server load, it can happen that the search index is not invalidated correctly, although the underlying data
has changed. Accordingly, it can happen that outdated data is found. This can lead to errors in the search component. To
solve this problem manually, administrators can have the search index rebuilt. However, this operation is time-consuming
and could slow down the SCM-Manager for a while. Accordingly, this operation should only be used with caution. If the
problems with the search only affect one repository, then administrators should instead have the search index for this
repository rebuilt. This can be done in the general settings of the repository.
The option for rebuilding can be found in the general settings:
![Screenshot of the general Settings to rebuild the search index](assets/rebuild_index.png)

View File

@@ -0,0 +1,2 @@
- type: added
description: Invalidation of caches and search index

View File

@@ -48,4 +48,10 @@ public interface CacheManager extends Closeable {
* @return the cache with the specified types and name
*/
<K, V> Cache<K, V> getCache(String name);
/**
* Clears (aka invalidates) all caches.
* @since 2.48.0
*/
void clearAllCaches();
}

View File

@@ -75,6 +75,13 @@ public class MapCacheManager
return (MapCache<K, V>) cacheMap.computeIfAbsent(name, k -> new MapCache<K, V>());
}
@Override
public void clearAllCaches() {
for(MapCache<?, ?> cache : cacheMap.values()) {
cache.clear();
}
}
//~--- fields ---------------------------------------------------------------
/** Field description */

View File

@@ -61,6 +61,7 @@ export * from "./contentType";
export * from "./annotations";
export * from "./search";
export * from "./loginInfo";
export * from "./useInvalidation";
export * from "./usePluginCenterAuthInfo";
export * from "./compare";
export * from "./utils";

View File

@@ -0,0 +1,50 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { useMutation } from "react-query";
import { apiClient } from "./apiclient";
import { useRequiredIndexLink } from "./base";
const useInvalidation = (link: string) => {
const { mutate, isLoading, error, isSuccess } = useMutation<unknown, Error, string>((link) =>
apiClient.post(link, {})
);
return {
invalidate: () => mutate(link),
isLoading,
isSuccess,
error,
};
};
export const useInvalidateAllCaches = () => {
const invalidateCacheLink = useRequiredIndexLink("invalidateCaches");
return useInvalidation(invalidateCacheLink);
};
export const useInvalidateSearchIndices = () => {
const invalidateSearchIndexLink = useRequiredIndexLink("invalidateSearchIndex");
return useInvalidation(invalidateSearchIndexLink);
};

View File

@@ -93,6 +93,18 @@
"login-attempt-limit-invalid": "Dies ist keine Zahl",
"plugin-url-invalid": "Dies ist keine gültige URL"
},
"invalidateCaches": {
"success": "Invalidierung von Caches war erfolgreich",
"subtitle": "Caches invalidieren",
"description": "Invalidieren sie Caches manuell, um bestimmte Probleme zu beheben. Achtung: Nach der Invalidierung ist der SCM-Manager verlangsamt.",
"button": "Invalidierung von Caches starten"
},
"invalidateSearchIndex": {
"success": "Neuaufbau vom Suchindex erfolgreich angestoßen",
"subtitle": "Suchindex neu aufbauen",
"description": "Bauen Sie den Suchindex neu auf, um Probleme mit den Suchergebnissen zu beheben. Achtung: während des Neuaufbaus ist der SCM-Manager verlangsamt.",
"button": "Neuaufbau vom Suchindex starten"
},
"help": {
"realmDescriptionHelpText": "Beschreibung des Authentication Realm.",
"dateFormatHelpText": "Moments Datumsformat. Zulässige Formate sind in der MomentJS Dokumentation beschrieben.",

View File

@@ -93,6 +93,18 @@
"login-attempt-limit-invalid": "This is not a number",
"plugin-url-invalid": "This is not a valid url"
},
"invalidateCaches": {
"success": "Successfully invalidated caches",
"subtitle": "Invalidate Caches",
"description": "Invalidate caches manually to fix certain issues. Warning: After invalidation the SCM-Manager is slowed down.",
"button": "Start cache invalidation"
},
"invalidateSearchIndex": {
"success": "Rebuild of the search index has been triggered",
"subtitle": "Rebuild Search Index",
"description": "Rebuild the search index to fix certain issues with search results. Warning: While rebuilding the search index the SCM-Manager is slowed down.",
"button": "Start recreation of search index"
},
"help": {
"realmDescriptionHelpText": "Enter authentication realm description.",
"dateFormatHelpText": "Moments date format. Please have a look at the MomentJS documentation.",

View File

@@ -23,7 +23,7 @@
*/
import React, { FC, FormEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Config, ConfigChangeHandler, NamespaceStrategies } from "@scm-manager/ui-types";
import { Config, ConfigChangeHandler, Link, NamespaceStrategies } from "@scm-manager/ui-types";
import { Level, Notification, SubmitButton } from "@scm-manager/ui-components";
import ProxySettings from "./ProxySettings";
import GeneralSettings from "./GeneralSettings";
@@ -31,6 +31,8 @@ import BaseUrlSettings from "./BaseUrlSettings";
import LoginAttempt from "./LoginAttempt";
import PluginSettings from "./PluginSettings";
import FunctionSettings from "./FunctionSettings";
import InvalidateCaches from "./InvalidateCaches";
import InvalidateSearchIndex from "./InvalidateSearchIndex";
type Props = {
submitForm: (p: Config) => void;
@@ -39,6 +41,8 @@ type Props = {
configReadPermission: boolean;
configUpdatePermission: boolean;
namespaceStrategies?: NamespaceStrategies;
invalidateCachesLink?: Link;
invalidateSearchIndexLink?: Link;
};
const ConfigForm: FC<Props> = ({
@@ -48,6 +52,8 @@ const ConfigForm: FC<Props> = ({
configReadPermission,
configUpdatePermission,
namespaceStrategies,
invalidateCachesLink,
invalidateSearchIndexLink,
}) => {
const [t] = useTranslation("config");
const [innerConfig, setInnerConfig] = useState<Config>({
@@ -196,6 +202,18 @@ const ConfigForm: FC<Props> = ({
hasUpdatePermission={configUpdatePermission}
/>
<hr />
{invalidateCachesLink ? (
<>
<InvalidateCaches />
<hr />
</>
) : null}
{invalidateSearchIndexLink ? (
<>
<InvalidateSearchIndex />
<hr />
</>
) : null}
<Level
right={
<SubmitButton

View File

@@ -0,0 +1,52 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import { ErrorNotification, Level, Notification, Subtitle } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import { Button, ButtonVariants } from "@scm-manager/ui-buttons";
import { useInvalidateAllCaches } from "@scm-manager/ui-api";
const InvalidateCaches: FC = () => {
const { invalidate, isLoading, error, isSuccess } = useInvalidateAllCaches();
const [t] = useTranslation("config");
return (
<div>
<Subtitle subtitle={t("invalidateCaches.subtitle")} />
{isSuccess ? <Notification type="success">{t("invalidateCaches.success")}</Notification> : null}
<ErrorNotification error={error} />
<p className="mb-4">{t("invalidateCaches.description")}</p>
<Level
right={
<Button variant={ButtonVariants.SIGNAL} isLoading={isLoading} onClick={invalidate}>
{t("invalidateCaches.button")}
</Button>
}
/>
</div>
);
};
export default InvalidateCaches;

View File

@@ -0,0 +1,52 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import { ErrorNotification, Level, Notification, Subtitle } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import { Button, ButtonVariants } from "@scm-manager/ui-buttons";
import { useInvalidateSearchIndices } from "@scm-manager/ui-api";
const InvalidateSearchIndex: FC = () => {
const { invalidate, isLoading, error, isSuccess } = useInvalidateSearchIndices();
const [t] = useTranslation("config");
return (
<div>
<Subtitle subtitle={t("invalidateSearchIndex.subtitle")} />
{isSuccess ? <Notification type="success">{t("invalidateSearchIndex.success")}</Notification> : null}
<ErrorNotification error={error} />
<p className="mb-4">{t("invalidateSearchIndex.description")}</p>
<Level
right={
<Button variant={ButtonVariants.SIGNAL} isLoading={isLoading} onClick={invalidate}>
{t("invalidateSearchIndex.button")}
</Button>
}
/>
</div>
);
};
export default InvalidateSearchIndex;

View File

@@ -26,15 +26,16 @@ import { useTranslation } from "react-i18next";
import { Link } from "@scm-manager/ui-types";
import { ErrorNotification, Loading, Title } from "@scm-manager/ui-components";
import ConfigForm from "../components/form/ConfigForm";
import { useConfig, useNamespaceStrategies, useUpdateConfig } from "@scm-manager/ui-api";
import { useConfig, useIndexLinks, useNamespaceStrategies, useUpdateConfig } from "@scm-manager/ui-api";
const GlobalConfig: FC = () => {
const indexLinks = useIndexLinks();
const { data: config, error: configLoadingError, isLoading: isLoadingConfig } = useConfig();
const { isLoading: isUpdating, error: updateError, isUpdated, update, reset } = useUpdateConfig();
const {
data: namespaceStrategies,
error: namespaceStrategiesLoadingError,
isLoading: isLoadingNamespaceStrategies
isLoading: isLoadingNamespaceStrategies,
} = useNamespaceStrategies();
const [t] = useTranslation("config");
const error = configLoadingError || namespaceStrategiesLoadingError || updateError || undefined;
@@ -67,6 +68,8 @@ const GlobalConfig: FC = () => {
namespaceStrategies={namespaceStrategies}
configUpdatePermission={canUpdateConfig}
configReadPermission={!!config}
invalidateCachesLink={indexLinks.invalidateCaches as Link | undefined}
invalidateSearchIndexLink={indexLinks.invalidateSearchIndex as Link | undefined}
/>
</>
);

View File

@@ -135,6 +135,10 @@ public class IndexDtoGenerator extends HalAppenderMapper {
}
if (ConfigurationPermissions.list().isPermitted()) {
builder.single(link("config", resourceLinks.config().self()));
if (ConfigurationPermissions.write(configuration.getId()).isPermitted()) {
builder.single(link("invalidateCaches", resourceLinks.invalidationLinks().caches()));
builder.single(link("invalidateSearchIndex", resourceLinks.invalidationLinks().searchIndex()));
}
if (!Strings.isNullOrEmpty(configuration.getReleaseFeedUrl())) {
builder.single(link("updateInfo", resourceLinks.adminInfo().updateInfo()));
}

View File

@@ -0,0 +1,102 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import sonia.scm.cache.CacheManager;
import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.search.IndexRebuilder;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
@OpenAPIDefinition(tags = {
@Tag(name = "Invalidations", description = "Invalidations of different resources like caches and search index")
})
@Path("v2/invalidations")
public class InvalidationResource {
private final CacheManager cacheManager;
private final IndexRebuilder indexRebuilder;
@Inject
public InvalidationResource(CacheManager cacheManager, IndexRebuilder indexRebuilder) {
this.cacheManager = cacheManager;
this.indexRebuilder = indexRebuilder;
}
@POST
@Path("/caches")
@Operation(
summary = "Invalidates the caches of every store",
description = "Deletes every cached object of every store from the cache",
tags = "Invalidations"
)
@ApiResponse(responseCode = "204", description = "Invalidated cache successfully")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:global\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public void invalidateCaches() {
ConfigurationPermissions.write("global").check();
cacheManager.clearAllCaches();
}
@POST
@Path("/search-index")
@Operation(
summary = "Invalidates the search index",
description = "Invalidates the search index, by completely recreating it",
tags = "Invalidations"
)
@ApiResponse(responseCode = "204", description = "Invalidated search index successfully")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:global\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public void invalidateSearchIndex() {
ConfigurationPermissions.write("global").check();
indexRebuilder.rebuildAll();
}
}

View File

@@ -347,6 +347,27 @@ class ResourceLinks {
}
}
InvalidationLinks invalidationLinks() {
return new InvalidationLinks(accessScmPathInfoStore().get());
}
static class InvalidationLinks {
private final LinkBuilder invalidationLinkBuilder;
InvalidationLinks(ScmPathInfo pathInfo) {
this.invalidationLinkBuilder = new LinkBuilder(pathInfo, InvalidationResource.class);
}
String caches() {
return invalidationLinkBuilder.method("invalidateCaches").parameters().href();
}
String searchIndex() {
return invalidationLinkBuilder.method("invalidateSearchIndex").parameters().href();
}
}
AdminInfoLinks adminInfo() {
return new AdminInfoLinks(accessScmPathInfoStore().get());
}

View File

@@ -79,6 +79,13 @@ public class GuavaCacheManager implements CacheManager, org.apache.shiro.cache.C
});
}
@Override
public void clearAllCaches() {
for(GuavaCache<?, ?> cache : caches.values()) {
cache.clear();
}
}
@Override
public void close() throws IOException {
LOG.info("close guava cache manager");

View File

@@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.search;
import javax.inject.Inject;
import java.util.Set;
public class IndexRebuilder {
private final SearchEngine searchEngine;
private final Set<Indexer> indexers;
@Inject
public IndexRebuilder(SearchEngine searchEngine, Set<Indexer> indexers) {
this.searchEngine = searchEngine;
this.indexers = indexers;
}
public void rebuildAll() {
for (Indexer indexer : indexers) {
searchEngine.forType(indexer.getType()).update(indexer.getReIndexAllTask());
}
}
}

View File

@@ -155,6 +155,68 @@ class IndexDtoGeneratorTest {
Link.linkBuilder("search", "/api/v2/search/query/group").withName("group").build()
);
}
@Nested
class InvalidationLinks {
@Test
void shouldAppendInvalidationLinks() {
when(subject.isAuthenticated()).thenReturn(true);
when(subject.isPermitted("configuration:list")).thenReturn(true);
when(subject.isPermitted("configuration:write:1")).thenReturn(true);
mockOtherPermissions();
when(configuration.getId()).thenReturn("1");
IndexDto dto = generator.generate();
assertThat(dto.getLinks().getLinkBy("invalidateCaches")).contains(
Link.linkBuilder("invalidateCaches", "/api/v2/invalidations/caches").build()
);
assertThat(dto.getLinks().getLinkBy("invalidateSearchIndex")).contains(
Link.linkBuilder("invalidateSearchIndex", "/api/v2/invalidations/search-index").build()
);
}
@Test
void shouldNotAppendInvalidationsIfWritePermissionIsMissing() {
when(subject.isAuthenticated()).thenReturn(true);
when(subject.isPermitted("configuration:list")).thenReturn(true);
when(subject.isPermitted("configuration:write:1")).thenReturn(false);
mockOtherPermissions();
when(configuration.getId()).thenReturn("1");
IndexDto dto = generator.generate();
assertThat(dto.getLinks().getLinkBy("invalidateCaches")).isEmpty();
assertThat(dto.getLinks().getLinkBy("invalidateSearchIndex")).isEmpty();
}
@Test
void shouldNotAppendInvalidationsIfListPermissionIsMissing() {
when(subject.isAuthenticated()).thenReturn(true);
when(subject.isPermitted("configuration:list")).thenReturn(false);
mockOtherPermissions();
IndexDto dto = generator.generate();
assertThat(dto.getLinks().getLinkBy("invalidateCaches")).isEmpty();
assertThat(dto.getLinks().getLinkBy("invalidateSearchIndex")).isEmpty();
}
@Test
void shouldNotAppendInvalidationsIfUnauthenticated() {
when(subject.isAuthenticated()).thenReturn(false);
IndexDto dto = generator.generate();
assertThat(dto.getLinks().getLinkBy("invalidateCaches")).isEmpty();
assertThat(dto.getLinks().getLinkBy("invalidateSearchIndex")).isEmpty();
}
private void mockOtherPermissions() {
when(subject.isPermitted("plugin:read")).thenReturn(false);
when(subject.isPermitted("plugin:write")).thenReturn(false);
when(subject.isPermitted("user:list")).thenReturn(false);
when(subject.isPermitted("user:autocomplete")).thenReturn(false);
when(subject.isPermitted("group:autocomplete")).thenReturn(false);
when(subject.isPermitted("group:list")).thenReturn(false);
}
}
}
private SearchableType searchableType(String name) {

View File

@@ -0,0 +1,126 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import org.github.sdorra.jse.ShiroExtension;
import org.github.sdorra.jse.SubjectAware;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.cache.CacheManager;
import sonia.scm.search.IndexRebuilder;
import sonia.scm.web.RestDispatcher;
import java.net.URISyntaxException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
@ExtendWith({MockitoExtension.class, ShiroExtension.class})
@SubjectAware("TrainerRed")
class InvalidationResourceTest {
@Mock
private CacheManager cacheManager;
@Mock
private IndexRebuilder indexRebuilder;
private RestDispatcher dispatcher;
private final String basePath = "/v2/invalidations";
@BeforeEach
void init() {
InvalidationResource invalidationResource = new InvalidationResource(cacheManager, indexRebuilder);
dispatcher = new RestDispatcher();
dispatcher.addSingletonResource(invalidationResource);
}
@Nested
class InvalidateCaches {
@Test
void shouldReturnForbiddenBecauseOfMissingPermission() throws URISyntaxException {
MockHttpResponse response = invokeInvalidateCaches();
assertThat(response.getStatus()).isEqualTo(403);
verifyNoInteractions(cacheManager);
}
@Test
@SubjectAware(permissions = {"configuration:write:global"})
void shouldClearCaches() throws URISyntaxException {
MockHttpResponse response = invokeInvalidateCaches();
assertThat(response.getStatus()).isEqualTo(204);
verify(cacheManager).clearAllCaches();
}
private MockHttpResponse invokeInvalidateCaches() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.post(basePath + "/caches");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
return response;
}
}
@Nested
class ReIndex {
@Test
void shouldReturnForbiddenBecauseOfMissingPermission() throws URISyntaxException {
MockHttpResponse response = invokeReIndex();
assertThat(response.getStatus()).isEqualTo(403);
verifyNoInteractions(indexRebuilder);
}
@Test
@SubjectAware(permissions = {"configuration:write:global"})
void shouldReIndexAll() throws URISyntaxException {
MockHttpResponse response = invokeReIndex();
assertThat(response.getStatus()).isEqualTo(204);
verify(indexRebuilder).rebuildAll();
}
private MockHttpResponse invokeReIndex() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.post(basePath + "/search-index");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
return response;
}
}
}

View File

@@ -87,6 +87,20 @@ public abstract class CacheManagerTestBase<C extends Cache>
assertIsSame(c1, c2);
}
@Test
public void shouldClearCache() {
Cache<String, String> c1 = cacheManager.getCache("test-1");
c1.put("key1", "value1");
Cache<String, String> c2 = cacheManager.getCache("test-2");
c2.put("key2", "value2");
cacheManager.clearAllCaches();
assertEquals(c1.size(), 0);
assertEquals(c2.size(), 0);
}
/**
* Method description
*