跳到主要内容

Timetable

Overview

This document covers the Time Table screen (gpuTimetable / route name Timetable) — a 12‑hour × 30‑minute grid showing, per GPU pool, which workload/pod occupies GPU time and any scheduled priority “bumps” (cron/one-off priority changes), with drag-to-schedule UX. It is one of the three screens under the “Workload Management” LNB menu group (gpuWorkloadGrp — routes.manifest.json:305-312), alongside Scheduling (see 1-scheduling.mdx) and Workload (see 3-workload.mdx).

Time Table lives under src/modules/scheduler/dashboard/ (the “scheduler(volcano) 병합 모듈” — a merged Volcano-scheduler feature set), and is wired into the host router via src/pages/index.ts + src/router/route-defs.ts + routes.manifest.json. Both a system (/system/gpu/...) and a project (/proj/{project}/gpu/...) variant exist, sharing the same Vue component.

Route Design

Route names/constants: src/constants/routeNames/index.ts, paths: src/constants/routes/index.ts.

ScreenRoute name (system)Route name (project)PathComponent (lazy import)
Time TableTimetable (SCHEDULER_TIMETABLE)projectGpuSchedulerTimetable (PROJECT_SCHEDULER_TIMETABLE)/timetable → /system/gpu/timetable / /proj/{project}/gpu/timetable@/modules/scheduler/dashboard/pages/Scheduler/index.vue — src/pages/index.ts:41-42

Route registration/mapping: src/router/index.ts:36-52 (system componentMap) and :71-86 (project componentMap). Route metadata (title/icon/parent/apis) is declared in src/router/route-defs.ts:620-643 (Timetable def) and mirrored/generated into routes.manifest.json:753-859 (gpuTimetable menu entry) plus :1323-1434 for the project-mode projectGpuSchedulerTimetable entry. An old alias redirect exists for the legacy path (/scheduler/timetable → SCHEDULER_TIMETABLE) — src/constants/routes/index.ts:66.

UI Structure

Workload Management (LNB group, order=3)
├── Scheduling (order 1) → see 05-scheduling.md
├── Time Table (order 2) → Scheduler/index.vue ← THIS DOC
└── Workload (order 3) → see 05-workload.md

Time Table page (Scheduler/index.vue)

  • Header: breadcrumb, title, prev/next window nav buttons, datetime-local picker, “reset to now”, “refresh” button (index.vue:12-43).
  • ScopeFilter component (name / project / cluster / namespace) — :45-57.
  • Legend bar (Running/Waiting/Idle counts + color key) — :59-78.
  • Timeline frame: hour axis header, per-pool blocks, each with rows per workload/pod and a 24-cell (TOTAL_CELLS=24, 30 min each = 12h window) time grid; drag-select on cells opens the schedule dialog — :80-257.
  • “Unknown pool” section for bumps whose target pod isn’t currently matched to any running workload — :197-255.
  • Cell hover tooltip (custom, bypasses native title delay) — :259-268.
  • SchedulePriorityDialog modal (create/list priority bump schedules) — :270-280.
  • Bump detail/cancel popover — :282-308.

Component Design

Key components used by this screen (all under src/modules/scheduler/dashboard/components/ unless noted):

ComponentFileProps/Emits (from usage)
ScopeFiltercomponents/ScopeFilter/index.vuev-model:name/project/cluster/namespace, :projects/:clusters/:namespaces, :project-locked, :hide-namespace, @update:namespace
SchedulePriorityDialogcomponents/SchedulePriorityDialog/index.vueprops: open, workload-name, namespace, cluster, existing (ScheduledBumpItem[]), initial-once-local, initial-ttl-seconds; emits: close, changed
VolcanoMissingNoticecomponents/VolcanoMissingNotice/index.vue:detail

Composables consumed:

ComposableFilePurpose
useClusterTopologycomposables/useClusterTopology.tsPolls current GPU↔︎workload occupancy topology per cluster for “current baseline” cell rendering (currentGpuLabelFor) — used at Scheduler/index.vue:324,451
useProfileStore (Pinia)src/modules/scheduler/_host/useProfileStore.tsRealm/cluster/namespace/project selection state shared across the whole scheduler module
getHostProjectIdsrc/modules/scheduler/_host/utils/hostContext.tsDetects project-mode (/proj/{id}/...) to lock filters/scope

