Skip to content

Commit d27b474

Browse files
committed
refactor(FR-1653): extract session creation logic into useStartSession hook
1 parent c810e47 commit d27b474

File tree

2 files changed

+399
-402
lines changed

2 files changed

+399
-402
lines changed
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
import { useSuspendedBackendaiClient } from '.';
2+
import { useSetBAINotification } from './useBAINotification';
3+
import {
4+
useCurrentProjectValue,
5+
useCurrentResourceGroupState,
6+
} from './useCurrentProject';
7+
import { toGlobalId } from 'backend.ai-ui';
8+
import _ from 'lodash';
9+
import { useTranslation } from 'react-i18next';
10+
import { fetchQuery, graphql, useRelayEnvironment } from 'react-relay';
11+
import { useStartSessionCreationQuery } from 'src/__generated__/useStartSessionCreationQuery.graphql';
12+
import { transformPortValuesToNumbers } from 'src/components/PortSelectFormItem';
13+
import { RESOURCE_ALLOCATION_INITIAL_FORM_VALUES } from 'src/components/SessionFormItems/ResourceAllocationFormItems';
14+
import { generateRandomString } from 'src/helper';
15+
import {
16+
SessionLauncherFormValue,
17+
SessionResources,
18+
} from 'src/pages/SessionLauncherPage';
19+
20+
// Type for successful session creation result
21+
type SessionCreationSuccess = {
22+
kernelId?: string;
23+
sessionId: string;
24+
sessionName: string;
25+
servicePorts: Array<{ name: string }>;
26+
};
27+
interface CreateSessionInfo {
28+
kernelName: string;
29+
sessionName: string;
30+
architecture: string;
31+
batchTimeout?: string;
32+
resources: SessionResources;
33+
}
34+
35+
export const SESSION_LAUNCHER_NOTI_PREFIX = 'session-launcher:';
36+
37+
export const useStartSession = () => {
38+
'use memo';
39+
40+
const { t } = useTranslation();
41+
const currentProject = useCurrentProjectValue();
42+
const { upsertNotification } = useSetBAINotification();
43+
44+
const relayEnv = useRelayEnvironment();
45+
const baiClient = useSuspendedBackendaiClient();
46+
const supportsMountById = baiClient.supports('mount-by-id');
47+
const supportBatchTimeout = baiClient?.supports('batch-timeout') ?? false;
48+
49+
const [currentGlobalResourceGroup] = useCurrentResourceGroupState();
50+
51+
const defaultFormValues: DeepPartial<SessionLauncherFormValue> = {
52+
sessionType: 'interactive',
53+
// If you set `allocationPreset` to 'custom', `allocationPreset` is not changed automatically any more.
54+
allocationPreset: 'auto-select',
55+
hpcOptimization: {
56+
autoEnabled: true,
57+
},
58+
batch: {
59+
enabled: false,
60+
command: undefined,
61+
scheduleDate: undefined,
62+
...(supportBatchTimeout && {
63+
timeoutEnabled: false,
64+
timeout: undefined,
65+
timeoutUnit: 's',
66+
}),
67+
},
68+
envvars: [],
69+
// set default_session_environment only if set
70+
...(baiClient._config?.default_session_environment && {
71+
environments: {
72+
environment: baiClient._config?.default_session_environment,
73+
},
74+
}),
75+
...RESOURCE_ALLOCATION_INITIAL_FORM_VALUES,
76+
resourceGroup: currentGlobalResourceGroup || undefined,
77+
};
78+
79+
const startSession = async (values: SessionLauncherFormValue) => {
80+
// If manual image is selected, use it as kernelName
81+
const imageFullName =
82+
values.environments.manual || values.environments.version;
83+
const [kernelName, architecture] = imageFullName
84+
? imageFullName.split('@')
85+
: ['', ''];
86+
87+
const sessionName = _.isEmpty(values.sessionName)
88+
? generateSessionId()
89+
: values.sessionName;
90+
91+
const sessionInfo: CreateSessionInfo = {
92+
// Basic session information
93+
sessionName: sessionName,
94+
kernelName,
95+
architecture,
96+
resources: {
97+
enqueueOnly: true,
98+
// Project and domain settings
99+
group_name: values.owner?.enabled
100+
? values.owner.project
101+
: currentProject.name,
102+
domain: values.owner?.enabled
103+
? values.owner.domainName
104+
: baiClient._config.domainName,
105+
106+
// Session configuration
107+
type: values.sessionType,
108+
cluster_mode: values.cluster_mode,
109+
cluster_size: values.cluster_size,
110+
maxWaitSeconds: 15,
111+
112+
// Owner settings (optional)
113+
// FYI, `config.scaling_group` also changes based on owner settings
114+
...(values.owner?.enabled
115+
? {
116+
owner_access_key: values.owner.accesskey,
117+
}
118+
: {}),
119+
120+
// Batch mode settings (optional)
121+
...(values.sessionType === 'batch'
122+
? {
123+
starts_at: values.batch.enabled
124+
? values.batch.scheduleDate
125+
: undefined,
126+
startupCommand: values.batch.command,
127+
}
128+
: {}),
129+
130+
// Bootstrap script (optional)
131+
...(values.bootstrap_script
132+
? { bootstrap_script: values.bootstrap_script }
133+
: {}),
134+
135+
// Batch timeout configuration (optional)
136+
...(supportBatchTimeout &&
137+
values?.batch?.timeoutEnabled &&
138+
!_.isUndefined(values?.batch?.timeout)
139+
? {
140+
batchTimeout:
141+
_.toString(values.batch.timeout) + values?.batch?.timeoutUnit,
142+
}
143+
: undefined),
144+
145+
config: {
146+
// Resource allocation
147+
resources: {
148+
cpu: values.resource.cpu,
149+
mem: values.resource.mem,
150+
// Add accelerator only if specified
151+
...(values.resource.accelerator > 0
152+
? {
153+
[values.resource.acceleratorType]:
154+
values.resource.accelerator,
155+
}
156+
: undefined),
157+
},
158+
scaling_group: values.owner?.enabled
159+
? values.owner.project
160+
: values.resourceGroup,
161+
resource_opts: {
162+
shmem: values.resource.shmem,
163+
// allow_fractional_resource_fragmentation can be added here if needed
164+
},
165+
166+
// Storage configuration
167+
[supportsMountById ? 'mount_ids' : 'mounts']: values.mount_ids,
168+
[supportsMountById ? 'mount_id_map' : 'mount_map']:
169+
values.mount_id_map,
170+
171+
// Environment variables
172+
environ: {
173+
..._.fromPairs(values.envvars.map((v) => [v.variable, v.value])),
174+
// set hpcOptimization options: "OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS"
175+
...(values.hpcOptimization.autoEnabled
176+
? {}
177+
: _.omit(values.hpcOptimization, 'autoEnabled')),
178+
},
179+
180+
// Networking
181+
preopen_ports: transformPortValuesToNumbers(values.ports),
182+
183+
// Agent selection (optional)
184+
...(baiClient.supports('agent-select') &&
185+
!baiClient?._config?.hideAgents &&
186+
values.agent !== 'auto'
187+
? {
188+
// Filter out undefined values
189+
agent_list: [values.agent].filter(
190+
(agent): agent is string => !!agent,
191+
),
192+
}
193+
: undefined),
194+
},
195+
},
196+
};
197+
const sessionPromises = _.map(_.range(values.num_of_sessions || 1), (i) => {
198+
const formattedSessionName =
199+
(values.num_of_sessions || 1) > 1
200+
? `${sessionInfo.sessionName}-${generateRandomString()}-${i}`
201+
: sessionInfo.sessionName;
202+
return baiClient
203+
.createIfNotExists(
204+
sessionInfo.kernelName,
205+
formattedSessionName,
206+
sessionInfo.resources,
207+
undefined,
208+
sessionInfo.architecture,
209+
)
210+
.then((res: { created: boolean; status: string }) => {
211+
// // When session is already created with the same name, the status code
212+
// // is 200, but the response body has 'created' field as false. For better
213+
// // user experience, we show the notification message.
214+
if (!res?.created) {
215+
// message.warning(t('session.launcher.SessionAlreadyExists'));
216+
throw new Error(t('session.launcher.SessionAlreadyExists'));
217+
}
218+
if (res?.status === 'CANCELLED') {
219+
// Case about failed to start new session kind of "docker image not found" or etc.
220+
throw new Error(t('session.launcher.FailedToStartNewSession'));
221+
}
222+
return res;
223+
})
224+
.catch((err: any) => {
225+
if (err?.message?.includes('The session already exists')) {
226+
throw new Error(t('session.launcher.SessionAlreadyExists'));
227+
} else {
228+
throw err;
229+
}
230+
});
231+
});
232+
233+
return Promise.allSettled(sessionPromises).then(
234+
async (
235+
sessionCreations: PromiseSettledResult<SessionCreationSuccess>[],
236+
) => {
237+
// Group session creations by their status
238+
const results = _.groupBy(sessionCreations, 'status') as {
239+
fulfilled?: PromiseFulfilledResult<SessionCreationSuccess>[];
240+
rejected?: PromiseRejectedResult[];
241+
};
242+
243+
return results;
244+
},
245+
);
246+
};
247+
248+
const upsertSessionNotification = async (
249+
successCreations: PromiseFulfilledResult<SessionCreationSuccess>[],
250+
upsertNotificationOverrides?: Parameters<typeof upsertNotification>,
251+
) => {
252+
const promises = _.map(successCreations, (creation) => {
253+
const session = creation.value;
254+
return fetchQuery<useStartSessionCreationQuery>(
255+
relayEnv,
256+
graphql`
257+
query useStartSessionCreationQuery($id: GlobalIDField!) {
258+
compute_session_node(id: $id) {
259+
...BAINodeNotificationItemFragment
260+
}
261+
}
262+
`,
263+
{
264+
id: toGlobalId('ComputeSessionNode', session.sessionId),
265+
},
266+
)
267+
.toPromise()
268+
.then((queryResult) => {
269+
const createdSession = queryResult?.compute_session_node ?? null;
270+
271+
if (createdSession) {
272+
upsertNotification(
273+
{
274+
key: `${SESSION_LAUNCHER_NOTI_PREFIX}${session.sessionId}`,
275+
node: createdSession,
276+
open: true,
277+
duration: 0,
278+
...upsertNotificationOverrides?.[0],
279+
},
280+
upsertNotificationOverrides?.[1],
281+
);
282+
}
283+
});
284+
});
285+
return Promise.allSettled(promises);
286+
};
287+
288+
return { startSession, defaultFormValues, upsertSessionNotification };
289+
};
290+
291+
const generateSessionId = () => {
292+
let text = '';
293+
const possible =
294+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
295+
for (let i = 0; i < 8; i++) {
296+
text += possible.charAt(Math.floor(Math.random() * possible.length));
297+
}
298+
return text + '-session';
299+
};

0 commit comments

Comments
 (0)