跳到主要内容

Timetable

Part of the Scheduler feature area (axap-gpu-volcano-ui, MFE id 'scheduler'). Sibling docs: gpu-overview, gpu-inventory, workload.

Overview

Time Table (schedulerTimeTable, page title “Priority Timetable”) is a 12‑hour × 30‑minute grid per GPU pool/workload, supporting drag-to-schedule priority “bumps” (cron/one-off priority elevation via CronJob pairs) and cancellation. It lives at modules/volcano/dashboard/pages/Scheduler/index.vue.

The backend is Volcano via the FastAPI service axap-gpu-volcano (prefix /scheduler/v1, scheduled-bumps sub-resource), plus zmp-core-api (/core/v1) for project-mode pool scoping and useClusterTopology for the “현재 점유” baseline shown per workload row.

A demo mode is baked into this codebase and is ON by default (modules/volcano/dashboard/demo/isDemo.ts) — disabled only per-tab via ?demo=0. In demo mode this page uses demoWorkloads(), demoScheduledBumps(), and demoTopologyNodes() from src/modules/volcano/dashboard/demo/demoMetrics.ts instead of calling the real backend.

Route Design

Route constants: constants/routeNames/index.ts, paths: constants/routes/index.ts, lazy components: pages/index.ts. Registered in both the system and project route tables built by buildRoutes() in router/index.ts.

Route nameRaw pathSystem pathProject pathComponent
ROUTE_NAMES.TIMETABLE (schedulerTimeTable)/timetable/system/scheduler/timetable/proj/:projectId/scheduler/timetableScheduler/index.vue — router/index.ts

No hidden/drill-down routes belong to this screen; the “예약 상세” bump popover and the SchedulePriorityDialog are both in-page modals/popovers, not routes.

Route mechanics common to the whole Scheduler MFE (system vs. project route table selection) are the same as documented in gpu-overview. This page does call getHostProjectId() directly (see State Management/Business Logic below), unlike the Workload screen.

UI Structure

Time Table → Scheduler/index.vue
├── Global cron-progress bar (teleported) — shown while a bump create/delete is confirming
├── Header: breadcrumb, title "Priority Timetable", prev/next 12h window nav, datetime-local
│ picker, "지금" reset, manual refresh
├── ScopeFilter (Name / Project / Cluster / Namespace — Namespace hidden in project mode)
├── Legend bar: Running / Waiting / Idle counts
├── Timeline frame
│ ├── Left/right paging arrows (mirrors header prev/next)
│ ├── Hover vertical line + time label
│ ├── Time axis header (hourLabels)
│ ├── Per-pool blocks (pool name/color/type + workload rows)
│ │ └── Per-workload 24-cell grid (idle/baseline/bump cell kinds, drag-select)
│ └── "Pool 미상" section — bumps whose target pod isn't matched to a known pool
├── Cell hover tooltip (teleported, bypasses native title delay)
├── SchedulePriorityDialog (create/list bumps for the clicked/dragged workload)
└── Bump detail/cancel popover (selectedBump)

Component Design