API Design

All Volcano-scheduler API calls are centralized in src/modules/scheduler/dashboard/apis/index.ts (base path /core/v1/scheduler, axios instance = services.axiosInstance from _host/baseUi.ts). GPU-pool “core” endpoints (a separate /core/v1/realms/{realm}/gpu/pools) live in src/modules/scheduler/dashboard/apis/coreGpu.ts (fetchCoreGpuPools), used to scope by project pool ownership.

FunctionMethod & PathUsed by (file:line)Notes
fetchWorkloads(namespace, gpuPool?, cluster?, project?)GET {realm-cluster}/workloadsScheduler/index.vue:319,530Normalizes 2 possible BE response shapes via normalizeWorkload() (apis/index.ts:184-276). Auto-injects project from getHostProjectId() if caller omits it (apis/index.ts:328).
listScheduledBumps(filter?, cluster?)GET {realm-cluster}/scheduled-bumpsScheduler/index.vue:318,529Returns ScheduledBumpItem[] (cron/once priority-bump records)
createScheduledBump(body, cluster?)POST \{realm-cluster\}/scheduled-bumpsinside SchedulePriorityDialog/index.vue (not shown in excerpt, referenced by dialog’s create flow)body: ScheduledBumpCreateRequest (namespace/pod/schedule/is_cron/target_priority/ttl_seconds/timezone/end_action)
deleteScheduledBump(pairId, cluster?)DELETE {realm-cluster}/scheduled-bumps/{pairId}Scheduler/index.vue:323,1204Called from bump-cancel popover (onCancel)
elevateWorkloadPriority(namespace, podName, priority, cluster?)POST \{realm-cluster\}/actions/direct-runreferenced by priority-run flows (see 3-workload.mdx)priority 100–900 (P1–P9)
fetchCoreGpuPools()GET /core/v1/realms/{realm}/gpu/poolsapis/coreGpu.tsPool ownership ledger, used to scope “my project’s pools”
resolveCluster() / resolveRealm()n/a (client helper)this pageSee Business Logic — cluster resolution/guard rules

