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 }