github.com/neohugo/neohugo@v0.123.8/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  	"errors"
    19  	"sync"
    20  	"sync/atomic"
    21  	"time"
    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  	// Used mainly for testing.
    32  	initCount uint64
    33  
    34  	mu sync.Mutex
    35  
    36  	prev     *Init
    37  	children []*Init
    38  
    39  	init onceMore
    40  	out  any
    41  	err  error
    42  	f    func(context.Context) (any, error)
    43  }
    44  
    45  // Add adds a func as a new child dependency.
    46  func (ini *Init) Add(initFn func(context.Context) (any, error)) *Init {
    47  	if ini == nil {
    48  		ini = New()
    49  	}
    50  	return ini.add(false, initFn)
    51  }
    52  
    53  // InitCount gets the number of this this Init has been initialized.
    54  func (ini *Init) InitCount() int {
    55  	i := atomic.LoadUint64(&ini.initCount)
    56  	return int(i)
    57  }
    58  
    59  // AddWithTimeout is same as Add, but with a timeout that aborts initialization.
    60  func (ini *Init) AddWithTimeout(timeout time.Duration, f func(ctx context.Context) (any, error)) *Init {
    61  	return ini.Add(func(ctx context.Context) (any, error) {
    62  		return ini.withTimeout(ctx, timeout, f)
    63  	})
    64  }
    65  
    66  // Branch creates a new dependency branch based on an existing and adds
    67  // the given dependency as a child.
    68  func (ini *Init) Branch(initFn func(context.Context) (any, error)) *Init {
    69  	if ini == nil {
    70  		ini = New()
    71  	}
    72  	return ini.add(true, initFn)
    73  }
    74  
    75  // BranchWithTimeout is same as Branch, but with a timeout.
    76  func (ini *Init) BranchWithTimeout(timeout time.Duration, f func(ctx context.Context) (any, error)) *Init {
    77  	return ini.Branch(func(ctx context.Context) (any, error) {
    78  		return ini.withTimeout(ctx, timeout, f)
    79  	})
    80  }
    81  
    82  // Do initializes the entire dependency graph.
    83  func (ini *Init) Do(ctx context.Context) (any, error) {
    84  	if ini == nil {
    85  		panic("init is nil")
    86  	}
    87  
    88  	ini.init.Do(func() {
    89  		atomic.AddUint64(&ini.initCount, 1)
    90  		prev := ini.prev
    91  		if prev != nil {
    92  			// A branch. Initialize the ancestors.
    93  			if prev.shouldInitialize() {
    94  				_, err := prev.Do(ctx)
    95  				if err != nil {
    96  					ini.err = err
    97  					return
    98  				}
    99  			} else if prev.inProgress() {
   100  				// Concurrent initialization. The following init func
   101  				// may depend on earlier state, so wait.
   102  				prev.wait()
   103  			}
   104  		}
   105  
   106  		if ini.f != nil {
   107  			ini.out, ini.err = ini.f(ctx)
   108  		}
   109  
   110  		for _, child := range ini.children {
   111  			if child.shouldInitialize() {
   112  				_, err := child.Do(ctx)
   113  				if err != nil {
   114  					ini.err = err
   115  					return
   116  				}
   117  			}
   118  		}
   119  	})
   120  
   121  	ini.wait()
   122  
   123  	return ini.out, ini.err
   124  }
   125  
   126  // TODO(bep) investigate if we can use sync.Cond for this.
   127  func (ini *Init) wait() {
   128  	var counter time.Duration
   129  	for !ini.init.Done() {
   130  		counter += 10
   131  		if counter > 600000000 {
   132  			panic("BUG: timed out in lazy init")
   133  		}
   134  		time.Sleep(counter * time.Microsecond)
   135  	}
   136  }
   137  
   138  func (ini *Init) inProgress() bool {
   139  	return ini != nil && ini.init.InProgress()
   140  }
   141  
   142  func (ini *Init) shouldInitialize() bool {
   143  	return !(ini == nil || ini.init.Done() || ini.init.InProgress())
   144  }
   145  
   146  // Reset resets the current and all its dependencies.
   147  func (ini *Init) Reset() {
   148  	mu := ini.init.ResetWithLock()
   149  	ini.err = nil
   150  	defer mu.Unlock()
   151  	for _, d := range ini.children {
   152  		d.Reset()
   153  	}
   154  }
   155  
   156  func (ini *Init) add(branch bool, initFn func(context.Context) (any, error)) *Init {
   157  	ini.mu.Lock()
   158  	defer ini.mu.Unlock()
   159  
   160  	if branch {
   161  		return &Init{
   162  			f:    initFn,
   163  			prev: ini,
   164  		}
   165  	}
   166  
   167  	ini.checkDone()
   168  	ini.children = append(ini.children, &Init{
   169  		f: initFn,
   170  	})
   171  
   172  	return ini
   173  }
   174  
   175  func (ini *Init) checkDone() {
   176  	if ini.init.Done() {
   177  		panic("init cannot be added to after it has run")
   178  	}
   179  }
   180  
   181  func (ini *Init) withTimeout(ctx context.Context, timeout time.Duration, f func(ctx context.Context) (any, error)) (any, error) {
   182  	// Create a new context with a timeout not connected to the incoming context.
   183  	waitCtx, cancel := context.WithTimeout(context.Background(), timeout)
   184  	defer cancel()
   185  	c := make(chan verr, 1)
   186  
   187  	go func() {
   188  		v, err := f(ctx)
   189  		select {
   190  		case <-waitCtx.Done():
   191  			return
   192  		default:
   193  			c <- verr{v: v, err: err}
   194  		}
   195  	}()
   196  
   197  	select {
   198  	case <-waitCtx.Done():
   199  		//lint:ignore ST1005 end user message.
   200  		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.")
   201  	case ve := <-c:
   202  		return ve.v, ve.err
   203  	}
   204  }
   205  
   206  type verr struct {
   207  	v   any
   208  	err error
   209  }