github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/status/status.go (about) 1 // Copyright 2018 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache 2.0 3 // license that can be found in the LICENSE file. 4 5 // Package status provides facilities for reporting statuses from a 6 // number of tasks working towards a common goal. The toplevel Status 7 // represents the status for the whole job; it in turn comprises a 8 // number of groups; each group has 0 or more tasks. 9 // 10 // Tasks (and groups) may be updated via their Print[f] functions; 11 // reporters receive notifications when updates have been made. 12 // 13 // Package status also includes a standard console reporter that 14 // formats nice status screens when the output is a terminal, or else 15 // issues periodic status updates. 16 package status 17 18 import ( 19 "fmt" 20 "io" 21 "sort" 22 "sync" 23 "text/tabwriter" 24 "time" 25 ) 26 27 const expiry = 10 * time.Second 28 29 // Value is a task or group status at a point in time, it includes a 30 // title, status, as well as its start and stop times (undefined for 31 // groups). 32 type Value struct { 33 Title, Status string 34 Begin, End time.Time 35 LastBegin time.Time 36 Count int 37 } 38 39 // A Task is a single unit of work. It has a title, a beginning and 40 // an end time, and may receive zero or more status updates 41 // throughout its lifetime. 42 type Task struct { 43 group *Group 44 // next is managed by the task's group. 45 next *Task 46 47 mu sync.Mutex 48 value Value 49 } 50 51 // Print formats a message as fmt.Sprint and updates the task's 52 // status. 53 func (t *Task) Print(v ...interface{}) { 54 if t == nil { 55 return 56 } 57 t.mu.Lock() 58 t.value.Status = fmt.Sprint(v...) 59 t.mu.Unlock() 60 t.group.notify() 61 } 62 63 // Printf formats a message as fmt.Sprintf and updates the task's 64 // status. 65 func (t *Task) Printf(format string, args ...interface{}) { 66 if t == nil { 67 return 68 } 69 t.mu.Lock() 70 t.value.Status = fmt.Sprintf(format, args...) 71 t.mu.Unlock() 72 t.group.notify() 73 } 74 75 // Title formats a title as fmt.Sprint, and updates the task's title. 76 func (t *Task) Title(v ...interface{}) { 77 if t == nil { 78 return 79 } 80 t.mu.Lock() 81 t.value.Title = fmt.Sprint(v...) 82 t.mu.Unlock() 83 t.group.notify() 84 } 85 86 // Titlef formats a title as fmt.Sprintf, and updates the task's title. 87 func (t *Task) Titlef(format string, args ...interface{}) { 88 if t == nil { 89 return 90 } 91 t.mu.Lock() 92 t.value.Title = fmt.Sprintf(format, args...) 93 t.mu.Unlock() 94 t.group.notify() 95 } 96 97 // Done sets the completion time of the task to the current time. 98 // Tasks should not be updated after a call to Done; they will be 99 // discarded by the group after a timeout. 100 func (t *Task) Done() { 101 if t == nil { 102 return 103 } 104 t.mu.Lock() 105 t.value.End = time.Now() 106 t.mu.Unlock() 107 t.group.notify() 108 } 109 110 // Value returns this tasks's current value. 111 func (t *Task) Value() Value { 112 t.mu.Lock() 113 v := t.value 114 t.mu.Unlock() 115 return v 116 } 117 118 // A Group is a collection of tasks, working toward a common goal. 119 // Groups are persistent: they have no beginning or end; they have a 120 // "toplevel" status that can be updated. 121 type Group struct { 122 status *Status 123 124 mu sync.Mutex 125 value Value 126 task *Task 127 } 128 129 // Print formats a status as fmt.Sprint and sets it as the group's status. 130 func (g *Group) Print(v ...interface{}) { 131 if g == nil { 132 return 133 } 134 g.mu.Lock() 135 g.value.Status = fmt.Sprint(v...) 136 g.mu.Unlock() 137 g.notify() 138 } 139 140 // Printf formats a status as fmt.Sprintf and sets it as the group's status. 141 func (g *Group) Printf(format string, args ...interface{}) { 142 if g == nil { 143 return 144 } 145 g.mu.Lock() 146 g.value.Status = fmt.Sprintf(format, args...) 147 g.mu.Unlock() 148 g.notify() 149 } 150 151 // Start creates a new task associated with this group and returns it. 152 // The task's initial title is formatted from the provided arguments as 153 // fmt.Sprint. 154 func (g *Group) Start(v ...interface{}) *Task { 155 if g == nil { 156 return nil 157 } 158 task := new(Task) 159 g.mu.Lock() 160 task.value.Begin = time.Now() 161 task.group = g 162 p := &g.task 163 for *p != nil { 164 p = &(*p).next 165 } 166 *p = task 167 g.mu.Unlock() 168 task.Title(v...) // this will also notify 169 return task 170 } 171 172 // Startf creates a new task associated with tihs group and returns it. 173 // The task's initial title is formatted from the provided arguments as 174 // fmt.Sprintf. 175 func (g *Group) Startf(format string, args ...interface{}) *Task { 176 return g.Start(fmt.Sprintf(format, args...)) 177 } 178 179 // Tasks returns a snapshot of the group's currently active tasks. 180 // Expired tasks are garbage collected on calls to Tasks. Tasks are 181 // returned in the order of creation: the oldest is always first. 182 func (g *Group) Tasks() []*Task { 183 now := time.Now() 184 g.mu.Lock() 185 defer g.mu.Unlock() 186 var tasks []*Task 187 for p := &g.task; *p != nil; { 188 value := (*p).Value() 189 if !value.End.IsZero() && now.Sub(value.End) > expiry { 190 *p = (*p).next 191 } else { 192 tasks = append(tasks, *p) 193 p = &(*p).next 194 } 195 } 196 return tasks 197 } 198 199 // Value returns the group's current value. 200 func (g *Group) Value() Value { 201 g.mu.Lock() 202 v := g.value 203 g.mu.Unlock() 204 return v 205 } 206 207 func (g *Group) notify() { 208 g.status.notify() 209 } 210 211 type waiter struct { 212 version int 213 c chan int 214 } 215 216 // Status represents a toplevel status object. A status comprises a 217 // number of groups which in turn comprise a number of sub-tasks. 218 type Status struct { 219 mu sync.Mutex 220 groups map[string]*Group 221 version int 222 order []string 223 waiters []waiter 224 } 225 226 // Group creates and returns a new group named by the provided 227 // arguments as formatted by fmt.Sprint. If the group already exists, 228 // it is returned. 229 func (s *Status) Group(v ...interface{}) *Group { 230 name := fmt.Sprint(v...) 231 s.mu.Lock() 232 if s.groups == nil { 233 s.groups = make(map[string]*Group) 234 } 235 if s.groups[name] == nil { 236 s.groups[name] = &Group{status: s, value: Value{Title: name}} 237 } 238 g := s.groups[name] 239 s.mu.Unlock() 240 s.notify() 241 return g 242 } 243 244 // Groupf creates and returns a new group named by the provided 245 // arguments as formatted by fmt.Sprintf. If the group already exists, 246 // it is returned. 247 func (s *Status) Groupf(format string, args ...interface{}) *Group { 248 return s.Group(fmt.Sprintf(format, args...)) 249 } 250 251 // Wait returns a channel that is blocked until the version of 252 // status data is greater than the provided version. When the 253 // status version exceeds v, it is written to the channel and 254 // then closed. 255 // 256 // This allows status observers to implement a simple loop 257 // that coalesces updates: 258 // 259 // v := -1 260 // for { 261 // v = <-status.Wait(v) 262 // groups := status.Groups() 263 // // ... process groups 264 // } 265 func (s *Status) Wait(v int) <-chan int { 266 s.mu.Lock() 267 defer s.mu.Unlock() 268 c := make(chan int, 1) 269 if v < s.version { 270 c <- s.version 271 return c 272 } 273 i := sort.Search(len(s.waiters), func(i int) bool { 274 return s.waiters[i].version > v 275 }) 276 s.waiters = append(s.waiters[:i], append([]waiter{{v, c}}, s.waiters[i:]...)...) 277 return c 278 } 279 280 // Groups returns a snapshot of the status groups. Groups maintains a 281 // consistent order of returned groups: when a group cohort first 282 // appears, it is returned in arbitrary order; each cohort is 283 // appended to the last, and the order of all groups is remembered 284 // across invocations. 285 func (s *Status) Groups() []*Group { 286 s.mu.Lock() 287 seen := make(map[string]bool) 288 for _, name := range s.order { 289 seen[name] = true 290 } 291 for name := range s.groups { 292 if !seen[name] { 293 s.order = append(s.order, name) 294 } 295 } 296 var groups []*Group 297 for _, name := range s.order { 298 if s.groups[name] != nil { 299 groups = append(groups, s.groups[name]) 300 } 301 } 302 s.mu.Unlock() 303 return groups 304 } 305 306 // Marshal writes s in a human-readable format to w. 307 func (s *Status) Marshal(w io.Writer) error { 308 now := time.Now() 309 for _, group := range s.Groups() { 310 v := group.Value() 311 tw := tabwriter.NewWriter(w, 2, 4, 2, ' ', 0) 312 if _, err := fmt.Fprintf(tw, "%s: %s\n", v.Title, v.Status); err != nil { 313 return err 314 } 315 for _, task := range group.Tasks() { 316 v := task.Value() 317 elapsed := now.Sub(v.Begin) 318 elapsed -= elapsed % time.Second 319 if _, err := fmt.Fprintf(tw, "\t%s:\t%s\t%s\n", v.Title, v.Status, elapsed); err != nil { 320 return err 321 } 322 } 323 if err := tw.Flush(); err != nil { 324 return err 325 } 326 } 327 return nil 328 } 329 330 func (s *Status) notify() { 331 if s == nil { 332 return 333 } 334 s.mu.Lock() 335 defer s.mu.Unlock() 336 s.version++ 337 for len(s.waiters) > 0 && s.waiters[0].version < s.version { 338 s.waiters[0].c <- s.version 339 s.waiters = s.waiters[1:] 340 } 341 }