pages/Scheduler/index.vue:

  • Header controls: shiftWindow(-1|1) (prev/next 12h), onPickStart (datetime-local picker → sets windowStart), resetToNow (clears windowStart back to “now” mode), reload() (manual refresh, loading=true).
  • ScopeFilter (Name/Project/Cluster/Namespace): filterName is client-side (pod-name) filtering; filterProject/filterCluster/filterNamespace are backed by useProfileStore; hide-namespace is bound to isProjectMode; project-locked is bound to !!hostProjectId.
  • Legend bar: runningCount/waitingCount/idleCount computed from the rendered rows.
  • Timeline frame: TOTAL_CELLS = 24 (12h ÷ 30min), CELL_MS per-cell duration; hourLabels computed for the axis header (date label shown only when the label’s column crosses a day boundary, per inline comment about startMs floor alignment); onTimelineMove/onTimelineLeave drive the hover vertical line + hoverTimeLabel.
  • poolRows (PoolRow[]): pool name/color/typeLabel (Full GPU/MIG/Time Slicing) + workloads: WorkloadRow[]; each WorkloadRow has cells: CellState[] (kind: 'idle' | 'baseline' | 'bump', isLowering stripe flag for priority-decrease bumps, pair_id for bump cells), scheduledCount, nextFireIso.
  • Cell interaction: onCellDown (mousedown starts a drag selection), onCellEnter + onCellDragOver (extends the drag range across a row), isDragSel (highlights the in-progress selection), release opens SchedulePriorityDialog pre-seeded with the selected time range.
  • “Pool 미상” section (unknownPoolWorkloads): same per-workload row/cell rendering as the pool blocks, but grouped separately with a hint icon explaining the pod isn’t currently in the cluster (aged out / rolling-update renamed).
  • SchedulePriorityDialog (modules/volcano/dashboard/components/SchedulePriorityDialog/index.vue): two views — list (existing bumps for the target workload: mode 반복/1회성 tag, priority pill, TTL/end-action summary, next-fire/last-success timestamps, per-item delete) and create (mode tabs 1회성/반복, datetime-local input with a past-time guard onceLocalIsPast, cron preset grid).
  • Bump popover (selectedBump): shows workload/pair_id, with a “예약 취소” (onCancel) button (cancelBusy disables it mid-request).
  • cronWait progress bar (teleported to <body>): a global progress indicator shown while waiting for a create/delete to be confirmed at the CronJob level (label + percent fill), decoupled from the page’s main loading flag.

API Design

All calls use services.axiosInstance from @cloudz-mp/zmp-base-ui.

Base path resolution

Shared with the other screens — see gpu-overview for resolveRealm()/ resolveCluster()/getRealmClusterPath()/getRealmClusterPathFor(cluster) precedence rules (modules/volcano/dashboard/apis/index.ts). This screen is the primary user of getRealmClusterPathFor(cluster) / the cluster parameter overloads, because of its multi-cluster fan-out (“All clusters” mode, see Business Logic).

Endpoints used by Time Table

FunctionMethod & pathNotes
fetchWorkloads(namespace, undefined, cluster)GET {realmClusterFor(cluster)}/workloads?namespacecalled once per target cluster inside reload()’s fan-out Promise.all
listScheduledBumps(undefined, cluster)GET {API_BASE}/clusters/{cluster}/scheduled-bumpscalled once per target cluster, in parallel with the workloads call above; demo mode short-circuits to []
createScheduledBump(body, cluster?)POST {API_BASE}/clusters/{cluster}/scheduled-bumpscalled from SchedulePriorityDialog’s create flow
deleteScheduledBump(pairId, cluster?)DELETE {API_BASE}/clusters/{cluster}/scheduled-bumps/{pair_id}called from the bump popover’s “예약 취소” (onCancel); cluster resolved via the page’s bumpClusterByPairId map (see Business Logic)
fetchCoreGpuPools() (apis/coreGpu.ts)GET {realmPath}/gpu/pools?size=200called once on mount only in project mode, to build myPoolNames
useClusterTopology() → fetchNodeCapacities/fetchAxapGpuNodessee gpu-overviewprovides the “현재 점유” (currentGpuLabel) baseline per workload row via topology.refresh()

ScheduledBumpItem/ScheduledBumpCreateRequest fields (apis/index.ts): pair_id, target_namespace, target_pod, target_priority (100–900), original_priority, schedule_up/schedule_down (cron expr or ISO datetime), is_cron, ttl_seconds, timezone, last_schedule_time_up, last_successful_time_up, next_fire_estimate, end_action: 'restore' | 'stop' | null.

Dev-time proxy: /scheduler/v1 → axap-gpu-volcano (localhost:8000); /core/v1 → zmp-core-api (localhost:8081) — vite.config.ts.

