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 }