lab.nexedi.com/kirr/go123@v0.0.0-20240207185015-8299741fa871/xcontext/xcontext.go (about)

     1  // Copyright (C) 2017-2020  Nexedi SA and Contributors.
     2  //                          Kirill Smelkov <kirr@nexedi.com>
     3  //
     4  // This program is free software: you can Use, Study, Modify and Redistribute
     5  // it under the terms of the GNU General Public License version 3, or (at your
     6  // option) any later version, as published by the Free Software Foundation.
     7  //
     8  // You can also Link and Combine this program with other software covered by
     9  // the terms of any of the Free Software licenses or any of the Open Source
    10  // Initiative approved licenses and Convey the resulting work. Corresponding
    11  // source of such a combination shall include the source code for all other
    12  // software used.
    13  //
    14  // This program is distributed WITHOUT ANY WARRANTY; without even the implied
    15  // warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    16  //
    17  // See COPYING file for full licensing terms.
    18  // See https://www.nexedi.com/licensing for rationale and options.
    19  
    20  // Package xcontext provides addons to std package context.
    21  //
    22  // # Merging contexts
    23  //
    24  // Merge could be handy in situations where spawned job needs to be canceled
    25  // whenever any of 2 contexts becomes done. This frequently arises with service
    26  // methods that accept context as argument, and the service itself, on another
    27  // control line, could be instructed to become non-operational. For example:
    28  //
    29  //	func (srv *Service) DoSomething(ctx context.Context) (err error) {
    30  //		defer xerr.Contextf(&err, "%s: do something", srv)
    31  //
    32  //		// srv.serveCtx is context that becomes canceled when srv is
    33  //		// instructed to stop providing service.
    34  //		origCtx := ctx
    35  //		ctx, cancel := xcontext.Merge(ctx, srv.serveCtx)
    36  //		defer cancel()
    37  //
    38  //		err = srv.doJob(ctx)
    39  //		if err != nil {
    40  //			if ctx.Err() != nil && origCtx.Err() == nil {
    41  //				// error due to service shutdown
    42  //				err = ErrServiceDown
    43  //			}
    44  //			return err
    45  //		}
    46  //
    47  //		...
    48  //	}
    49  package xcontext
    50  
    51  import (
    52  	"context"
    53  	"sync"
    54  	"sync/atomic"
    55  	"time"
    56  )
    57  
    58  // XXX if we could change std context, then Merge could work by simply creating
    59  // cancelCtx and registering it to parent1 and parent2.
    60  //
    61  //	https://github.com/golang/go/issues/36503
    62  //	https://github.com/golang/go/issues/36448
    63  //
    64  // For the reference: here is how it is done in pygolang:
    65  //
    66  //	https://lab.nexedi.com/kirr/pygolang/blob/d3bfb1bf/golang/context.py#L115-130
    67  //	https://lab.nexedi.com/kirr/pygolang/blob/d3bfb1bf/golang/context.py#L228-264
    68  
    69  
    70  // mergeCtx represents 2 context merged into 1.
    71  type mergeCtx struct {
    72  	parent1, parent2 context.Context
    73  
    74  	done     chan struct{}
    75  	doneMark uint32
    76  	doneOnce sync.Once
    77  	doneErr  error
    78  
    79  	cancelCh   chan struct{}
    80  	cancelOnce sync.Once
    81  }
    82  
    83  // Merge merges 2 contexts into 1.
    84  //
    85  // The result context:
    86  //
    87  //   - is done when parent1 or parent2 is done, or cancel called, whichever happens first,
    88  //   - has deadline = min(parent1.Deadline, parent2.Deadline),
    89  //   - has associated values merged from parent1 and parent2, with parent1 taking precedence.
    90  //
    91  // Canceling this context releases resources associated with it, so code should
    92  // call cancel as soon as the operations running in this Context complete.
    93  func Merge(parent1, parent2 context.Context) (context.Context, context.CancelFunc) {
    94  	mc := &mergeCtx{
    95  		parent1:  parent1,
    96  		parent2:  parent2,
    97  		done:     make(chan struct{}),
    98  		cancelCh: make(chan struct{}),
    99  	}
   100  
   101  	// if src ctx is already cancelled - make mc cancelled right after creation
   102  	//
   103  	// this saves goroutine spawn and makes
   104  	//
   105  	//	ctx = Merge(ctx1, ctx2); ctx.Err != nil
   106  	//
   107  	// check possible.
   108  	select {
   109  	case <-parent1.Done():
   110  		mc.finish(parent1.Err())
   111  
   112  	case <-parent2.Done():
   113  		mc.finish(parent2.Err())
   114  
   115  	default:
   116  		// src ctx not canceled - spawn parent{1,2}.done merger.
   117  		go mc.wait()
   118  	}
   119  
   120  	return mc, mc.cancel
   121  }
   122  
   123  // finish marks merge ctx as done with specified error.
   124  //
   125  // it is safe to call finish multiple times and from multiple goroutines
   126  // simultaneously - only the first call has the effect.
   127  //
   128  // finish returns the first error - with which ctx was actually marked as done.
   129  func (mc *mergeCtx) finish(err error) error {
   130  	mc.doneOnce.Do(func() {
   131  		mc.doneErr = err
   132  		atomic.StoreUint32(&mc.doneMark, 1)
   133  		close(mc.done)
   134  	})
   135  	return mc.doneErr
   136  }
   137  
   138  // wait waits for (.parent1 | .parent2 | .cancelCh) and then marks mergeCtx as done.
   139  func (mc *mergeCtx) wait() {
   140  	var err error
   141  	select {
   142  	case <-mc.parent1.Done():
   143  		err = mc.parent1.Err()
   144  
   145  	case <-mc.parent2.Done():
   146  		err = mc.parent2.Err()
   147  
   148  	case <-mc.cancelCh:
   149  		err = context.Canceled
   150  	}
   151  
   152  	mc.finish(err)
   153  }
   154  
   155  // cancel sends signal to wait to shutdown.
   156  //
   157  // cancel is the context.CancelFunc returned for mergeCtx by Merge.
   158  func (mc *mergeCtx) cancel() {
   159  	mc.cancelOnce.Do(func() {
   160  		close(mc.cancelCh)
   161  	})
   162  }
   163  
   164  // Done implements context.Context .
   165  func (mc *mergeCtx) Done() <-chan struct{} {
   166  	return mc.done
   167  }
   168  
   169  // Err implements context.Context .
   170  func (mc *mergeCtx) Err() error {
   171  	// fast path: if already done
   172  	if atomic.LoadUint32(&mc.doneMark) != 0 {
   173  		return mc.doneErr
   174  	}
   175  
   176  	// slow path: poll all sources so that there is no delay for e.g.
   177  	// parent1.Err -> mergeCtx.Err, if user checks mergeCtx.Err directly.
   178  	var err error
   179  	select {
   180  	case <-mc.parent1.Done():
   181  		err = mc.parent1.Err()
   182  
   183  	case <-mc.parent2.Done():
   184  		err = mc.parent2.Err()
   185  
   186  	case <-mc.cancelCh:
   187  		err = context.Canceled
   188  
   189  	default:
   190  		return nil
   191  	}
   192  
   193  	return mc.finish(err)
   194  }
   195  
   196  // Deadline implements context.Context .
   197  func (mc *mergeCtx) Deadline() (time.Time, bool) {
   198  	d1, ok1 := mc.parent1.Deadline()
   199  	d2, ok2 := mc.parent2.Deadline()
   200  	switch {
   201  	case !ok1:
   202  		return d2, ok2
   203  	case !ok2:
   204  		return d1, ok1
   205  	case d1.Before(d2):
   206  		return d1, true
   207  	default:
   208  		return d2, true
   209  	}
   210  }
   211  
   212  // Value implements context.Context .
   213  func (mc *mergeCtx) Value(key interface{}) interface{} {
   214  	v := mc.parent1.Value(key)
   215  	if v != nil {
   216  		return v
   217  	}
   218  	return mc.parent2.Value(key)
   219  }
   220  
   221  // ----------------------------------------
   222  
   223  // chanCtx wraps channel into context.Context interface.
   224  type chanCtx struct {
   225  	done <-chan struct{}
   226  }
   227  
   228  // MergeChan merges context and channel into 1 context.
   229  //
   230  // MergeChan, similarly to Merge, provides resulting context which:
   231  //
   232  //   - is done when parent1 is done or done2 is closed, or cancel called, whichever happens first,
   233  //   - has the same deadline as parent1,
   234  //   - has the same associated values as parent1.
   235  //
   236  // Canceling this context releases resources associated with it, so code should
   237  // call cancel as soon as the operations running in this Context complete.
   238  func MergeChan(parent1 context.Context, done2 <-chan struct{}) (context.Context, context.CancelFunc) {
   239  	return Merge(parent1, chanCtx{done2})
   240  }
   241  
   242  // Done implements context.Context .
   243  func (c chanCtx) Done() <-chan struct{} {
   244  	return c.done
   245  }
   246  
   247  // Err implements context.Context .
   248  func (c chanCtx) Err() error {
   249  	select {
   250  	case <-c.done:
   251  		return context.Canceled
   252  	default:
   253  		return nil
   254  	}
   255  }
   256  
   257  // Deadline implements context.Context .
   258  func (c chanCtx) Deadline() (time.Time, bool) {
   259  	return time.Time{}, false
   260  }
   261  
   262  // Value implements context.Context .
   263  func (c chanCtx) Value(key interface{}) interface{} {
   264  	return nil
   265  }