No structured error normalizer exists for the scheduled-bump endpoints — reload() wraps each per-cluster fan-out call in its own try/catch (console.warn on failure, continues with other clusters), and the top-level reload() has an outer try/catch (console.error).

State Management

  • No Pinia store is used by this page — workloads, bumps, windowStart, now, dragSel, schedule (dialog state), selectedBump, cronWait are all page-local ref/reactive state.
  • useProfileStore (stores/useProfileStore.ts) backs filterProject/filterCluster/filterNamespace (read/write via setSelectedCluster/ setSelectedNamespace) and supplies profileStore.clusters for the multi-cluster fan-out target list.
  • useClusterTopology() is a separate, independently-polling composable instance (not shared with GPU Overview’s instance) providing the topology baseline used for currentGpuLabel.
  • myPoolNames (a ref<Set<string>>) is page-local, populated once on mount only when isProjectMode is true, from fetchCoreGpuPools() filtered by p.project === hostProjectId.
  • bumpClusterByPairId (a plain Map, not reactive) tracks which cluster each bump belongs to, rebuilt on every reload(), so the cancel action (onCancel) routes deleteScheduledBump to the correct cluster even when viewing an “All clusters” aggregated view.

Business Logic

  1. 12-cell/30-min/24-total window — TOTAL_CELLS = 24 over a 12-hour window (TOTAL_MS), “사용자 명시 명세” per inline comment; startMs floors to the current 30-min cell boundary in “now” mode (windowStart === null).
  2. Paging bounds — the timeline cannot page into the future beyond “다음 12시간” unboundedly forward but cannot page into the past before the current time (canPrev disabled once the window start would precede “now” — per the nav-btn tooltip “현재보다 과거로 이동 불가”).
  3. Multi-cluster fan-out — if profileStore.selectedCluster is unset (“All”), reload() fans out fetchWorkloads/listScheduledBumps across every cluster in profileStore.clusters (or falls back to [resolveCluster()] if that list is empty), tagging each result with _cluster and recording bumpClusterByPairId so cancel actions route correctly; selecting a specific cluster narrows the fan-out to just that one.
  4. Project mode restricts to “my pools” — getHostProjectId() non-null ⇒ isProjectMode = true; myPoolNames (from fetchCoreGpuPools() filtered by p.project === hostProjectId) is used to narrow the pool rows shown (Namespace filter is also hidden in this mode).
  5. Scheduled priority bump end-action — end_action: 'restore' | 'stop': restore (default) reverts to original_priority after ttl_seconds; stop terminates the workload via the KServe annotation serving.kserve.io/stop=true (apis/index.ts ScheduledBumpItem/ScheduledBumpCreateRequest comment).
  6. Past-time guard for one-off bumps — SchedulePriorityDialog’s mode==='once' view disallows a past datetime-local value (onceLocalIsPast warning: “⚠ 과거 시각으로는 예약 불가”).
  7. Priority class ↔︎ numeric value mapping — same 100-per-tier scheme as GPU Overview (see gpu-overview item 4); ScheduledBumpCreateRequest.target_priority ranges 100–900 (P1–P9), and the bump-detail popover renders P{Math.round(target_priority/100)}.
  8. “Lowering” cell overlay — a bump cell whose effect is a priority decrease is rendered with an is-lowering stripe overlay instead of a distinct fill color, to avoid colliding with the fixed priority color palette (isLowering flag on CellState).
  9. Unknown-pool bumps are still shown — a scheduled bump whose target_pod doesn’t currently resolve to a running/known pool workload is not dropped; it’s rendered in a separate “Pool 미상” section so the schedule remains visible/cancellable even if the pod is temporarily absent (renamed by a rolling update, not yet started, etc.).
  10. Silent polling — the 30s auto-poll (pollTimer) calls reload(true) (silent=true), which skips toggling the loading flag so the grid doesn’t flash/reset visually on each poll; only the initial mount call and manual refresh show the loading state.
  11. Demo mode is default-ON — same cross-cutting rule as the other three screens; isDemoMode() short-circuits reload() entirely to populate workloads/bumps/ topology.nodes from static demo data.

