improve peer state handling

This commit is contained in:
Gareth George
2025-07-12 22:54:15 -07:00
committed by Gareth
parent 775b717354
commit c371cb4f02
6 changed files with 61 additions and 85 deletions
+21 -15
View File
@@ -1,10 +1,6 @@
import { Operation, OperationEvent, OperationEventType, OperationStatus } from "../../gen/ts/v1/operations_pb";
import { GetOperationsRequest, GetOperationsRequestSchema, OpSelector } from "../../gen/ts/v1/service_pb";
import { getOperations, subscribeToOperations, unsubscribeFromOperations } from "./oplog";
import {
STATS_OPERATION_HISTORY,
STATUS_OPERATION_HISTORY,
} from "../constants";
import { create } from "@bufbuild/protobuf";
type Subscriber = (ids: bigint[], flowIDs: bigint[], event: OperationEventType) => void;
@@ -90,7 +86,6 @@ export const getStatusForSelector = async (sel: OpSelector) => {
return await getStatus(req);
};
export class OplogState {
private byID: Map<bigint, Operation> = new Map();
private byFlowID: Map<bigint, Operation[]> = new Map();
@@ -220,15 +215,26 @@ export class OplogState {
}
export const matchSelector = (selector: OpSelector, op: Operation) => {
if (selector.planId && selector.planId !== op.planId) {
return false;
}
if (selector.repoGuid && selector.repoGuid !== op.repoGuid) {
return false;
}
if (selector.flowId && selector.flowId !== op.flowId) {
return false;
// Defining matchers for each field in OpSelector to determine if an operation matches the selector.
// Type system asserts that a check must exist for each field in OpSelector.
const selectorFieldMatchers: { [K in keyof OpSelector]: (op: Operation, sel: OpSelector) => boolean } = {
planId: (op, sel) => op.planId === sel.planId,
repoGuid: (op, sel) => op.repoGuid === sel.repoGuid,
flowId: (op, sel) => op.flowId === sel.flowId,
instanceId: (op, sel) => op.instanceId === sel.instanceId,
snapshotId: (op, sel) => op.snapshotId === sel.snapshotId,
originalInstanceKeyid: (op, sel) => op.originalInstanceKeyid === sel.originalInstanceKeyid,
ids: (op: Operation, sel: OpSelector) => sel.ids.includes(op.id),
["$typeName"]: (op: Operation, sel: OpSelector): boolean => true, // $typeName is a proto property that isn't used for matching
};
export const matchSelector = (selector: OpSelector, op: Operation): boolean => {
for (const key in selector) {
const matcher = selectorFieldMatchers[key as keyof OpSelector];
if (matcher && !matcher(op, selector)) {
return false;
}
}
return true;
}
};
+1 -1
View File
@@ -10,6 +10,7 @@ import { EmptySchema } from "../../gen/ts/types/value_pb";
import { create } from "@bufbuild/protobuf";
import _ from "lodash";
import { backrestService } from "../api";
import { useEffect, useState } from "react";
const subscribers: ((event?: OperationEvent, err?: Error) => void)[] = [];
@@ -57,7 +58,6 @@ export const unsubscribeFromOperations = (
console.log("unsubscribed from operations, subscriber count: ", subscribers.length);
};
export const shouldHideOperation = (operation: Operation) => {
return (
operation.op.case === "operationStats" ||
+24 -2
View File
@@ -1,3 +1,4 @@
import { useEffect, useState } from "react";
import { PeerState } from "../../gen/ts/v1/syncservice_pb";
import { syncStateService } from "../api";
@@ -43,19 +44,20 @@ const subscribeToSyncStates = async (
let peerStates: Map<string, PeerState> = new Map();
const subscribers: Set<(peerStates: PeerState[]) => void> = new Set();
export const subscribeToPeerStates = (
const subscribeToPeerStates = (
callback: (peerStates: PeerState[]) => void,
): void => {
subscribers.add(callback);
callback(Array.from(peerStates.values()));
};
export const unsubscribeFromPeerStates = (
const unsubscribeFromPeerStates = (
callback: (peerStates: PeerState[]) => void,
): void => {
subscribers.delete(callback);
};
(async () => {
const abortController = new AbortController(); // never aborts at the moment.
subscribeToSyncStates(() => {
@@ -73,3 +75,23 @@ export const unsubscribeFromPeerStates = (
}
}, abortController);
})();
export const useSyncStates = (): PeerState[] => {
const [syncStates, setSyncStates] = useState<PeerState[]>(() =>
Array.from(peerStates.values())
);
useEffect(() => {
const handleStateUpdate = (states: PeerState[]) => {
setSyncStates(states);
};
subscribeToPeerStates(handleStateUpdate);
return () => {
unsubscribeFromPeerStates(handleStateUpdate);
};
}, []);
return syncStates;
};
+3 -28
View File
@@ -35,10 +35,7 @@ import { Route, Routes, useNavigate, useParams } from "react-router-dom";
import { MainContentAreaTemplate } from "./MainContentArea";
import { create } from "@bufbuild/protobuf";
import { PeerState } from "../../gen/ts/v1/syncservice_pb";
import {
subscribeToPeerStates,
unsubscribeFromPeerStates,
} from "../state/peerstates";
import { useSyncStates } from "../state/peerstates";
const { Header, Sider } = Layout;
const SummaryDashboard = React.lazy(() =>
@@ -97,18 +94,7 @@ const RepoViewContainer = () => {
const RemoteRepoViewContainer = () => {
const { peerInstanceId, repoId } = useParams();
const [peerStates, setPeerStates] = useState<PeerState[]>([]);
// subscribe to peer states
useEffect(() => {
const cb = (states: PeerState[]) => {
setPeerStates(states);
};
subscribeToPeerStates(cb);
return () => {
unsubscribeFromPeerStates(cb);
};
}, []);
const peerStates = useSyncStates();
// Peer state is used to find the right repo
const peerState = peerStates.find(
@@ -171,18 +157,7 @@ export const App: React.FC = () => {
const navigate = useNavigate();
const [config, setConfig] = useConfig();
const [peerStates, setPeerStates] = useState<PeerState[]>([]);
useEffect(() => {
if (!config || !config.multihost) return;
const cb = (states: PeerState[]) => {
setPeerStates(states);
};
subscribeToPeerStates(cb);
return () => {
unsubscribeFromPeerStates(cb);
};
}, [config]);
const peerStates = useSyncStates();
const items = getSidenavItems(config, peerStates);
+2 -15
View File
@@ -30,10 +30,7 @@ import {
Multihost_Permission_Type,
} from "../../gen/ts/v1/config_pb";
import { PeerState } from "../../gen/ts/v1/syncservice_pb";
import {
subscribeToPeerStates,
unsubscribeFromPeerStates,
} from "../state/peerstates";
import { useSyncStates } from "../state/peerstates";
import { PeerStateConnectionStatusIcon } from "../components/SyncStateIcon";
interface FormData {
@@ -76,7 +73,7 @@ export const SettingsModal = () => {
const showModal = useShowModal();
const alertsApi = useAlertApi()!;
const [form] = Form.useForm<FormData>();
const [peerStates, setPeerStates] = useState<PeerState[]>([]);
const peerStates = useSyncStates();
const [reloadOnCancel, setReloadOnCancel] = useState(false);
const [formEdited, setFormEdited] = useState(false);
@@ -84,16 +81,6 @@ export const SettingsModal = () => {
return null;
}
useEffect(() => {
const cb = (syncStates: PeerState[]) => {
setPeerStates(syncStates);
};
subscribeToPeerStates(cb);
return () => {
unsubscribeFromPeerStates(cb);
};
}, []);
const handleOk = async () => {
try {
// Validate form
+10 -24
View File
@@ -11,7 +11,7 @@ import {
Spin,
Typography,
} from "antd";
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useMemo } from "react";
import { useConfig } from "../components/ConfigProvider";
import {
SummaryDashboardResponse,
@@ -42,10 +42,7 @@ import { isMobile } from "../lib/browserutil";
import { useNavigate } from "react-router";
import { toJsonString } from "@bufbuild/protobuf";
import { ConfigSchema, Multihost } from "../../gen/ts/v1/config_pb";
import {
subscribeToPeerStates,
unsubscribeFromPeerStates,
} from "../state/peerstates";
import { useSyncStates } from "../state/peerstates";
import { PeerState } from "../../gen/ts/v1/syncservice_pb";
import { PeerStateConnectionStatusIcon } from "../components/SyncStateIcon";
import { last } from "lodash";
@@ -322,25 +319,14 @@ const MultihostSummary = ({
}: {
multihostConfig: Multihost | null;
}) => {
const [peerStates, setPeerStates] = useState<Map<string, PeerState>>(
new Map()
);
useEffect(() => {
const cb = (syncStates: PeerState[]) => {
setPeerStates((prev) => {
const updated = new Map(prev);
for (const state of syncStates) {
updated.set(state.peerKeyid, state);
}
return updated;
});
};
subscribeToPeerStates(cb);
return () => {
unsubscribeFromPeerStates(cb);
};
}, []);
const allPeerStates = useSyncStates();
const peerStates = useMemo(() => {
const map = new Map<string, PeerState>();
for (const state of allPeerStates) {
map.set(state.peerKeyid, state);
}
return map;
}, [allPeerStates]);
const knownHostTiles: JSX.Element[] = [];
for (const cfgPeer of multihostConfig?.knownHosts || []) {