Route-level API permission bindings for this screen (used by the host’s manifest-based RBAC, DERIVED_ONLY mode) are declared in the schedulerTimetableApis constant — src/router/route-defs.ts:240-260 — and mirrored in routes.manifest.json:761-802 (gpuTimetable). Key mutating endpoints requiring edit verb: POST .../scheduled-bumps, DELETE .../scheduled-bumps/*, POST .../actions/direct-run-internal.

State Management

  • Pinia store: useProfileStore (src/modules/scheduler/_host/useProfileStore.ts, store id from STORES.PROFILE, defined with the Composition API defineStore setup syntax). Holds realm/cluster list (clusters, schedulerClusters), selectedCluster, selectedNamespace, selectedProject, projects, namespaces, and ensureProfile() to lazy-load the profile. This page reads/writes this store for scope filters (Scheduler/index.vue:333,356-384).
  • Local component state (no store): this page otherwise uses plain ref/reactive/computed local to the SFC — no dedicated Pinia store for the page. It keeps workloads, bumps, windowStart, filterCluster/Namespace/Name, drag-selection state, etc. as page-local refs (Scheduler/index.vue:362-497).
  • Cross-page cache: none observed — the page fetches independently on mount/poll; no shared workload cache/store with the Workload list page (each calls fetchWorkloads separately with its own params).

Business Logic

  1. Cluster resolution / guard rule — resolveCluster() (apis/index.ts:67-83): explicit selection (?cluster= query or store.selectedCluster) wins; if realm has exactly 1 cluster it auto-selects; if it has >1 clusters and none explicitly chosen it throws (“클러스터를 먼저 선택해 주세요.”). Time Table instead defaults to an “All” fan-out across profileStore.schedulerClusters (or .clusters fallback) rather than requiring single-cluster selection (Scheduler/index.vue:501-520).
  2. Default/root queue exclusion — Timetable excludes K8s/Volcano default queues from visualization: pool === 'default' || pool.startsWith('default-') || pool === 'root' (Scheduler/index.vue:895).
  3. Project isolation (“설계 ④”) — When in project mode (getHostProjectId() truthy), Timetable narrows pools to myPoolNames (loaded via fetchCoreGpuPools() filtered by p.project === hostProjectId) — Scheduler/index.vue:457-471,897.
  4. Priority class mapping — priorityClassToValue() (apis/index.ts:174-182): p1–p9 → 100–900; default-batch → 100; unknown → null. Priority badge display divides by 100 and prefixes P (Scheduler/index.vue:751).
  5. Scheduled priority bump “effective span” & lowering detection — buildCells() (Scheduler/index.vue:670-822): each bump’s end = start + ttl_seconds*1000 if TTL set, else +Infinity (manual revert only). A bump is flagged isLowering if target_priority < runningPriority (chained from the previous bump’s resulting priority) — rendered with a stripe overlay instead of a separate color (Scheduler/index.vue:700,772-775).
  6. Stale/“keep-last-good” anti-flicker guard — Timetable’s polling reload ignores a transient empty response if the previous non-empty data is younger than STALE_KEEP_MS = 75_000 ms (~2.5 poll cycles), to avoid MCM-proxy timeout/graceful-degrade empty arrays wiping the grid (Scheduler/index.vue:490-568).
  7. Stale-response race guard — a monotonically increasing reloadGen counter ensures a slow-to-resolve reload doesn’t overwrite a newer one’s results (Scheduler/index.vue:489,498,548).
  8. Time-window paging constraints — Timetable cannot page into the past before “now” (canPrev = startMs > nowFloor), but has no upper bound into the future (canNext always true) — Scheduler/index.vue:591-592.
  9. Pod identity matching across rollouts — matchPod() (Scheduler/index.vue:844-855) treats two pod names as the same logical workload if they match after stripping the last 1 or 2 delimited tokens (K8s ReplicaSet/pod hash suffixes), so a rolling update doesn’t lose its schedule/occupancy association.
  10. RBAC via route manifest (DERIVED_ONLY) — API-level permission is derived solely from the apis array attached to this route’s menu entry; any endpoint not listed there returns 403 for non-super-admin roles even if reachable in code (route-defs.ts:133-144 comment).
  11. Cron-wait progress UX rule — after creating/deleting a scheduled bump, the UI polls (reload(true)) up to a fixed number of times with a fake progress percentage until the change is confirmed reflected (awaitCron(), Scheduler/index.vue:427-445,1206).

Data Flow

Time Table load cycle:
onMounted
→ profileStore.ensureProfile()
→ reload()
→ resolve target clusters (filterCluster || schedulerClusters || clusters || resolveCluster())
→ Promise.all per cluster: listScheduledBumps() + fetchWorkloads()
→ topology.refresh() (useClusterTopology)
→ commit workloads/bumps refs (with stale-guard)
→ poolRows computed (groups workloads by queue, builds per-pod CellState[] via buildCells())
→ template renders pool blocks + time grid
setInterval(30s) → reload(true) [silent poll]
Time Table user action — schedule a priority bump:
User drags across cells (mousedown → mouseenter × N → mouseup)
→ onCellDown/onCellDragOver track dragSel
→ onCellDragUp computes span → openSchedule(wl, startCellIndex, ttlSeconds)
→ schedule.value = `&#123;$([regex]::Match({open:true, workloadName, namespace, cluster, initialOnceLocal, initialTtlSeconds}, "(?<=\{)(.*?)(?=\})").Value &#125;`
→ <SchedulePriorityDialog :open> renders CREATE view
→ user picks once/cron + target priority + TTL + end-action → dialog calls createScheduledBump()
→ dialog emits 'changed' → onBumpChanged() → awaitCron() polls reload(true) until new bump visible
→ poolRows recomputed → new orange "bump" cell rendered with tooltip
Time Table user action — cancel a bump:
User clicks bump cell → onBumpClick sets selectedBump
→ bump popover shown → user clicks "Cancel" → onCancel(pairId)
→ deleteScheduledBump(pairId, cluster)
→ awaitCron(..., done = () => bump no longer in bumps.value) polls reload(true)
→ popover closes, cell reverts to baseline/idle

Dependency Graph

src/pages/index.ts
└─(dyn import)→ src/modules/scheduler/dashboard/pages/Scheduler/index.vue

src/router/index.ts ──uses names from── src/constants/routeNames/index.ts
src/router/route-defs.ts ──uses paths from── src/constants/routes/index.ts
routes.manifest.json ──generated/mirrors── src/router/route-defs.ts (per build-manifest script, referenced in route-defs.ts:1-9)

Scheduler/index.vue
├─→ @/modules/scheduler/dashboard/apis (fetchWorkloads, listScheduledBumps, deleteScheduledBump, resolveCluster)
├─→ @/modules/scheduler/dashboard/apis/coreGpu (fetchCoreGpuPools)
├─→ @/modules/scheduler/dashboard/composables/useClusterTopology
├─→ @/modules/scheduler/dashboard/constants (POOL_CATEGORIES, categorizePool)
├─→ @/modules/scheduler/dashboard/components/SchedulePriorityDialog/index.vue
├─→ @/modules/scheduler/dashboard/components/VolcanoMissingNotice/index.vue
├─→ @/modules/scheduler/dashboard/components/ScopeFilter/index.vue
├─→ @/modules/scheduler/_host/useProfileStore
├─→ @/modules/scheduler/_host/utils/hostContext (getHostProjectId)
└─→ @/constants/routeNames (ROUTE_NAMES)

@/modules/scheduler/dashboard/apis/index.ts
├─→ @/modules/scheduler/_host/baseUi (services.axiosInstance, getRealm)
├─→ @/modules/scheduler/_host/utils/hostContext (getHostProjectId)
├─→ @/modules/scheduler/_host/utils/projectScope (injectProjectParam)
├─→ @/modules/scheduler/_host/useProfileStore
└─→ @/modules/scheduler/common/types/server, common/types/client (type defs)

Sequence Flow

A. Enter Time Table (system mode), initial render:
  1. Router resolves Timetable → lazy-loads Scheduler/index.vue.
  2. onMounted fires → profileStore.ensureProfile() resolves realm/cluster list.
  3. reload() runs: computes targets (all Volcano clusters), fan-out listScheduledBumps() + fetchWorkloads() per cluster, then topology.refresh().
  4. Results committed to workloads/bumps refs → poolRows computed re-derives grid.
  5. Template renders pool blocks; loading flips false; 30s poll timer + 60s clock timer start.
B. User schedules a one-off priority bump by drag-selecting cells:
  1. mousedown on cell N → onCellDown starts dragSel.
  2. mouseenter on later cells while held → onCellDragOver extends dragSel.to.
  3. mouseup (window listener, &#123;$([regex]::Match({once:true}, "(?<=\{)(.*?)(?=\})").Value &#125;) → onCellDragUp computes span length, converts to TTL seconds, calls openSchedule(wl, startIndex, ttlSeconds).
  4. SchedulePriorityDialog opens in CREATE mode pre-filled with start time + TTL.
  5. User selects target priority (P1–P9), optionally toggles TTL/end-action, submits.
  6. Dialog internally calls createScheduledBump() (POST /scheduled-bumps), emits changed.
  7. onBumpChanged() → awaitCron() polls reload(true) (silent) up to 12× @800ms apart, showing a fake progress bar (cronWait) until the new bump round-trips through the cron-backed backend and appears in listScheduledBumps().
  8. poolRows/buildCells() recompute → an orange “bump” cell appears with tooltip describing direction (raise/lower), priority, end time, and next execution time.
C. User cancels a bump:
  1. User clicks bump cell → onBumpClick sets selectedBump.
  2. Bump popover shown → user clicks “Cancel” → onCancel(pairId).
  3. deleteScheduledBump(pairId, cluster) sent.
  4. awaitCron(..., done = () => bump no longer in bumps.value) polls reload(true).
  5. Popover closes, cell reverts to baseline/idle.

Key Findings

  • Time Table is purely a read + narrow-write UI over the Volcano scheduler API surface centralized in src/modules/scheduler/dashboard/apis/index.ts.
  • The page implements client-side multi-cluster fan-out (“All” cluster mode) because the backend /workloads and /scheduled-bumps endpoints are single-cluster only.
  • No dedicated Pinia store exists for Timetable page state — only the shared useProfileStore (realm/cluster/project/namespace scope) is used; all page-specific data (workload list, bumps, filters, drag/dialog state) lives in local ref/reactive inside the SFC <script setup>.
  • The page routes project isolation through getHostProjectId() + a “GPU pool ownership ledger” (fetchCoreGpuPools) as an additional safety net beyond label matching — this pattern is explicitly called out in code comments as security-relevant (“설계 ④”, “IDOR fail-open 차단”).
  • Scheduled priority bumps are implemented as paired Kubernetes CronJobs on the backend (per code comments in apis/index.ts:380-382), which is why the frontend must poll (awaitCron) rather than trust an immediate synchronous result.

Risks / Technical Debt

  • Scheduler/index.vue is a large monolithic SFC (2000+ lines including styles) with substantial inline business logic (cell-building, filter derivation, priority math) — no unit test files were found colocated for its main logic.
  • Heavy reliance on loosely-typed any/Record<string, unknown> for raw workload/bump shapes (workloads = ref<any[]>([]) in Scheduler/index.vue:577; wls: any[] in reload) — normalization logic (normalizeWorkload) attempts to reconcile two backend response shapes, which is inherently fragile to backend contract drift.
  • Pod identity matching by name-prefix truncation could mis-associate unrelated pods sharing a common prefix in edge cases.
  • The “keep-last-good” stale-data guard (STALE_KEEP_MS) is a client-side workaround for a backend graceful-degrade behavior (empty-array 200 on proxy timeout) rather than a fix at the source — noted directly in comments as a “보완책” (mitigation), referencing PR #337/#779.
  • Project-mode workload filtering trusts 2 independent signals (pool ledger + labels) with a fallback “if pool list not loaded yet, allow through” behavior (Scheduler/index.vue:897: isProjectMode && myPoolNames.value.size && ...) — during the loading window before myPoolNames populates, unfiltered cross-project data could transiently render.

Assumptions

  • SchedulePriorityDialog’s internal call to createScheduledBump() was inferred from the surrounding page’s emit-driven flow (@changed="onBumpChanged") and the exported createScheduledBump function signature in apis/index.ts; the dialog’s full internal script was not read in this pass (truncated large-file budget) — treat the exact call site inside SchedulePriorityDialog/index.vue’s <script> as Not Found in Code (not directly viewed) though its existence is strongly implied by the API module and UI text.

  • routes.manifest.json
  • src/router/route-defs.ts
  • src/router/index.ts
  • src/constants/routeNames/index.ts
  • src/constants/routes/index.ts
  • src/pages/index.ts
  • src/modules/scheduler/dashboard/pages/Scheduler/index.vue
  • src/modules/scheduler/dashboard/apis/index.ts
  • src/modules/scheduler/dashboard/apis/coreGpu.ts
  • src/modules/scheduler/dashboard/components/ScopeFilter/index.vue
  • src/modules/scheduler/dashboard/components/SchedulePriorityDialog/index.vue
  • src/modules/scheduler/dashboard/components/VolcanoMissingNotice/index.vue
  • src/modules/scheduler/dashboard/composables/useClusterTopology.ts
  • src/modules/scheduler/dashboard/constants/index.ts
  • src/modules/scheduler/dashboard/constants/poolCategories.ts
  • src/modules/scheduler/_host/useProfileStore.ts
  • src/modules/scheduler/_host/baseUi.ts
  • src/modules/scheduler/_host/utils/hostContext.ts
  • src/modules/scheduler/_host/utils/projectScope.ts
  • src/modules/scheduler/common/types/server/index.ts
  • src/modules/scheduler/common/types/client/index.ts
  • src/i18n/locales/en/dash-scheduler.ts (and ja/ko equivalents)