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.
| Screen | Route name (system) | Route name (project) | Path | Component (lazy import) |
|---|---|---|---|---|
| Time Table | Timetable (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):
| Component | File | Props/Emits (from usage) |
|---|---|---|
| ScopeFilter | components/ScopeFilter/index.vue | v-model:name/project/cluster/namespace, :projects/:clusters/:namespaces, :project-locked, :hide-namespace, @update:namespace |
| SchedulePriorityDialog | components/SchedulePriorityDialog/index.vue | props: open, workload-name, namespace, cluster, existing (ScheduledBumpItem[]), initial-once-local, initial-ttl-seconds; emits: close, changed |
| VolcanoMissingNotice | components/VolcanoMissingNotice/index.vue | :detail |
Composables consumed:
| Composable | File | Purpose |
|---|---|---|
| useClusterTopology | composables/useClusterTopology.ts | Polls 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.ts | Realm/cluster/namespace/project selection state shared across the whole scheduler module |
| getHostProjectId | src/modules/scheduler/_host/utils/hostContext.ts | Detects 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.
| Function | Method & Path | Used by (file:line) | Notes |
|---|---|---|---|
| fetchWorkloads(namespace, gpuPool?, cluster?, project?) | GET {realm-cluster}/workloads | Scheduler/index.vue:319,530 | Normalizes 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-bumps | Scheduler/index.vue:318,529 | Returns ScheduledBumpItem[] (cron/once priority-bump records) |
| createScheduledBump(body, cluster?) | POST \{realm-cluster\}/scheduled-bumps | inside 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,1204 | Called from bump-cancel popover (onCancel) |
| elevateWorkloadPriority(namespace, podName, priority, cluster?) | POST \{realm-cluster\}/actions/direct-run | referenced by priority-run flows (see 3-workload.mdx) | priority 100–900 (P1–P9) |
| fetchCoreGpuPools() | GET /core/v1/realms/{realm}/gpu/pools | apis/coreGpu.ts | Pool ownership ledger, used to scope “my project’s pools” |
| resolveCluster() / resolveRealm() | n/a (client helper) | this page | See 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
- 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).
- Default/root queue exclusion — Timetable excludes K8s/Volcano default queues from visualization: pool === 'default' || pool.startsWith('default-') || pool === 'root' (Scheduler/index.vue:895).
- 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.
- 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).
- 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).
- 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).
- 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).
- 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.
- 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.
- 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).
- 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]
User drags across cells (mousedown → mouseenter × N → mouseup)
→ onCellDown/onCellDragOver track dragSel
→ onCellDragUp computes span → openSchedule(wl, startCellIndex, ttlSeconds)
→ schedule.value = `{$([regex]::Match({open:true, workloadName, namespace, cluster, initialOnceLocal, initialTtlSeconds}, "(?<=\{)(.*?)(?=\})").Value }`
→ <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
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:- Router resolves Timetable → lazy-loads Scheduler/index.vue.
- onMounted fires → profileStore.ensureProfile() resolves realm/cluster list.
- reload() runs: computes targets (all Volcano clusters), fan-out listScheduledBumps() + fetchWorkloads() per cluster, then topology.refresh().
- Results committed to workloads/bumps refs → poolRows computed re-derives grid.
- Template renders pool blocks; loading flips false; 30s poll timer + 60s clock timer start.
- mousedown on cell N → onCellDown starts dragSel.
- mouseenter on later cells while held → onCellDragOver extends dragSel.to.
- mouseup (window listener,
{$([regex]::Match({once:true}, "(?<=\{)(.*?)(?=\})").Value }) → onCellDragUp computes span length, converts to TTL seconds, calls openSchedule(wl, startIndex, ttlSeconds). - SchedulePriorityDialog opens in CREATE mode pre-filled with start time + TTL.
- User selects target priority (P1–P9), optionally toggles TTL/end-action, submits.
- Dialog internally calls createScheduledBump() (POST /scheduled-bumps), emits changed.
- 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().
- poolRows/buildCells() recompute → an orange “bump” cell appears with tooltip describing direction (raise/lower), priority, end time, and next execution time.
- User clicks bump cell → onBumpClick sets selectedBump.
- Bump popover shown → user clicks “Cancel” → onCancel(pairId).
- deleteScheduledBump(pairId, cluster) sent.
- awaitCron(..., done = () => bump no longer in bumps.value) polls reload(true).
- 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.
Related Files
- 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)