fix: show worker added message

This commit is contained in:
jialin
2026-06-05 10:20:55 +08:00
committed by jialin
parent faf6246fe6
commit 907187d53a
8 changed files with 118 additions and 28 deletions
+10 -18
View File
@@ -14,7 +14,6 @@ type EventsType = 'CREATE' | 'UPDATE' | 'DELETE' | 'INSERT';
export function useUpdateChunkedList(options: { export function useUpdateChunkedList(options: {
events?: EventsType[]; events?: EventsType[];
dataList?: any[]; dataList?: any[];
triggerAt?: React.MutableRefObject<number>;
limit?: number; limit?: number;
onCreate?: (args: any) => void; onCreate?: (args: any) => void;
onUpdate?: (args: any) => void; onUpdate?: (args: any) => void;
@@ -24,9 +23,9 @@ export function useUpdateChunkedList(options: {
filterFun?: (args: any) => boolean; filterFun?: (args: any) => boolean;
mapFun?: (args: any) => any; mapFun?: (args: any) => any;
computedID?: (d: object) => string; computedID?: (d: object) => string;
isNewItem?: (item: any) => boolean;
}) { }) {
const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'], triggerAt } = const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'] } = options;
options;
const deletedIdsRef = useRef<Set<number | string>>(new Set()); const deletedIdsRef = useRef<Set<number | string>>(new Set());
const cacheDataListRef = useRef<any[]>(options.dataList || []); const cacheDataListRef = useRef<any[]>(options.dataList || []);
const timerRef = useRef<any>(null); const timerRef = useRef<any>(null);
@@ -71,17 +70,14 @@ export function useUpdateChunkedList(options: {
(sItem: any) => sItem.id === item.id (sItem: any) => sItem.id === item.id
); );
const updateItem = { ...item }; const updateItem = { ...item };
if (updateIndex === -1 && !triggerAt?.current) { if (updateIndex === -1) {
acc.push(updateItem); acc.push(updateItem);
} else if (!triggerAt?.current) { } else {
cacheDataListRef.current[updateIndex] = updateItem; cacheDataListRef.current[updateIndex] = updateItem;
} }
// only push items created after the watch started
// TODO only push items created after triggerAt if (options.isNewItem?.(item)) {
if (
triggerAt?.current &&
Date.parse(item.created_at) >= triggerAt.current
) {
latestCreateList.push(updateItem); latestCreateList.push(updateItem);
} }
@@ -103,10 +99,8 @@ export function useUpdateChunkedList(options: {
cacheDataListRef.current = cacheDataListRef.current?.filter( cacheDataListRef.current = cacheDataListRef.current?.filter(
(item: any) => { (item: any) => {
// collect deleted items // collect deleted items
if (triggerAt?.current) { if (ids?.includes(item.id) && !options.isNewItem?.(item)) {
if (ids?.includes(item.id)) { deletedList.push(item);
deletedList.push(item);
}
} }
return !ids?.includes(item.id); return !ids?.includes(item.id);
} }
@@ -133,10 +127,8 @@ export function useUpdateChunkedList(options: {
updateItem, updateItem,
...cacheDataListRef.current.slice(0, limit - 1) ...cacheDataListRef.current.slice(0, limit - 1)
]; ];
if (options.onUpdate && triggerAt?.current) { if (options.onUpdate && options.isNewItem?.(item)) {
if (Date.parse(item.created_at) >= triggerAt.current) { options.onUpdate?.([updateItem]);
options.onUpdate?.([updateItem]);
}
} }
} }
}); });
-1
View File
@@ -396,7 +396,6 @@ export default (props: any) => {
)} )}
onCollapse={onCollapse} onCollapse={onCollapse}
onMenuHeaderClick={onMenuHeaderClick} onMenuHeaderClick={onMenuHeaderClick}
menuHeaderRender={renderMenuHeader}
collapsed={userSettings.collapsed} collapsed={userSettings.collapsed}
onPageChange={onPageChange} onPageChange={onPageChange}
formatMessage={formatMessage} formatMessage={formatMessage}
@@ -67,6 +67,7 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
); );
const { update, summary, register } = useSummaryStatus(); const { update, summary, register } = useSummaryStatus();
const { addedCount, createModelsChunkRequest } = useAddWorkerMessage(); const { addedCount, createModelsChunkRequest } = useAddWorkerMessage();
console.log('addedCount=========', addedCount);
const onToggle = (open: boolean, key: string) => { const onToggle = (open: boolean, key: string) => {
setCollapseKey(open ? new Set([key]) : new Set()); setCollapseKey(open ? new Set([key]) : new Set());
@@ -81,6 +82,7 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
setCollapseKey(new Set([stepList[0]])); setCollapseKey(new Set([stepList[0]]));
}, [stepList]); }, [stepList]);
console.log('actionSource=========', actionSource, registrationInfo);
React.useEffect(() => { React.useEffect(() => {
// this effect is only triggered when used in cluster create page inner // this effect is only triggered when used in cluster create page inner
if (actionSource === 'page' && registrationInfo?.cluster_id) { if (actionSource === 'page' && registrationInfo?.cluster_id) {
@@ -164,6 +166,7 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
</> </>
)} )}
{actionSource === 'modal' && ( {actionSource === 'modal' && (
// show in cluster create page inner
<AddedMessage addedCount={addedCount}></AddedMessage> <AddedMessage addedCount={addedCount}></AddedMessage>
)} )}
</Container> </Container>
@@ -75,6 +75,7 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
}); });
const handleOnClusterChange = async (value: number, row?: any) => { const handleOnClusterChange = async (value: number, row?: any) => {
console.log('handleOnClusterChange value, row=========', value, row);
try { try {
createModelsChunkRequest({ cluster_id: value }); createModelsChunkRequest({ cluster_id: value });
axiosTokenRef.current?.cancel?.(); axiosTokenRef.current?.cancel?.();
@@ -121,6 +122,8 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
); );
}; };
console.log('addedCount 1========', addedCount);
return ( return (
<GSDrawer <GSDrawer
title={title} title={title}
@@ -1,6 +1,7 @@
import { workerAddedCountAtom } from '@/atoms/clusters'; import { workerAddedCountAtom } from '@/atoms/clusters';
import useSetChunkRequest from '@/hooks/use-chunk-request'; import useSetChunkRequest from '@/hooks/use-chunk-request';
import useUpdateChunkedList from '@/hooks/use-update-chunk-list'; import useUpdateChunkedList from '@/hooks/use-update-chunk-list';
import useQueryWorkerList from '@/pages/resources/services/use-query-worker-list';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
import qs from 'query-string'; import qs from 'query-string';
@@ -13,7 +14,31 @@ export default function useAddWorkerMessage() {
const [addedCount, setAddedCount] = useState(0); const [addedCount, setAddedCount] = useState(0);
const timerRef = useRef<any>(null); const timerRef = useRef<any>(null);
const triggerAtRef = useRef<number>(0); const triggerAtRef = useRef<number>(0);
const existingIdsRef = useRef<Set<string | number>>(new Set());
const snapshotReceivedRef = useRef<boolean>(false);
const [, setWorkerAddedCount] = useAtom(workerAddedCountAtom); const [, setWorkerAddedCount] = useAtom(workerAddedCountAtom);
// fetchData auto-cancels any in-flight request on each call and on unmount,
// so a stale seed from a previous open/cluster can't leak in
const { fetchData: fetchWorkerList, cancelRequest: cancelWorkerListRequest } =
useQueryWorkerList();
const isNewWorker = (item: any) => {
console.log(
'isNewWorker item:',
item,
existingIdsRef.current,
snapshotReceivedRef.current
);
if (item?.id == null) {
return false;
}
if (existingIdsRef.current.has(item.id)) {
return false;
}
existingIdsRef.current.add(item.id);
// before the full snapshot has been received, every item is pre-existing
return snapshotReceivedRef.current;
};
const updateAddedCount = (count: number) => { const updateAddedCount = (count: number) => {
setAddedCount(count); setAddedCount(count);
@@ -30,8 +55,9 @@ export default function useAddWorkerMessage() {
const { updateChunkedList } = useUpdateChunkedList({ const { updateChunkedList } = useUpdateChunkedList({
events: ['CREATE', 'INSERT'], events: ['CREATE', 'INSERT'],
dataList: [], dataList: [],
triggerAt: triggerAtRef, isNewItem: isNewWorker,
onCreate: (newItems: any) => { onCreate: (newItems: any) => {
console.log('onCreate newItems:', newItems, triggerAtRef.current);
if (triggerAtRef.current) { if (triggerAtRef.current) {
newItemsRef.current = newItemsRef.current.concat(newItems); newItemsRef.current = newItemsRef.current.concat(newItems);
showAddWorkerMessage(); showAddWorkerMessage();
@@ -49,6 +75,27 @@ export default function useAddWorkerMessage() {
_.each(list, (data: any) => { _.each(list, (data: any) => {
updateChunkedList(data); updateChunkedList(data);
}); });
// fallback: if seeding the baseline failed, the first watch chunk marks the
// snapshot as received (only reliable when there is at least one worker)
snapshotReceivedRef.current = true;
};
// Seed the baseline set of already-existing worker ids via REST before the
// watch starts. Relying on the watch's first chunk fails when a cluster has
// zero workers (no CREATE event is sent, so the snapshot flag never flips and
// genuinely new workers get misclassified as pre-existing).
const seedExistingWorkers = async (params: Record<string, any>) => {
try {
const items = await fetchWorkerList({ ...params, page: -1 } as any);
(items || []).forEach((item: any) => {
if (item?.id != null) {
existingIdsRef.current.add(item.id);
}
});
snapshotReceivedRef.current = true;
} catch (error) {
// ignore: fall back to the first watch chunk (see updateHandler)
}
}; };
const resetAddedCount = () => { const resetAddedCount = () => {
@@ -56,11 +103,18 @@ export default function useAddWorkerMessage() {
chunkRequestRef.current?.current?.cancel?.(); chunkRequestRef.current?.current?.cancel?.();
newItemsRef.current = []; newItemsRef.current = [];
triggerAtRef.current = 0; triggerAtRef.current = 0;
existingIdsRef.current = new Set();
snapshotReceivedRef.current = false;
// cancel any in-flight seed request
cancelWorkerListRequest();
clearTimeout(timerRef.current); clearTimeout(timerRef.current);
}; };
const createModelsChunkRequest = async (params = {}) => { const createModelsChunkRequest = async (params = {}) => {
resetAddedCount(); resetAddedCount();
// seed the baseline before watching so new workers are detected even when
// the cluster currently has zero workers
await seedExistingWorkers(params);
try { try {
chunkRequestRef.current = setChunkRequest({ chunkRequestRef.current = setChunkRequest({
url: `${WORKERS_API}?${qs.stringify(_.pickBy(params, (val: any) => !!val))}`, url: `${WORKERS_API}?${qs.stringify(_.pickBy(params, (val: any) => !!val))}`,
+8 -1
View File
@@ -6,7 +6,7 @@ import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, Table } from 'antd'; import { ConfigProvider, Table } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import { forwardRef, useImperativeHandle } from 'react'; import { forwardRef, useEffect, useImperativeHandle } from 'react';
import { import {
deleteModelInstance, deleteModelInstance,
MODEL_INSTANCE_API, MODEL_INSTANCE_API,
@@ -16,6 +16,7 @@ import ViewLogsModal from '../components/view-logs-modal';
import { useDeploymentsContext } from '../config/deploments-context'; import { useDeploymentsContext } from '../config/deploments-context';
import { ModelInstanceListItem as ListItem } from '../config/types'; import { ModelInstanceListItem as ListItem } from '../config/types';
import useViewInstanceLogs from '../hooks/use-view-instance-logs'; import useViewInstanceLogs from '../hooks/use-view-instance-logs';
import useQueryModelList from '../services/use-query-model-list';
import LeftFilters from './left-filters'; import LeftFilters from './left-filters';
import useInstanceColumns from './use-instance-columns'; import useInstanceColumns from './use-instance-columns';
@@ -44,10 +45,15 @@ const InstanceView = forwardRef((props, ref) => {
contentForDelete: 'menu.models.instances' contentForDelete: 'menu.models.instances'
}); });
const intl = useIntl(); const intl = useIntl();
const { dataList: modelList, fetchData: fetchModelList } =
useQueryModelList();
const { clusterList, workerList } = useDeploymentsContext(); const { clusterList, workerList } = useDeploymentsContext();
const { openViewLogsModal, openViewLogsModalStatus, closeViewLogsModal } = const { openViewLogsModal, openViewLogsModalStatus, closeViewLogsModal } =
useViewInstanceLogs(); useViewInstanceLogs();
useEffect(() => {
fetchModelList({ page: -1 });
}, []);
const handleSelect = useMemoizedFn((val: any, row: ListItem) => { const handleSelect = useMemoizedFn((val: any, row: ListItem) => {
if (val === 'delete') { if (val === 'delete') {
handleDelete(row, { handleDelete(row, {
@@ -119,6 +125,7 @@ const InstanceView = forwardRef((props, ref) => {
const columns = useInstanceColumns({ const columns = useInstanceColumns({
handleSelect, handleSelect,
clusterList, clusterList,
modelList,
workerList workerList
}); });
@@ -15,7 +15,10 @@ import InstanceStatusCell from '../components/instance-cells/instance-status-cel
import NameCell, { import NameCell, {
NameCellProps NameCellProps
} from '../components/instance-cells/name-cell'; } from '../components/instance-cells/name-cell';
import { ModelInstanceListItem as ListItem } from '../config/types'; import {
ModelInstanceListItem as ListItem,
ListItem as ModelListItem
} from '../config/types';
import { calcTotalVram } from '../utils'; import { calcTotalVram } from '../utils';
const WorkerInfoContent: React.FC<NameCellProps> = ({ record, modelData }) => { const WorkerInfoContent: React.FC<NameCellProps> = ({ record, modelData }) => {
let workerIp = '-'; let workerIp = '-';
@@ -48,6 +51,7 @@ const WorkerInfoContent: React.FC<NameCellProps> = ({ record, modelData }) => {
const useInstancesColumns = (options: { const useInstancesColumns = (options: {
workerList: workerListItem[]; workerList: workerListItem[];
modelList: ModelListItem[];
clusterList: Global.BaseOption< clusterList: Global.BaseOption<
number, number,
{ {
@@ -60,7 +64,8 @@ const useInstancesColumns = (options: {
onCellClick?: (record: ListItem, dataIndex: string) => void; onCellClick?: (record: ListItem, dataIndex: string) => void;
}): ColumnsType<ListItem> => { }): ColumnsType<ListItem> => {
const intl = useIntl(); const intl = useIntl();
const { workerList, clusterList, handleSelect, onCellClick } = options; const { workerList, clusterList, modelList, handleSelect, onCellClick } =
options;
const renderWorkerCell = (text: number, record: ListItem) => { const renderWorkerCell = (text: number, record: ListItem) => {
if (text) { if (text) {
@@ -144,7 +149,7 @@ const useInstancesColumns = (options: {
width: 160, width: 160,
render: (text: string, record: ListItem) => ( render: (text: string, record: ListItem) => (
<span style={{ gap: 4 }} className="flex-center"> <span style={{ gap: 4 }} className="flex-center">
<InstanceStatusCell record={record} onSelect={() => {}} /> <InstanceStatusCell record={record} onSelect={handleSelect} />
<DownloadingStatusCell <DownloadingStatusCell
backend={record?.backend} backend={record?.backend}
distributed_servers={record.distributed_servers} distributed_servers={record.distributed_servers}
@@ -173,15 +178,17 @@ const useInstancesColumns = (options: {
render: (value: string, record: ListItem) => ( render: (value: string, record: ListItem) => (
<ActionsCell <ActionsCell
record={record} record={record}
modelData={{ modelData={
categories: record.categories || [] modelList.find(
}} (model) => model.id === record.model_id
) as ModelListItem
}
onSelect={handleSelect} onSelect={handleSelect}
></ActionsCell> ></ActionsCell>
) )
} }
]; ];
}, [handleSelect, onCellClick, workerList, clusterList]); }, [handleSelect, onCellClick, workerList, clusterList, modelList]);
}; };
export default useInstancesColumns; export default useInstancesColumns;
@@ -0,0 +1,25 @@
import { useQueryDataList } from '@gpustack/core-ui';
import { queryWorkersList } from '../apis';
import { ListItem } from '../config/types';
export const useQueryWorkerList = (optons?: {
getLabel?: (item: ListItem) => string;
getValue?: (item: ListItem) => any;
}) => {
const { dataList, loading, fetchData, cancelRequest } = useQueryDataList<
ListItem,
Global.SearchParams
>({
key: 'workerList',
fetchList: queryWorkersList
});
return {
dataList,
loading,
fetchData,
cancelRequest
};
};
export default useQueryWorkerList;