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  }