Data Flow

flowchart TD
R[/system/scheduler/timetable/] --> P[Scheduler/index.vue]
P -->|onMounted, project mode only| A0[apis/coreGpu: fetchCoreGpuPools -> myPoolNames]
P -->|onMounted -> reload| DEMO{isDemoMode?}
DEMO -->|yes| D1[demoWorkloads / demoScheduledBumps / demoTopologyNodes]
DEMO -->|no| FANOUT[targets = selectedCluster or profileStore.clusters]
FANOUT --> A1[apis: fetchWorkloads per cluster]
FANOUT --> A2[apis: listScheduledBumps per cluster]
A1 & A2 --> BE[(axap-gpu-volcano /scheduler/v1)]
P --> H1[useClusterTopology.refresh]
H1 --> A3[apis/coreGpu: fetchNodeCapacities / apis/topology: fetchAxapGpuNodes]
P --> Grid[poolRows / unknownPoolWorkloads render]
Grid -->|drag-select| Dialog[SchedulePriorityDialog]
Dialog -->|create| A4[apis: createScheduledBump]
Grid -->|click bump| Popover[bump popover]
Popover -->|cancel| A5[apis: deleteScheduledBump]
A4 & A5 --> BE

Dependency Graph

flowchart LR
Scheduler["Scheduler (Time Table)"] --> useClusterTopology
Scheduler --> useProfileStore
Scheduler --> apisIndex["apis/index.ts (scheduler/v1)"]
Scheduler --> apisCoreGpu["apis/coreGpu.ts (core/v1)"]
Scheduler --> SchedulePriorityDialog
Scheduler --> ScopeFilter
Scheduler --> VolcanoMissingNotice
useClusterTopology --> apisCoreGpu
useClusterTopology --> apisMcm["apis/mcm.ts"]
useClusterTopology --> apisTopology["apis/topology.ts"]
SchedulePriorityDialog --> apisIndex
apisIndex --> useProfileStore
apisCoreGpu --> apisIndex

Sequence Flow

Time Table initial load (multi-cluster fan-out)

sequenceDiagram
actor User
participant Page as Scheduler/index.vue
participant API as apis/index.ts
participant Topo as useClusterTopology
participant BE as axap-gpu-volcano

User->>Page: navigate to /system/scheduler/timetable
opt project mode
Page->>API: fetchCoreGpuPools()
API-->>Page: myPoolNames derived
end
Page->>Page: onMounted -> reload() (loading=true)
Page->>Page: targets = selectedCluster ? [selectedCluster] : profileStore.clusters
par per target cluster
Page->>API: listScheduledBumps(undefined, cluster)
API->>BE: GET /scheduler/v1/clusters/{cluster}/scheduled-bumps
BE-->>API: ScheduledBumpItem[]
and
Page->>API: fetchWorkloads(ns, undefined, cluster)
API->>BE: GET .../workloads?namespace
BE-->>API: raw items -> normalizeWorkload()
end
Page->>Topo: topology.refresh()
Topo-->>Page: nodes (baseline occupancy)
Page->>Page: bumps / workloads / now updated -> poolRows recomputed
Page-->>User: renders timeline grid
Note over Page: nowTimer ticks every 60s; pollTimer calls reload(true) every 30s (silent)

Create a scheduled priority bump (User Action → … → UI Update)

sequenceDiagram
actor User
participant TT as Scheduler/index.vue
participant Dlg as SchedulePriorityDialog
participant API as apis/index.ts
participant BE as axap-gpu-volcano

