11// Package cli provides easy-to-use commands to manage, monitor, and utilize AIS clusters.
2- // This file handles commands that interact with the cluster.
32/*
43 * Copyright (c) 2021-2026, NVIDIA CORPORATION. All rights reserved.
54 */
@@ -24,7 +23,14 @@ import (
2423)
2524
2625const (
26+ // migration mode: TX/RX split
2727 showRebHdr = "REB ID\t NODE\t OBJECTS RECV\t SIZE RECV\t OBJECTS SENT\t SIZE SENT\t START\t END\t STATE"
28+
29+ // cleanup mode: removed = Objs/Bytes (see reb/cleanup.go)
30+ showRebCleanupHdr = "REB ID\t NODE\t REMOVED OBJECTS\t REMOVED SIZE\t START\t END\t STATE"
31+
32+ rebColCount = 9 // showRebHdr column count (\t-separated)
33+ rebCleanupColCount = 7 // showRebCleanupHdr column count
2834)
2935
3036var showRebUsage = "Show global rebalance status and per-target stats.\n " +
@@ -33,7 +39,8 @@ var showRebUsage = "Show global rebalance status and per-target stats.\n" +
3339 indent1 + "\t - show rebalance g21\t - show a specific rebalance by ID;\n " +
3440 indent1 + "\t - show rebalance --refresh 10\t - continuous monitoring with 10s refresh (until Ctrl-C);\n " +
3541 indent1 + "\t - show rebalance --refresh 10 --count 5\t - same as above, limited to 5 iterations;\n " +
36- indent1 + "\t - show rebalance --all\t - include all rebalances: running, finished, and aborted.\n " +
42+ indent1 + "\t - show rebalance --all\t - include all rebalances: running, finished, and aborted\n " +
43+ indent1 + "\t \t (note: 'rebalance --cleanup' generations render with a cleanup-mode template).\n " +
3744 indent1 + "for alternative views, see also:\n " +
3845 indent1 + "\t - 'ais show job'\t - all running jobs (possibly including rebalance) across the cluster;\n " +
3946 indent1 + "\t - 'ais performance intra-data'\t - live peer-to-peer RX/TX counters and sizes (rebalance generates intra-cluster traffic)."
5562 }
5663)
5764
65+ // TODO -- FIXME:
66+ // add Flags field to core.Snap (requires msgpack regen)
67+ // and replace with `snap.Flags & xact.FlagRemoveMisplaced`
68+ func _isRebCleanup (snap * core.Snap ) bool {
69+ return snap .Kind == apc .ActRebalance && strings .Contains (snap .CtlMsg , ":cleanup" )
70+ }
71+
72+ func isRebCleanupGroup (group []* targetRebSnap ) bool {
73+ for _ , sts := range group {
74+ if _isRebCleanup (sts .snap ) {
75+ return true
76+ }
77+ }
78+ return false
79+ }
80+
5881// (implemented over Go text/tabwriter directly w/ no templates)
5982func showRebalanceHandler (c * cli.Context ) error {
6083 var (
@@ -113,12 +136,7 @@ func showRebalanceHandler(c *cli.Context) error {
113136 return V (err )
114137 }
115138
116- var (
117- numMigratedObjs int64 // acknowledged migrations
118- sizeMigratedBytes int64
119- prevID string
120- allSnaps = make ([]* targetRebSnap , 0 , 32 )
121- )
139+ allSnaps := make ([]* targetRebSnap , 0 , 32 )
122140 for daemonID , daemonStats := range rebSnaps {
123141 if xargs .DaemonID != "" && xargs .DaemonID != daemonID {
124142 continue
@@ -131,6 +149,7 @@ func showRebalanceHandler(c *cli.Context) error {
131149 }
132150 }
133151
152+ var lastID string
134153 if len (allSnaps ) > 0 {
135154 sort .Slice (allSnaps , func (i , j int ) bool {
136155 if allSnaps [i ].snap .ID != allSnaps [j ].snap .ID {
@@ -142,40 +161,12 @@ func showRebalanceHandler(c *cli.Context) error {
142161 if printed {
143162 fmt .Fprintln (tw )
144163 }
145- if ! hideHeader {
146- // NOTE: when/if changing header update `colCount` & `displayRebStats` as well
147- fmt .Fprintln (tw , showRebHdr )
148- }
149- for _ , sts := range allSnaps {
150- if flagIsSet (c , allJobsFlag ) {
151- if prevID != "" && sts .snap .ID != prevID {
152- fmt .Fprintln (tw , strings .Repeat ("\t " , 9 /*colCount*/ ))
153- numMigratedObjs , sizeMigratedBytes = 0 , 0
154- }
155- displayRebStats (tw , sts , units , datedTime )
156- } else {
157- if prevID != "" && sts .snap .ID != prevID {
158- break
159- }
160- latestAborted = latestAborted || sts .snap .AbortedX
161- latestFinished = latestFinished || ! sts .snap .EndTime .IsZero ()
162- displayRebStats (tw , sts , units , datedTime )
163- }
164- // migrated total, as in: objects successfully sent out
165- numMigratedObjs += sts .snap .Stats .OutObjs
166- sizeMigratedBytes += sts .snap .Stats .OutBytes
167- prevID = sts .snap .ID
168- }
169- tw .Flush ()
170- printed = true
171- }
172164
173- id := fcyan (prevID )
174- if numMigratedObjs > 0 {
175- fmt .Fprintf (c .App .Writer , "%s: %d objects migrated (total size %s)\n " ,
176- id , numMigratedObjs , teb .FmtSize (sizeMigratedBytes , units , 1 ))
165+ latestAborted , latestFinished , printed , lastID = renderSnaps (c , tw , allSnaps , units , datedTime , hideHeader )
177166 }
167+
178168 if ! flagIsSet (c , allJobsFlag ) {
169+ id := fcyan (lastID )
179170 if latestFinished && latestAborted {
180171 fmt .Fprintf (c .App .Writer , "\n Rebalance %s aborted.\n " , id )
181172 break
@@ -200,15 +191,146 @@ func showRebalanceHandler(c *cli.Context) error {
200191 return nil
201192}
202193
203- func displayRebStats (tw * tabwriter.Writer , st * targetRebSnap , units string , datedTime bool ) {
204- var startTime , endTime string
205- if datedTime {
206- startTime = teb .FmtDateTime (st .snap .StartTime )
207- endTime = teb .FmtDateTime (st .snap .EndTime )
208- } else {
209- startTime = teb .FmtTime (st .snap .StartTime )
210- endTime = teb .FmtTime (st .snap .EndTime )
194+ // walk sorted snaps, grouping by rebalance ID. Each ID-group renders
195+ // with its own header (migration vs cleanup mode), body, and per-group totals
196+ // footer; return aborted/finished flags and ID for the selected latest generation
197+ // when not showing all; with --all these return values are ignored by caller.
198+ func renderSnaps (c * cli.Context , tw * tabwriter.Writer , allSnaps []* targetRebSnap ,
199+ units string , datedTime , hideHeader bool ) (latestAborted , latestFinished , printed bool , lastID string ) {
200+ showAll := flagIsSet (c , allJobsFlag )
201+
202+ // allSnaps is sorted newest-ID first above; groupByRebID preserves that order.
203+ groups := groupByRebID (allSnaps )
204+ if ! showAll && len (groups ) > 0 {
205+ groups = groups [len (groups )- 1 :] // only the most-recent generation; preserve old first-ID behavior
206+ }
207+
208+ for i , group := range groups {
209+ cleanup := isRebCleanupGroup (group )
210+ renderGroup (c , tw , group , units , datedTime , hideHeader , cleanup )
211+
212+ // generation transition separator (between groups only)
213+ if showAll && i < len (groups )- 1 {
214+ fmt .Fprintln (tw , strings .Repeat ("\t " , colCountFor (cleanup )))
215+ }
216+
217+ latestAborted , latestFinished = false , false
218+ for _ , sts := range group {
219+ latestAborted = latestAborted || sts .snap .AbortedX
220+ latestFinished = latestFinished || ! sts .snap .EndTime .IsZero ()
221+ }
222+ lastID = group [0 ].snap .ID
223+ }
224+
225+ tw .Flush ()
226+ return latestAborted , latestFinished , len (groups ) > 0 , lastID
227+ }
228+
229+ // groupByRebID partitions a slice of snaps (sorted by ID then tid) into per-ID
230+ // groups, preserving order. Returns nil for empty input.
231+ func groupByRebID (allSnaps []* targetRebSnap ) [][]* targetRebSnap {
232+ if len (allSnaps ) == 0 {
233+ return nil
234+ }
235+ groups := make ([][]* targetRebSnap , 0 , 4 )
236+ start := 0
237+ for i := 1 ; i <= len (allSnaps ); i ++ {
238+ if i == len (allSnaps ) || allSnaps [i ].snap .ID != allSnaps [start ].snap .ID {
239+ groups = append (groups , allSnaps [start :i ])
240+ start = i
241+ }
242+ }
243+ return groups
244+ }
245+
246+ // print header + body + per-group totals footer for one rebalance generation
247+ func renderGroup (c * cli.Context , tw * tabwriter.Writer , snaps []* targetRebSnap , units string , datedTime , hideHeader , cleanup bool ) {
248+ // caption: per-target ctlMsg lines (deduped) above the table
249+ tw .Flush ()
250+ printGroupCtlMsgs (c , snaps )
251+
252+ if ! hideHeader {
253+ if cleanup {
254+ fmt .Fprintln (tw , showRebCleanupHdr )
255+ } else {
256+ fmt .Fprintln (tw , showRebHdr )
257+ }
211258 }
259+
260+ var totalObjs , totalBytes int64
261+ for _ , sts := range snaps {
262+ if cleanup {
263+ displayCleanupStats (tw , sts , units , datedTime )
264+ // cleanup: locally-processed counters (ObjsAdd => Stats.Objs/Bytes)
265+ totalObjs += sts .snap .Stats .Objs
266+ totalBytes += sts .snap .Stats .Bytes
267+ } else {
268+ displayMigrationStats (tw , sts , units , datedTime )
269+ totalObjs += sts .snap .Stats .OutObjs
270+ totalBytes += sts .snap .Stats .OutBytes
271+ }
272+ }
273+
274+ tw .Flush ()
275+ if totalObjs == 0 || len (snaps ) == 0 {
276+ return
277+ }
278+ id := fcyan (snaps [0 ].snap .ID )
279+ verb := "migrated"
280+ if cleanup {
281+ verb = "removed"
282+ }
283+ fmt .Fprintf (c .App .Writer , "%s: %d objects %s (total size %s)\n " ,
284+ id , totalObjs , verb , teb .FmtSize (totalBytes , units , 1 ))
285+ }
286+
287+ // printGroupCtlMsgs prints deduped, deterministically-ordered ctlMsg lines
288+ // above the per-generation table. Mirrors the caption block in `ais show job`
289+ // (see job_show_hdlr.go: seenCtl/ctlmsgs collection, jobCptn rendering).
290+ //
291+ // For regular rebalance (migration) mode, ctlMsg carries traversal phase timings:
292+ // <fin> trav:6s post-trav:2s fin:28s fin-streams:12s
293+ // For cleanup mode, it carries visits/loads/removed counters:
294+ // t[PAYt8083]:cleanup done visits=257259 loads=86170 removed=86170
295+ func printGroupCtlMsgs (c * cli.Context , snaps []* targetRebSnap ) {
296+ if len (snaps ) == 0 {
297+ return
298+ }
299+ var (
300+ seen = make (map [string ]struct {}, len (snaps ))
301+ msgs = make ([]string , 0 , len (snaps ))
302+ )
303+ for _ , sts := range snaps {
304+ m := sts .snap .CtlMsg
305+ if m == "" {
306+ continue
307+ }
308+ if _ , ok := seen [m ]; ok {
309+ continue
310+ }
311+ seen [m ] = struct {}{}
312+ msgs = append (msgs , m )
313+ }
314+ if len (msgs ) == 0 {
315+ return
316+ }
317+ sort .Strings (msgs ) // deterministic regardless of map iteration order
318+ id := fcyan (snaps [0 ].snap .ID )
319+ fmt .Fprintf (c .App .Writer , "%s ctl:\n " , id )
320+ for _ , m := range msgs {
321+ fmt .Fprintf (c .App .Writer , " %s\n " , m )
322+ }
323+ }
324+
325+ func colCountFor (cleanup bool ) int {
326+ if cleanup {
327+ return rebCleanupColCount
328+ }
329+ return rebColCount
330+ }
331+
332+ func displayMigrationStats (tw * tabwriter.Writer , st * targetRebSnap , units string , datedTime bool ) {
333+ startTime , endTime := fmtRebTimes (st .snap , datedTime )
212334 fmt .Fprintf (tw ,
213335 "%s\t %s\t %d\t %s\t %d\t %s\t %s\t %s\t %s\n " ,
214336 st .snap .ID , st .tid ,
@@ -217,3 +339,24 @@ func displayRebStats(tw *tabwriter.Writer, st *targetRebSnap, units string, date
217339 startTime , endTime , teb .FmtXactRunFinAbrt (st .snap ),
218340 )
219341}
342+
343+ func displayCleanupStats (tw * tabwriter.Writer , st * targetRebSnap , units string , datedTime bool ) {
344+ startTime , endTime := fmtRebTimes (st .snap , datedTime )
345+ fmt .Fprintf (tw ,
346+ "%s\t %s\t %d\t %s\t %s\t %s\t %s\n " ,
347+ st .snap .ID , st .tid ,
348+ st .snap .Stats .Objs , teb .FmtSize (st .snap .Stats .Bytes , units , 2 ),
349+ startTime , endTime , teb .FmtXactRunFinAbrt (st .snap ),
350+ )
351+ }
352+
353+ func fmtRebTimes (snap * core.Snap , datedTime bool ) (startTime , endTime string ) {
354+ if datedTime {
355+ startTime = teb .FmtDateTime (snap .StartTime )
356+ endTime = teb .FmtDateTime (snap .EndTime )
357+ } else {
358+ startTime = teb .FmtTime (snap .StartTime )
359+ endTime = teb .FmtTime (snap .EndTime )
360+ }
361+ return
362+ }
0 commit comments