feat: schedule index operations and stats refresh from repo view

This commit is contained in:
Gareth George
2023-12-29 02:29:13 +00:00
parent 9b1bcaf11f
commit d6b057f166
9 changed files with 105 additions and 33 deletions
+26 -11
View File
@@ -40,6 +40,14 @@ func (t *PruneTask) Name() string {
}
func (t *PruneTask) Next(now time.Time) *time.Time {
shouldRun, err := t.shouldRun(now)
if err != nil {
zap.S().Errorf("task %v failed to check if it should run: %v", t.Name(), err)
}
if !shouldRun {
return nil
}
ret := t.at
if ret != nil {
t.at = nil
@@ -58,6 +66,24 @@ func (t *PruneTask) Next(now time.Time) *time.Time {
return ret
}
func (t *PruneTask) shouldRun(now time.Time) (bool, error) {
if t.force {
return true, nil
}
repo, err := t.orch.GetRepo(t.plan.Repo)
if err != nil {
return false, fmt.Errorf("get repo %v: %w", t.plan.Repo, err)
}
nextPruneTime, err := t.getNextPruneTime(repo, repo.repoConfig.PrunePolicy)
if err != nil {
return false, fmt.Errorf("get next prune time: %w", err)
}
return nextPruneTime.Before(now), nil
}
func (t *PruneTask) getNextPruneTime(repo *RepoOrchestrator, policy *v1.PrunePolicy) (time.Time, error) {
var lastPruneTime time.Time
t.orch.OpLog.ForEachByRepo(t.plan.Repo, indexutil.CollectLastN(100), func(op *v1.Operation) error {
@@ -86,17 +112,6 @@ func (t *PruneTask) Run(ctx context.Context) error {
}
op.Op = opPrune
if !t.force {
nextPruneTime, err := t.getNextPruneTime(repo, repo.repoConfig.PrunePolicy)
if err != nil {
return fmt.Errorf("get next prune time: %w", err)
}
if nextPruneTime.After(time.Now()) {
op.Status = v1.OperationStatus_STATUS_SYSTEM_CANCELLED
return nil
}
}
ctx, cancel := context.WithCancel(ctx)
interval := time.NewTicker(1 * time.Second)
defer interval.Stop()
+1 -1
View File
@@ -46,7 +46,7 @@ func (t *TaskWithOperation) setOperation(op *v1.Operation) error {
func (t *TaskWithOperation) runWithOpAndContext(ctx context.Context, do func(ctx context.Context, op *v1.Operation) error) error {
if t.op == nil {
return errors.New("task has no operation, a call to setOperation first is required.")
return errors.New("task has no operation, a call to setOperation first is required")
}
if t.running.Load() {
return errors.New("task is already running")
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"go.uber.org/zap"
)
var statBytesThreshold int64 = 1024 * 1024 * 1024 // 1GB
var statBytesThreshold int64 = 10 * 1024 * 1024 * 1024 // 10 GB added.
// StatsTask tracks a restic stats operation.
type StatsTask struct {
+1
View File
@@ -5,6 +5,7 @@ import { Backrest } from "../gen/ts/v1/service_connect";
const transport = createConnectTransport({
baseUrl: "/",
useBinaryFormat: true,
});
export const backrestService = createPromiseClient(Backrest, transport);
+29
View File
@@ -0,0 +1,29 @@
import React from "react";
import { Button, ButtonProps } from "antd";
import { useState } from "react";
export const SpinButton: React.FC<ButtonProps & {
onClickAsync: () => Promise<void>;
}> = ({ onClickAsync, ...props }) => {
const [loading, setLoading] = useState(false);
const onClick = async () => {
if (loading) {
return;
}
try {
setLoading(true);
await onClickAsync();
} finally {
setLoading(false);
}
};
return (
<Button
{...props}
loading={loading}
onClick={onClick}
/>
);
}
+2
View File
@@ -1 +1,3 @@
export const MAX_OPERATION_HISTORY = 10000;
export const STATUS_OPERATION_HISTORY = 10; // number of operations to load when determining plan / repo status.
export const STATS_OPERATION_HISTORY = 100; // number of operations to load when searching for stats for a repo.
+3 -2
View File
@@ -9,6 +9,7 @@ import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic_pb";
import _ from "lodash";
import { formatDuration, formatTime } from "../lib/formatting";
import { backrestService } from "../api";
import { STATS_OPERATION_HISTORY } from "../constants";
const subscribers: ((event: OperationEvent) => void)[] = [];
@@ -56,7 +57,7 @@ export const unsubscribeFromOperations = (
export const getStatusForPlan = async (plan: string) => {
const req = new GetOperationsRequest({
planId: plan,
lastN: BigInt(8),
lastN: BigInt(STATS_OPERATION_HISTORY),
});
return await getStatus(req);
}
@@ -64,7 +65,7 @@ export const getStatusForPlan = async (plan: string) => {
export const getStatusForRepo = async (repo: string) => {
const req = new GetOperationsRequest({
repoId: repo,
lastN: BigInt(8),
lastN: BigInt(STATS_OPERATION_HISTORY),
});
return await getStatus(req);
}
+15 -14
View File
@@ -9,6 +9,7 @@ import { OperationTree } from "../components/OperationTree";
import { MAX_OPERATION_HISTORY } from "../constants";
import { backrestService } from "../api";
import { GetOperationsRequest } from "../../gen/ts/v1/service_pb";
import { SpinButton } from "../components/SpinButton";
export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
const alertsApi = useAlertApi()!;
@@ -30,29 +31,29 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
}
};
const handlePruneNow = () => {
const handlePruneNow = async () => {
try {
backrestService.prune({ value: plan.id });
await backrestService.prune({ value: plan.id });
alertsApi.success("Prune scheduled.");
} catch (e: any) {
alertsApi.error("Failed to schedule prune: " + e.message);
}
};
const handleUnlockNow = () => {
const handleUnlockNow = async () => {
try {
alertsApi.info("Unlocking repo...");
backrestService.unlock({ value: plan.repo! });
await backrestService.unlock({ value: plan.repo! });
alertsApi.success("Repo unlocked.");
} catch (e: any) {
alertsApi.error("Failed to unlock repo: " + e.message);
}
};
const handleClearErrorHistory = () => {
const handleClearErrorHistory = async () => {
try {
alertsApi.info("Clearing error history...");
backrestService.clearHistory({ planId: plan.id, onlyFailed: true });
await backrestService.clearHistory({ planId: plan.id, onlyFailed: true });
alertsApi.success("Error history cleared.");
} catch (e: any) {
alertsApi.error("Failed to clear error history: " + e.message);
@@ -67,23 +68,23 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
</Typography.Title>
</Flex>
<Flex gap="small" align="center" wrap="wrap">
<Button type="primary" onClick={handleBackupNow}>
<SpinButton type="primary" onClickAsync={handleBackupNow}>
Backup Now
</Button>
</SpinButton>
<Tooltip title="Runs a prune operation on the repository that will remove old snapshots and free up space">
<Button type="default" onClick={handlePruneNow}>
<SpinButton type="default" onClickAsync={handlePruneNow}>
Prune Now
</Button>
</SpinButton>
</Tooltip>
<Tooltip title="Removes lockfiles and checks the repository for errors. Only run if you are sure the repo is not being accessed by another system">
<Button type="default" onClick={handleUnlockNow}>
<SpinButton type="default" onClickAsync={handleUnlockNow}>
Unlock Repo
</Button>
</SpinButton>
</Tooltip>
<Tooltip title="Removes failed operations from the list">
<Button type="default" onClick={handleClearErrorHistory}>
<SpinButton type="default" onClickAsync={handleClearErrorHistory}>
Clear Error History
</Button>
</SpinButton>
</Tooltip>
</Flex>
<Tabs
+27 -4
View File
@@ -1,17 +1,20 @@
import React, { useEffect, useState } from "react";
import { Repo } from "../../gen/ts/v1/config_pb";
import { Col, Empty, Flex, Row, Spin, Tabs, Typography } from "antd";
import { Col, Empty, Flex, Row, Spin, TabsProps, Tabs, Tooltip, Typography } from "antd";
import { useRecoilValue } from "recoil";
import { configState } from "../state/config";
import { useAlertApi } from "../components/Alerts";
import { OperationList } from "../components/OperationList";
import { OperationTree } from "../components/OperationTree";
import { MAX_OPERATION_HISTORY } from "../constants";
import { MAX_OPERATION_HISTORY, STATS_OPERATION_HISTORY } from "../constants";
import { GetOperationsRequest } from "../../gen/ts/v1/service_pb";
import { getOperations } from "../state/oplog";
import { RepoStats } from "../../gen/ts/v1/restic_pb";
import { formatBytes, formatDate, formatTime } from "../lib/formatting";
import { formatBytes, formatTime } from "../lib/formatting";
import { Operation } from "../../gen/ts/v1/operations_pb";
import { backrestService } from "../api";
import { StringValue } from "@bufbuild/protobuf";
import { SpinButton } from "../components/SpinButton";
export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
const alertsApi = useAlertApi()!;
@@ -21,7 +24,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
useEffect(() => {
setLoading(true);
setStatsOperation(null);
getOperations(new GetOperationsRequest({ repoId: repo.id!, lastN: BigInt(100) })).then((operations) => {
getOperations(new GetOperationsRequest({ repoId: repo.id!, lastN: BigInt(STATS_OPERATION_HISTORY) })).then((operations) => {
for (const op of operations) {
if (op.op.case === "operationStats") {
const stats = op.op.value.stats;
@@ -37,6 +40,11 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
});
}, [repo.id]);
// Task handlers
const handleIndexNow = async () => {
await backrestService.indexSnapshots(new StringValue({ value: repo.id! }));
}
// Gracefully handle deletions by checking if the plan is still in the config.
const config = useRecoilValue(configState);
let repoInConfig = config.repos?.find((p) => p.id === repo.id);
@@ -64,6 +72,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
}
</>
),
destroyInactiveTabPane: true,
},
{
key: "2",
@@ -76,6 +85,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
/>
</>
),
destroyInactiveTabPane: true,
},
{
key: "3",
@@ -89,6 +99,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
/>
</>
),
destroyInactiveTabPane: true,
},
]
return (
@@ -98,6 +109,18 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
{repo.id}
</Typography.Title>
</Flex>
<Flex gap="small" align="center" wrap="wrap">
<Tooltip title="Indexes the snapshots in the repository. Snapshots are also indexed automatically after each backup.">
<SpinButton type="default" onClickAsync={handleIndexNow}>
Index Snapshots
</SpinButton>
</Tooltip>
<Tooltip title="Computes stats for the repository. May take some time to refresh.">
<SpinButton type="default" onClickAsync={handleIndexNow}>
Index Snapshots
</SpinButton>
</Tooltip>
</Flex>
<Tabs
defaultActiveKey={items[0].key}
items={items}