github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/async/async.go (about)

     1  package async
     2  
     3  import (
     4  	"sync/atomic"
     5  
     6  	"github.com/gnolang/gno/tm2/pkg/errors"
     7  )
     8  
     9  //----------------------------------------
    10  // Routine
    11  
    12  func Routine(fn func()) <-chan struct{} {
    13  	done := make(chan struct{})
    14  	go func() {
    15  		fn()
    16  		close(done)
    17  	}()
    18  	return done
    19  }
    20  
    21  //----------------------------------------
    22  // Task
    23  
    24  // val: the value returned after task execution.
    25  // err: the error returned during task completion.
    26  // abort: tells Parallel to return, whether or not all tasks have completed.
    27  type Task func(i int) (val interface{}, err error, abort bool)
    28  
    29  type TaskResult struct {
    30  	Value interface{}
    31  	Error error
    32  }
    33  
    34  type TaskResultCh <-chan TaskResult
    35  
    36  type taskResultOK struct {
    37  	TaskResult
    38  	OK bool
    39  }
    40  
    41  type TaskResultSet struct {
    42  	chz     []TaskResultCh
    43  	results []taskResultOK
    44  }
    45  
    46  func newTaskResultSet(chz []TaskResultCh) *TaskResultSet {
    47  	return &TaskResultSet{
    48  		chz:     chz,
    49  		results: make([]taskResultOK, len(chz)),
    50  	}
    51  }
    52  
    53  func (trs *TaskResultSet) Channels() []TaskResultCh {
    54  	return trs.chz
    55  }
    56  
    57  func (trs *TaskResultSet) LatestResult(index int) (TaskResult, bool) {
    58  	if len(trs.results) <= index {
    59  		return TaskResult{}, false
    60  	}
    61  	resultOK := trs.results[index]
    62  	return resultOK.TaskResult, resultOK.OK
    63  }
    64  
    65  // NOTE: Not concurrency safe.
    66  // Writes results to trs.results without waiting for all tasks to complete.
    67  func (trs *TaskResultSet) Reap() *TaskResultSet {
    68  	for i := 0; i < len(trs.results); i++ {
    69  		trch := trs.chz[i]
    70  		select {
    71  		case result, ok := <-trch:
    72  			if ok {
    73  				// Write result.
    74  				trs.results[i] = taskResultOK{
    75  					TaskResult: result,
    76  					OK:         true,
    77  				}
    78  			}
    79  			// else {
    80  			// We already wrote it.
    81  			// }
    82  		default:
    83  			// Do nothing.
    84  		}
    85  	}
    86  	return trs
    87  }
    88  
    89  // NOTE: Not concurrency safe.
    90  // Like Reap() but waits until all tasks have returned or panic'd.
    91  func (trs *TaskResultSet) Wait() *TaskResultSet {
    92  	for i := 0; i < len(trs.results); i++ {
    93  		trch := trs.chz[i]
    94  		result, ok := <-trch
    95  		if ok {
    96  			// Write result.
    97  			trs.results[i] = taskResultOK{
    98  				TaskResult: result,
    99  				OK:         true,
   100  			}
   101  		}
   102  		// else {
   103  		// We already wrote it.
   104  		// }
   105  	}
   106  	return trs
   107  }
   108  
   109  // Returns the firstmost (by task index) error as
   110  // discovered by all previous Reap() calls.
   111  func (trs *TaskResultSet) FirstValue() interface{} {
   112  	for _, result := range trs.results {
   113  		if result.Value != nil {
   114  			return result.Value
   115  		}
   116  	}
   117  	return nil
   118  }
   119  
   120  // Returns the firstmost (by task index) error as
   121  // discovered by all previous Reap() calls.
   122  func (trs *TaskResultSet) FirstError() error {
   123  	for _, result := range trs.results {
   124  		if result.Error != nil {
   125  			return result.Error
   126  		}
   127  	}
   128  	return nil
   129  }
   130  
   131  //----------------------------------------
   132  // Parallel
   133  
   134  // Run tasks in parallel, with ability to abort early.
   135  // Returns ok=false iff any of the tasks returned abort=true.
   136  // NOTE: Do not implement quit features here.  Instead, provide convenient
   137  // concurrent quit-like primitives, passed implicitly via Task closures. (e.g.
   138  // it's not Parallel's concern how you quit/abort your tasks).
   139  func Parallel(tasks ...Task) (trs *TaskResultSet, ok bool) {
   140  	taskResultChz := make([]TaskResultCh, len(tasks)) // To return.
   141  	taskDoneCh := make(chan bool, len(tasks))         // A "wait group" channel, early abort if any true received.
   142  	numPanics := new(int32)                           // Keep track of panics to set ok=false later.
   143  	ok = true                                         // We will set it to false iff any tasks panic'd or returned abort.
   144  
   145  	// Start all tasks in parallel in separate goroutines.
   146  	// When the task is complete, it will appear in the
   147  	// respective taskResultCh (associated by task index).
   148  	for i, task := range tasks {
   149  		taskResultCh := make(chan TaskResult, 1) // Capacity for 1 result.
   150  		taskResultChz[i] = taskResultCh
   151  		go func(i int, task Task, taskResultCh chan TaskResult) {
   152  			// Recovery
   153  			defer func() {
   154  				if pnk := recover(); pnk != nil {
   155  					atomic.AddInt32(numPanics, 1)
   156  					// Send panic to taskResultCh.
   157  					taskResultCh <- TaskResult{nil, errors.Wrap(pnk, "Panic in task")}
   158  					// Closing taskResultCh lets trs.Wait() work.
   159  					close(taskResultCh)
   160  					// Decrement waitgroup.
   161  					taskDoneCh <- false
   162  				}
   163  			}()
   164  			// Run the task.
   165  			val, err, abort := task(i)
   166  			// Send val/err to taskResultCh.
   167  			// NOTE: Below this line, nothing must panic/
   168  			taskResultCh <- TaskResult{val, err}
   169  			// Closing taskResultCh lets trs.Wait() work.
   170  			close(taskResultCh)
   171  			// Decrement waitgroup.
   172  			taskDoneCh <- abort
   173  		}(i, task, taskResultCh)
   174  	}
   175  
   176  	// Wait until all tasks are done, or until abort.
   177  	// DONE_LOOP:
   178  	for i := 0; i < len(tasks); i++ {
   179  		abort := <-taskDoneCh
   180  		if abort {
   181  			ok = false
   182  			break
   183  		}
   184  	}
   185  
   186  	// Ok is also false if there were any panics.
   187  	// We must do this check here (after DONE_LOOP).
   188  	ok = ok && (atomic.LoadInt32(numPanics) == 0)
   189  
   190  	return newTaskResultSet(taskResultChz).Reap(), ok
   191  }