User->>TT: mousedown+drag over time cells for a workload row
TT->>TT: onCellDown / onCellDragOver builds dragSel range
User->>TT: release drag -> schedule.open=true (dialog opens with initial range)
User->>Dlg: choose once/cron mode, priority, TTL, end_action; submit
Dlg->>API: createScheduledBump(body, cluster)
API->>BE: `POST /scheduler/v1/clusters/{c}`/scheduled-bumps
BE-->>API: ScheduledBumpItem
API-->>Dlg: created item
Dlg->>TT: emit changed
TT->>API: listScheduledBumps(undefined, cluster) [via reload()]
API->>BE: GET .../scheduled-bumps
BE-->>API: ScheduledBumpItem[]
API-->>TT: bumps
TT->>TT: re-render pool/workload cell grid with new "bump" cell kind

Cancel a scheduled bump

sequenceDiagram
actor User
participant TT as Scheduler/index.vue
participant API as apis/index.ts
participant BE as axap-gpu-volcano

User->>TT: click a "bump" cell -> selectedBump set
User->>TT: click "예약 취소" in the bump popover
TT->>TT: cancelBusy = true
TT->>API: deleteScheduledBump(pair_id, bumpClusterByPairId.get(pair_id))
API->>BE: DELETE /scheduler/v1/clusters/{c}/scheduled-bumps/{pair_id}
BE-->>API: 204/ack
API-->>TT: resolved
TT->>TT: cancelBusy = false; selectedBump = null; reload()
TT-->>User: bump cell removed from grid on next reload

Key Findings

  • This is the only screen with true multi-cluster fan-out — when no cluster is explicitly selected, it queries every cluster in profileStore.clusters in parallel and merges the results, tagging each item with _cluster for later action routing.
  • Two independent timers drive this page: a 60s clock tick (nowTimer, re-floors the “now” window) and a 30s silent poll (pollTimer, reload(true)) — distinct from GPU Overview’s single user-configurable polling interval.
  • The “Pool 미상” bucket is a deliberate UX safety net — rather than silently dropping bumps for pods not currently resolvable, they remain visible and cancellable.
  • CronJob confirmation is treated as eventually-consistent — the cronWait progress bar exists specifically because create/delete of a bump is backed by an async CronJob reconciliation, not an immediate DB write (inferred from the progress-bar’s purpose comment “예약 생성/삭제 → cron 확인까지 진행 표시”).

Risks / Technical Debt

  • Per-cluster fan-out failures are silently swallowed (console.warn only) — a user viewing “All clusters” would not be told that one of several clusters failed to load its workloads/bumps, only that the aggregate list might be incomplete.
  • No error normalizer for createScheduledBump/deleteScheduledBump — unlike GPU Pool CRUD’s normalizeGpuPoolError, failures here are not mapped to structured validation/conflict/forbidden kinds in this page’s code path (as read in this pass).
  • bumpClusterByPairId is a plain non-reactive Map rebuilt on every reload() — correctness of cancel-routing depends on this map staying in sync with the currently rendered bumps list; a stale reference (e.g. after a partial fan-out failure) could route a cancel to the wrong/no cluster. Not confirmed to be an actual bug in this pass, flagged as a design risk.
  • Cron preset grid contents and exact once/cron payload shape sent to createScheduledBump were only partially inspected (dialog header + first ~120 lines) — full field-by-field request-building logic is Not Found in Code in this document (see Assumptions).

Assumptions

  • The exact construction of ScheduledBumpCreateRequest.schedule/schedule_up/ schedule_down from the dialog’s once/cron UI state was Not Found in Code in this pass (only the response-consuming/list-rendering side of SchedulePriorityDialog was read in full).
  • cronPresets (the preset grid options in “반복” mode) content was Not Found in Code in this pass.
  • Backend service axap-gpu-volcano’s actual CronJob-pair reconciliation logic is external to this repo and was Not Found in Code — inferred only from FE comments and the ScheduledBumpItem TypeScript interface declared in this repo.