github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/lazy/init.go (about)

     1  // Copyright 2019 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package lazy
    15  
    16  import (
    17  	"context"
    18  	"sync"
    19  	"time"
    20  
    21  	"github.com/pkg/errors"
    22  )
    23  
    24  // New creates a new empty Init.
    25  func New() *Init {
    26  	return &Init{}
    27  }
    28  
    29  // Init holds a graph of lazily initialized dependencies.
    30  type Init struct {
    31  	mu sync.Mutex
    32  
    33  	prev     *Init
    34  	children []*Init
    35  
    36  	init onceMore
    37  	out  interface{}
    38  	err  error
    39  	f    func() (interface{}, error)
    40  }
    41  
    42  // Add adds a func as a new child dependency.
    43  func (ini *Init) Add(initFn func() (interface{}, error)) *Init {
    44  	if ini == nil {
    45  		ini = New()
    46  	}
    47  	return ini.add(false, initFn)
    48  }
    49  
    50  // AddWithTimeout is same as Add, but with a timeout that aborts initialization.
    51  func (ini *Init) AddWithTimeout(timeout time.Duration, f func(ctx context.Context) (interface{}, error)) *Init {
    52  	return ini.Add(func() (interface{}, error) {
    53  		return ini.withTimeout(timeout, f)
    54  	})
    55  }
    56  
    57  // Branch creates a new dependency branch based on an existing and adds
    58  // the given dependency as a child.
    59  func (ini *Init) Branch(initFn func() (interface{}, error)) *Init {
    60  	if ini == nil {
    61  		ini = New()
    62  	}
    63  	return ini.add(true, initFn)
    64  }
    65  
    66  // BranchdWithTimeout is same as Branch, but with a timeout.
    67  func (ini *Init) BranchWithTimeout(timeout time.Duration, f func(ctx context.Context) (interface{}, error)) *Init {
    68  	return ini.Branch(func() (interface{}, error) {
    69  		return ini.withTimeout(timeout, f)
    70  	})
    71  }
    72  
    73  // Do initializes the entire dependency graph.
    74  func (ini *Init) Do() (interface{}, error) {
    75  	if ini == nil {
    76  		panic("init is nil")
    77  	}
    78  
    79  	ini.init.Do(func() {
    80  		prev := ini.prev
    81  		if prev != nil {
    82  			// A branch. Initialize the ancestors.
    83  			if prev.shouldInitialize() {
    84  				_, err := prev.Do()
    85  				if err != nil {
    86  					ini.err = err
    87  					return
    88  				}
    89  			} else if prev.inProgress() {
    90  				// Concurrent initialization. The following init func
    91  				// may depend on earlier state, so wait.
    92  				prev.wait()
    93  			}
    94  		}
    95  
    96  		if ini.f != nil {
    97  			ini.out, ini.err = ini.f()
    98  		}
    99  
   100  		for _, child := range ini.children {
   101  			if child.shouldInitialize() {
   102  				_, err := child.Do()
   103  				if err != nil {
   104  					ini.err = err
   105  					return
   106  				}
   107  			}
   108  		}
   109  	})
   110  
   111  	ini.wait()
   112  
   113  	return ini.out, ini.err
   114  }
   115  
   116  // TODO(bep) investigate if we can use sync.Cond for this.
   117  func (ini *Init) wait() {
   118  	var counter time.Duration
   119  	for !ini.init.Done() {
   120  		counter += 10
   121  		if counter > 600000000 {
   122  			panic("BUG: timed out in lazy init")
   123  		}
   124  		time.Sleep(counter * time.Microsecond)
   125  	}
   126  }
   127  
   128  func (ini *Init) inProgress() bool {
   129  	return ini != nil && ini.init.InProgress()
   130  }
   131  
   132  func (ini *Init) shouldInitialize() bool {
   133  	return !(ini == nil || ini.init.Done() || ini.init.InProgress())
   134  }
   135  
   136  // Reset resets the current and all its dependencies.
   137  func (ini *Init) Reset() {
   138  	mu := ini.init.ResetWithLock()
   139  	defer mu.Unlock()
   140  	for _, d := range ini.children {
   141  		d.Reset()
   142  	}
   143  }
   144  
   145  func (ini *Init) add(branch bool, initFn func() (interface{}, error)) *Init {
   146  	ini.mu.Lock()
   147  	defer ini.mu.Unlock()
   148  
   149  	if branch {
   150  		return &Init{
   151  			f:    initFn,
   152  			prev: ini,
   153  		}
   154  	}
   155  
   156  	ini.checkDone()
   157  	ini.children = append(ini.children, &Init{
   158  		f: initFn,
   159  	})
   160  
   161  	return ini
   162  }
   163  
   164  func (ini *Init) checkDone() {
   165  	if ini.init.Done() {
   166  		panic("init cannot be added to after it has run")
   167  	}
   168  }
   169  
   170  func (ini *Init) withTimeout(timeout time.Duration, f func(ctx context.Context) (interface{}, error)) (interface{}, error) {
   171  	ctx, cancel := context.WithTimeout(context.Background(), timeout)
   172  	defer cancel()
   173  	c := make(chan verr, 1)
   174  
   175  	go func() {
   176  		v, err := f(ctx)
   177  		select {
   178  		case <-ctx.Done():
   179  			return
   180  		default:
   181  			c <- verr{v: v, err: err}
   182  		}
   183  	}()
   184  
   185  	select {
   186  	case <-ctx.Done():
   187  		return nil, errors.New("timed out initializing value. You may have a circular loop in a shortcode, or your site may have resources that take longer to build than the `timeout` limit in your Hugo config file.")
   188  	case ve := <-c:
   189  		return ve.v, ve.err
   190  	}
   191  }
   192  
   193  type verr struct {
   194  	v   interface{}
   195  	err error
   196  }