mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-18 03:01:05 +01:00
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:
BIN
docs/de/user/admin/assets/cache_invalidation.png
Normal file
BIN
docs/de/user/admin/assets/cache_invalidation.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/de/user/admin/assets/rebuild_index.png
Normal file
BIN
docs/de/user/admin/assets/rebuild_index.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -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
|
||||
|
||||
30
docs/de/user/admin/troubleshooting.md
Normal file
30
docs/de/user/admin/troubleshooting.md
Normal 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:
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
BIN
docs/en/user/admin/assets/cache_invalidation.png
Normal file
BIN
docs/en/user/admin/assets/cache_invalidation.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/en/user/admin/assets/rebuild_index.png
Normal file
BIN
docs/en/user/admin/assets/rebuild_index.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -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.
|
||||
|
||||
28
docs/en/user/admin/troubleshooting.md
Normal file
28
docs/en/user/admin/troubleshooting.md
Normal 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:
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
2
gradle/changelog/invalidation.yaml
Normal file
2
gradle/changelog/invalidation.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Invalidation of caches and search index
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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";
|
||||
|
||||
50
scm-ui/ui-api/src/useInvalidation.tsx
Normal file
50
scm-ui/ui-api/src/useInvalidation.tsx
Normal 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);
|
||||
};
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user