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: {
events?: EventsType[];
dataList?: any[];
triggerAt?: React.MutableRefObject<number>;
limit?: number;
onCreate?: (args: any) => void;
onUpdate?: (args: any) => void;
@@ -24,9 +23,9 @@ export function useUpdateChunkedList(options: {
filterFun?: (args: any) => boolean;
mapFun?: (args: any) => any;
computedID?: (d: object) => string;
isNewItem?: (item: any) => boolean;
}) {
const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'], triggerAt } =
options;
const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'] } = options;
const deletedIdsRef = useRef<Set<number | string>>(new Set());
const cacheDataListRef = useRef<any[]>(options.dataList || []);
const timerRef = useRef<any>(null);
@@ -71,17 +70,14 @@ export function useUpdateChunkedList(options: {
(sItem: any) => sItem.id === item.id
);
const updateItem = { ...item };
if (updateIndex === -1 && !triggerAt?.current) {
if (updateIndex === -1) {
acc.push(updateItem);
} else if (!triggerAt?.current) {
} else {
cacheDataListRef.current[updateIndex] = updateItem;
}
// only push items created after the watch started
// TODO only push items created after triggerAt
if (
triggerAt?.current &&
Date.parse(item.created_at) >= triggerAt.current
) {
if (options.isNewItem?.(item)) {
latestCreateList.push(updateItem);
}
@@ -103,10 +99,8 @@ export function useUpdateChunkedList(options: {
cacheDataListRef.current = cacheDataListRef.current?.filter(
(item: any) => {
// collect deleted items
if (triggerAt?.current) {
if (ids?.includes(item.id)) {
deletedList.push(item);
}
if (ids?.includes(item.id) && !options.isNewItem?.(item)) {
deletedList.push(item);
}
return !ids?.includes(item.id);
}
@@ -133,10 +127,8 @@ export function useUpdateChunkedList(options: {
updateItem,
...cacheDataListRef.current.slice(0, limit - 1)
];
if (options.onUpdate && triggerAt?.current) {
if (Date.parse(item.created_at) >= triggerAt.current) {
options.onUpdate?.([updateItem]);
}
if (options.onUpdate && options.isNewItem?.(item)) {
options.onUpdate?.([updateItem]);
}
}
});
-1
View File
@@ -396,7 +396,6 @@ export default (props: any) => {
)}
onCollapse={onCollapse}
onMenuHeaderClick={onMenuHeaderClick}
menuHeaderRender={renderMenuHeader}
collapsed={userSettings.collapsed}
onPageChange={onPageChange}
formatMessage={formatMessage}
@@ -67,6 +67,7 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
);
const { update, summary, register } = useSummaryStatus();
const { addedCount, createModelsChunkRequest } = useAddWorkerMessage();
console.log('addedCount=========', addedCount);
const onToggle = (open: boolean, key: string) => {
setCollapseKey(open ? new Set([key]) : new Set());
@@ -81,6 +82,7 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
setCollapseKey(new Set([stepList[0]]));
}, [stepList]);
console.log('actionSource=========', actionSource, registrationInfo);
React.useEffect(() => {
// this effect is only triggered when used in cluster create page inner
if (actionSource === 'page' && registrationInfo?.cluster_id) {
@@ -164,6 +166,7 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
</>
)}
{actionSource === 'modal' && (
// show in cluster create page inner
<AddedMessage addedCount={addedCount}></AddedMessage>
)}
</Container>
@@ -75,6 +75,7 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
});
const handleOnClusterChange = async (value: number, row?: any) => {
console.log('handleOnClusterChange value, row=========', value, row);
try {
createModelsChunkRequest({ cluster_id: value });
axiosTokenRef.current?.cancel?.();
@@ -121,6 +122,8 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
);
};
console.log('addedCount 1========', addedCount);
return (
<GSDrawer
title={title}
@@ -1,6 +1,7 @@
import { workerAddedCountAtom } from '@/atoms/clusters';
import useSetChunkRequest from '@/hooks/use-chunk-request';
import useUpdateChunkedList from '@/hooks/use-update-chunk-list';
import useQueryWorkerList from '@/pages/resources/services/use-query-worker-list';
import { useAtom } from 'jotai';
import _ from 'lodash';
import qs from 'query-string';
@@ -13,7 +14,31 @@ export default function useAddWorkerMessage() {
const [addedCount, setAddedCount] = useState(0);
const timerRef = useRef<any>(null);
const triggerAtRef = useRef<number>(0);
const existingIdsRef = useRef<Set<string | number>>(new Set());
const snapshotReceivedRef = useRef<boolean>(false);
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) => {
setAddedCount(count);
@@ -30,8 +55,9 @@ export default function useAddWorkerMessage() {
const { updateChunkedList } = useUpdateChunkedList({
events: ['CREATE', 'INSERT'],
dataList: [],
triggerAt: triggerAtRef,
isNewItem: isNewWorker,
onCreate: (newItems: any) => {
console.log('onCreate newItems:', newItems, triggerAtRef.current);
if (triggerAtRef.current) {
newItemsRef.current = newItemsRef.current.concat(newItems);
showAddWorkerMessage();
@@ -49,6 +75,27 @@ export default function useAddWorkerMessage() {
_.each(list, (data: any) => {
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 = () => {
@@ -56,11 +103,18 @@ export default function useAddWorkerMessage() {
chunkRequestRef.current?.current?.cancel?.();
newItemsRef.current = [];
triggerAtRef.current = 0;
existingIdsRef.current = new Set();
snapshotReceivedRef.current = false;
// cancel any in-flight seed request
cancelWorkerListRequest();
clearTimeout(timerRef.current);
};
const createModelsChunkRequest = async (params = {}) => {
resetAddedCount();
// seed the baseline before watching so new workers are detected even when
// the cluster currently has zero workers
await seedExistingWorkers(params);
try {
chunkRequestRef.current = setChunkRequest({
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 { ConfigProvider, Table } from 'antd';
import _ from 'lodash';
import { forwardRef, useImperativeHandle } from 'react';
import { forwardRef, useEffect, useImperativeHandle } from 'react';
import {
deleteModelInstance,
MODEL_INSTANCE_API,
@@ -16,6 +16,7 @@ import ViewLogsModal from '../components/view-logs-modal';
import { useDeploymentsContext } from '../config/deploments-context';
import { ModelInstanceListItem as ListItem } from '../config/types';
import useViewInstanceLogs from '../hooks/use-view-instance-logs';
import useQueryModelList from '../services/use-query-model-list';
import LeftFilters from './left-filters';
import useInstanceColumns from './use-instance-columns';
@@ -44,10 +45,15 @@ const InstanceView = forwardRef((props, ref) => {
contentForDelete: 'menu.models.instances'
});
const intl = useIntl();
const { dataList: modelList, fetchData: fetchModelList } =
useQueryModelList();
const { clusterList, workerList } = useDeploymentsContext();
const { openViewLogsModal, openViewLogsModalStatus, closeViewLogsModal } =
useViewInstanceLogs();
useEffect(() => {
fetchModelList({ page: -1 });
}, []);
const handleSelect = useMemoizedFn((val: any, row: ListItem) => {
if (val === 'delete') {
handleDelete(row, {
@@ -119,6 +125,7 @@ const InstanceView = forwardRef((props, ref) => {
const columns = useInstanceColumns({
handleSelect,
clusterList,
modelList,
workerList
});
@@ -15,7 +15,10 @@ import InstanceStatusCell from '../components/instance-cells/instance-status-cel
import NameCell, {
NameCellProps
} 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';
const WorkerInfoContent: React.FC<NameCellProps> = ({ record, modelData }) => {
let workerIp = '-';
@@ -48,6 +51,7 @@ const WorkerInfoContent: React.FC<NameCellProps> = ({ record, modelData }) => {
const useInstancesColumns = (options: {
workerList: workerListItem[];
modelList: ModelListItem[];
clusterList: Global.BaseOption<
number,
{
@@ -60,7 +64,8 @@ const useInstancesColumns = (options: {
onCellClick?: (record: ListItem, dataIndex: string) => void;
}): ColumnsType<ListItem> => {
const intl = useIntl();
const { workerList, clusterList, handleSelect, onCellClick } = options;
const { workerList, clusterList, modelList, handleSelect, onCellClick } =
options;
const renderWorkerCell = (text: number, record: ListItem) => {
if (text) {
@@ -144,7 +149,7 @@ const useInstancesColumns = (options: {
width: 160,
render: (text: string, record: ListItem) => (
<span style={{ gap: 4 }} className="flex-center">
<InstanceStatusCell record={record} onSelect={() => {}} />
<InstanceStatusCell record={record} onSelect={handleSelect} />
<DownloadingStatusCell
backend={record?.backend}
distributed_servers={record.distributed_servers}
@@ -173,15 +178,17 @@ const useInstancesColumns = (options: {
render: (value: string, record: ListItem) => (
<ActionsCell
record={record}
modelData={{
categories: record.categories || []
}}
modelData={
modelList.find(
(model) => model.id === record.model_id
) as ModelListItem
}
onSelect={handleSelect}
></ActionsCell>
)
}
];
}, [handleSelect, onCellClick, workerList, clusterList]);
}, [handleSelect, onCellClick, workerList, clusterList, modelList]);
};
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;