2019-01-02 06:33:26 -05:00
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package lazy
import (
"context"
"sync"
2022-02-09 07:41:04 -05:00
"sync/atomic"
2019-01-02 06:33:26 -05:00
"time"
2022-05-02 10:07:52 -04:00
"errors"
2019-01-02 06:33:26 -05:00
)
// New creates a new empty Init.
func New ( ) * Init {
return & Init { }
}
// Init holds a graph of lazily initialized dependencies.
type Init struct {
2023-02-11 10:20:24 -05:00
// Used mainly for testing.
2022-02-09 07:41:04 -05:00
initCount uint64
2019-01-02 06:33:26 -05:00
mu sync . Mutex
prev * Init
children [ ] * Init
init onceMore
2022-03-17 17:03:27 -04:00
out any
2019-01-02 06:33:26 -05:00
err error
2023-02-11 10:20:24 -05:00
f func ( context . Context ) ( any , error )
2019-01-02 06:33:26 -05:00
}
// Add adds a func as a new child dependency.
2023-02-11 10:20:24 -05:00
func ( ini * Init ) Add ( initFn func ( context . Context ) ( any , error ) ) * Init {
2019-01-02 06:33:26 -05:00
if ini == nil {
ini = New ( )
}
return ini . add ( false , initFn )
}
2022-02-09 07:41:04 -05:00
// InitCount gets the number of this this Init has been initialized.
func ( ini * Init ) InitCount ( ) int {
i := atomic . LoadUint64 ( & ini . initCount )
return int ( i )
}
2019-01-02 06:33:26 -05:00
// AddWithTimeout is same as Add, but with a timeout that aborts initialization.
2022-03-17 17:03:27 -04:00
func ( ini * Init ) AddWithTimeout ( timeout time . Duration , f func ( ctx context . Context ) ( any , error ) ) * Init {
2023-02-11 10:20:24 -05:00
return ini . Add ( func ( ctx context . Context ) ( any , error ) {
return ini . withTimeout ( ctx , timeout , f )
2019-01-02 06:33:26 -05:00
} )
}
// Branch creates a new dependency branch based on an existing and adds
// the given dependency as a child.
2023-02-11 10:20:24 -05:00
func ( ini * Init ) Branch ( initFn func ( context . Context ) ( any , error ) ) * Init {
2019-01-02 06:33:26 -05:00
if ini == nil {
ini = New ( )
}
return ini . add ( true , initFn )
}
2023-05-18 05:05:56 -04:00
// BranchWithTimeout is same as Branch, but with a timeout.
2022-03-17 17:03:27 -04:00
func ( ini * Init ) BranchWithTimeout ( timeout time . Duration , f func ( ctx context . Context ) ( any , error ) ) * Init {
2023-02-11 10:20:24 -05:00
return ini . Branch ( func ( ctx context . Context ) ( any , error ) {
return ini . withTimeout ( ctx , timeout , f )
2019-01-02 06:33:26 -05:00
} )
}
// Do initializes the entire dependency graph.
2023-02-11 10:20:24 -05:00
func ( ini * Init ) Do ( ctx context . Context ) ( any , error ) {
2019-01-02 06:33:26 -05:00
if ini == nil {
panic ( "init is nil" )
}
ini . init . Do ( func ( ) {
2022-02-09 07:41:04 -05:00
atomic . AddUint64 ( & ini . initCount , 1 )
2019-01-02 06:33:26 -05:00
prev := ini . prev
2019-04-29 13:05:28 -04:00
if prev != nil {
// A branch. Initialize the ancestors.
2019-01-02 06:33:26 -05:00
if prev . shouldInitialize ( ) {
2023-02-11 10:20:24 -05:00
_ , err := prev . Do ( ctx )
2019-04-29 13:05:28 -04:00
if err != nil {
ini . err = err
return
}
} else if prev . inProgress ( ) {
// Concurrent initialization. The following init func
// may depend on earlier state, so wait.
prev . wait ( )
2019-01-02 06:33:26 -05:00
}
}
if ini . f != nil {
2023-02-11 10:20:24 -05:00
ini . out , ini . err = ini . f ( ctx )
2019-01-02 06:33:26 -05:00
}
2019-04-29 13:05:28 -04:00
for _ , child := range ini . children {
if child . shouldInitialize ( ) {
2023-02-11 10:20:24 -05:00
_ , err := child . Do ( ctx )
2019-04-29 13:05:28 -04:00
if err != nil {
ini . err = err
return
}
2019-01-02 06:33:26 -05:00
}
}
} )
2019-04-29 13:05:28 -04:00
ini . wait ( )
return ini . out , ini . err
}
// TODO(bep) investigate if we can use sync.Cond for this.
func ( ini * Init ) wait ( ) {
2019-01-02 06:33:26 -05:00
var counter time . Duration
for ! ini . init . Done ( ) {
counter += 10
if counter > 600000000 {
panic ( "BUG: timed out in lazy init" )
}
time . Sleep ( counter * time . Microsecond )
}
2019-04-29 13:05:28 -04:00
}
2019-01-02 06:33:26 -05:00
2019-04-29 13:05:28 -04:00
func ( ini * Init ) inProgress ( ) bool {
return ini != nil && ini . init . InProgress ( )
2019-01-02 06:33:26 -05:00
}
func ( ini * Init ) shouldInitialize ( ) bool {
return ! ( ini == nil || ini . init . Done ( ) || ini . init . InProgress ( ) )
}
// Reset resets the current and all its dependencies.
func ( ini * Init ) Reset ( ) {
mu := ini . init . ResetWithLock ( )
2021-12-02 10:49:44 -05:00
ini . err = nil
2019-01-02 06:33:26 -05:00
defer mu . Unlock ( )
for _ , d := range ini . children {
d . Reset ( )
}
}
2023-02-11 10:20:24 -05:00
func ( ini * Init ) add ( branch bool , initFn func ( context . Context ) ( any , error ) ) * Init {
2019-01-02 06:33:26 -05:00
ini . mu . Lock ( )
defer ini . mu . Unlock ( )
2019-04-29 13:05:28 -04:00
if branch {
return & Init {
f : initFn ,
prev : ini ,
}
2019-01-02 06:33:26 -05:00
}
2019-04-29 13:05:28 -04:00
ini . checkDone ( )
ini . children = append ( ini . children , & Init {
f : initFn ,
} )
2019-01-02 06:33:26 -05:00
2019-04-29 13:05:28 -04:00
return ini
2019-01-02 06:33:26 -05:00
}
func ( ini * Init ) checkDone ( ) {
if ini . init . Done ( ) {
panic ( "init cannot be added to after it has run" )
}
}
2023-02-11 10:20:24 -05:00
func ( ini * Init ) withTimeout ( ctx context . Context , timeout time . Duration , f func ( ctx context . Context ) ( any , error ) ) ( any , error ) {
2023-03-04 12:08:29 -05:00
// Create a new context with a timeout not connected to the incoming context.
waitCtx , cancel := context . WithTimeout ( context . Background ( ) , timeout )
2019-01-02 06:33:26 -05:00
defer cancel ( )
c := make ( chan verr , 1 )
go func ( ) {
v , err := f ( ctx )
select {
2023-03-04 12:08:29 -05:00
case <- waitCtx . Done ( ) :
2019-01-02 06:33:26 -05:00
return
default :
c <- verr { v : v , err : err }
}
} ( )
select {
2023-03-04 12:08:29 -05:00
case <- waitCtx . Done ( ) :
2020-04-26 17:13:25 -04:00
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." )
2019-01-02 06:33:26 -05:00
case ve := <- c :
return ve . v , ve . err
}
}
type verr struct {
2022-03-17 17:03:27 -04:00
v any
2019-01-02 06:33:26 -05:00
err error
}