From d58f86e700c2de64a8fdc97a58292998a7699b3e Mon Sep 17 00:00:00 2001 From: notzippy Date: Fri, 25 Aug 2017 22:09:14 -0700 Subject: [PATCH] Merge pull request #1225 from runner-mei/template-map-sync2 Fix for the template map concurrency issue --- template.go | 232 ++++++++++++++++++++++++++++++--------------- template_engine.go | 10 +- 2 files changed, 161 insertions(+), 81 deletions(-) diff --git a/template.go b/template.go index 9a1cfe93f..40c1490a9 100644 --- a/template.go +++ b/template.go @@ -15,6 +15,8 @@ import ( "regexp" "strconv" "strings" + "sync" + "sync/atomic" ) // ErrorCSSClass httml CSS error class name @@ -23,16 +25,14 @@ var ErrorCSSClass = "hasError" // TemplateLoader object handles loading and parsing of templates. // Everything below the application's views directory is treated as a template. type TemplateLoader struct { - // Template data and implementation - templatesAndEngineList []TemplateEngine - // If an error was encountered parsing the templates, it is stored here. - compileError *Error // Paths to search for templates, in priority order. paths []string - // Map from template name to the path from whence it was loaded. - TemplatePaths map[string]string - // A map of looked up template results - TemplateMap map[string]Template + // load version seed for templates + loadVersionSeed int + // A templateRuntime of looked up template results + runtimeLoader atomic.Value + // Lock to prevent concurrent map writes + templateMutex sync.Mutex } type Template interface { @@ -56,32 +56,61 @@ func NewTemplateLoader(paths []string) *TemplateLoader { return loader } -// Refresh method scans the views directory and parses all templates as Go Templates. -// If a template fails to parse, the error is set on the loader. -// (It's awkward to refresh a single Go Template) +// WatchDir returns true of directory doesn't start with . (dot) +// otherwise false +func (loader *TemplateLoader) WatchDir(info os.FileInfo) bool { + // Watch all directories, except the ones starting with a dot. + return !strings.HasPrefix(info.Name(), ".") +} + +// WatchFile returns true of file doesn't start with . (dot) +// otherwise false +func (loader *TemplateLoader) WatchFile(basename string) bool { + // Watch all files, except the ones starting with a dot. + return !strings.HasPrefix(basename, ".") +} + +// DEPRECATED Use TemplateLang, will be removed in future release +func (loader *TemplateLoader) Template(name string) (tmpl Template, err error) { + runtimeLoader := loader.runtimeLoader.Load().(*templateRuntime) + return runtimeLoader.TemplateLang(name, "") +} + +func (loader *TemplateLoader) TemplateLang(name, lang string) (tmpl Template, err error) { + runtimeLoader := loader.runtimeLoader.Load().(*templateRuntime) + return runtimeLoader.TemplateLang(name, lang) +} + // Refresh method scans the views directory and parses all templates as Go Templates. // If a template fails to parse, the error is set on the loader. // (It's awkward to refresh a single Go Template) func (loader *TemplateLoader) Refresh() (err *Error) { + loader.templateMutex.Lock() + defer loader.templateMutex.Unlock() + + loader.loadVersionSeed++ + runtimeLoader := &templateRuntime{loader: loader, + version: loader.loadVersionSeed, + templateMap: map[string]Template{}} + TRACE.Printf("Refreshing templates from %s", loader.paths) - if len(loader.templatesAndEngineList) == 0 { - if err = loader.InitializeEngines(GO_TEMPLATE); err != nil { - return - } + if err = loader.initializeEngines(runtimeLoader, GO_TEMPLATE); err != nil { + return } - for _, engine := range loader.templatesAndEngineList { + for _, engine := range runtimeLoader.templatesAndEngineList { engine.Event(TEMPLATE_REFRESH_REQUESTED, nil) } fireEvent(TEMPLATE_REFRESH_REQUESTED, nil) defer func() { - for _, engine := range loader.templatesAndEngineList { + for _, engine := range runtimeLoader.templatesAndEngineList { engine.Event(TEMPLATE_REFRESH_COMPLETED, nil) } fireEvent(TEMPLATE_REFRESH_COMPLETED, nil) - // Reset the TemplateMap, we don't prepopulate the map because - loader.TemplateMap = map[string]Template{} + // Reset the runtimeLoader + loader.runtimeLoader.Store(runtimeLoader) }() + // Resort the paths, make sure the revel path is the last path, // so anything can override it revelTemplatePath := filepath.Join(RevelPath, "templates") @@ -94,8 +123,8 @@ func (loader *TemplateLoader) Refresh() (err *Error) { } TRACE.Printf("Refreshing templates from %s", loader.paths) - loader.compileError = nil - loader.TemplatePaths = map[string]string{} + runtimeLoader.compileError = nil + runtimeLoader.TemplatePaths = map[string]string{} for _, basePath := range loader.paths { // Walk only returns an error if the template loader is completely unusable @@ -133,14 +162,14 @@ func (loader *TemplateLoader) Refresh() (err *Error) { return nil } - fileBytes, err := loader.findAndAddTemplate(path, fullSrcDir, basePath) + fileBytes, err := runtimeLoader.findAndAddTemplate(path, fullSrcDir, basePath) // Store / report the first error encountered. - if err != nil && loader.compileError == nil { - loader.compileError, _ = err.(*Error) - if nil == loader.compileError { + if err != nil && runtimeLoader.compileError == nil { + runtimeLoader.compileError, _ = err.(*Error) + if nil == runtimeLoader.compileError { _, line, description := ParseTemplateError(err) - loader.compileError = &Error{ + runtimeLoader.compileError = &Error{ Title: "Template Compilation Error", Path: path, Description: description, @@ -149,7 +178,7 @@ func (loader *TemplateLoader) Refresh() (err *Error) { } } ERROR.Printf("Template compilation error (In %s around line %d):\n\t%s", - path, loader.compileError.Line, err.Error()) + path, runtimeLoader.compileError.Line, err.Error()) } else if nil != err { //&& strings.HasPrefix(templateName, "errors/") { if compileError, ok := err.(*Error); ok { @@ -173,18 +202,32 @@ func (loader *TemplateLoader) Refresh() (err *Error) { // If there was an error with the Funcs, set it and return immediately. if funcErr != nil { - loader.compileError = funcErr.(*Error) - return loader.compileError + runtimeLoader.compileError = funcErr.(*Error) + return runtimeLoader.compileError } } // Note: compileError may or may not be set. - return loader.compileError + return runtimeLoader.compileError +} + +type templateRuntime struct { + loader *TemplateLoader + // load version for templates + version int + // Template data and implementation + templatesAndEngineList []TemplateEngine + // If an error was encountered parsing the templates, it is stored here. + compileError *Error + // Map from template name to the path from whence it was loaded. + TemplatePaths map[string]string + // A map of looked up template results + templateMap map[string]Template } // Checks to see if template exists in templatePaths, if so it is skipped (templates are imported in order // reads the template file into memory, replaces namespace keys with module (if found -func (loader *TemplateLoader) findAndAddTemplate(path, fullSrcDir, basePath string) (fileBytes []byte, err error) { +func (runtimeLoader *templateRuntime) findAndAddTemplate(path, fullSrcDir, basePath string) (fileBytes []byte, err error) { templateName := filepath.ToSlash(path[len(fullSrcDir)+1:]) // Convert template names to use forward slashes, even on Windows. if os.PathSeparator == '\\' { @@ -192,7 +235,7 @@ func (loader *TemplateLoader) findAndAddTemplate(path, fullSrcDir, basePath stri } // Check to see if template was found - if place, found := loader.TemplatePaths[templateName]; found { + if place, found := runtimeLoader.TemplatePaths[templateName]; found { TRACE.Println("Not Loading, template is already exists: ", templateName, "\r\n\told file:", place, "\r\n\tnew file:", path) return @@ -205,7 +248,7 @@ func (loader *TemplateLoader) findAndAddTemplate(path, fullSrcDir, basePath stri } // Parse template file and replace the "_RNS_|" in the template with the module name // allow for namespaces to be renamed "_RNS_(.*?)|" - if module := ModuleFromPath(path, false);module != nil { + if module := ModuleFromPath(path, false); module != nil { fileBytes = namespaceReplace(fileBytes, module) } @@ -213,17 +256,17 @@ func (loader *TemplateLoader) findAndAddTemplate(path, fullSrcDir, basePath stri baseTemplate := NewBaseTemplate(templateName, path, basePath, fileBytes) // Try to find a default engine for the file - for _, engine := range loader.templatesAndEngineList { + for _, engine := range runtimeLoader.templatesAndEngineList { if engine.Handles(baseTemplate) { - _, err = loader.loadIntoEngine(engine, baseTemplate) + _, err = runtimeLoader.loadIntoEngine(engine, baseTemplate) return } } // Try all engines available var defaultError error - for _, engine := range loader.templatesAndEngineList { - if loaded, loaderr := loader.loadIntoEngine(engine, baseTemplate); loaded { + for _, engine := range runtimeLoader.templatesAndEngineList { + if loaded, loaderr := runtimeLoader.loadIntoEngine(engine, baseTemplate); loaded { return } else { TRACE.Printf("Engine '%s' unable to compile %s %s", engine.Name(), path, loaderr) @@ -244,7 +287,14 @@ func (loader *TemplateLoader) findAndAddTemplate(path, fullSrcDir, basePath stri return } -func (loader *TemplateLoader) loadIntoEngine(engine TemplateEngine, baseTemplate *TemplateView) (loaded bool, err error) { +func (runtimeLoader *templateRuntime) loadIntoEngine(engine TemplateEngine, baseTemplate *TemplateView) (loaded bool, err error) { + if loadedTemplate, found := runtimeLoader.templateMap[baseTemplate.TemplateName]; found { + // Duplicate template found in map + TRACE.Println("template already exists in map: ", baseTemplate.TemplateName, " in engine ", engine.Name(), "\r\n\told file:", + loadedTemplate.Location(), "\r\n\tnew file:", baseTemplate.FilePath) + return + } + if loadedTemplate := engine.Lookup(baseTemplate.TemplateName); loadedTemplate != nil { // Duplicate template found for engine TRACE.Println("template already exists: ", baseTemplate.TemplateName, " in engine ", engine.Name(), "\r\n\told file:", @@ -253,7 +303,10 @@ func (loader *TemplateLoader) loadIntoEngine(engine TemplateEngine, baseTemplate return } if err = engine.ParseAndAdd(baseTemplate); err == nil { - loader.TemplatePaths[baseTemplate.TemplateName] = baseTemplate.FilePath + if tmpl := engine.Lookup(baseTemplate.TemplateName); tmpl != nil { + runtimeLoader.templateMap[baseTemplate.TemplateName] = tmpl + } + runtimeLoader.TemplatePaths[baseTemplate.TemplateName] = baseTemplate.FilePath TRACE.Printf("Engine '%s' compiled %s", engine.Name(), baseTemplate.FilePath) loaded = true } else { @@ -262,20 +315,6 @@ func (loader *TemplateLoader) loadIntoEngine(engine TemplateEngine, baseTemplate return } -// WatchDir returns true of directory doesn't start with . (dot) -// otherwise false -func (loader *TemplateLoader) WatchDir(info os.FileInfo) bool { - // Watch all directories, except the ones starting with a dot. - return !strings.HasPrefix(info.Name(), ".") -} - -// WatchFile returns true of file doesn't start with . (dot) -// otherwise false -func (loader *TemplateLoader) WatchFile(basename string) bool { - // Watch all files, except the ones starting with a dot. - return !strings.HasPrefix(basename, ".") -} - // Parse the line, and description from an error message like: // html/template:Application/Register.html:36: no such template "footer.html" func ParseTemplateError(err error) (templateName string, line int, description string) { @@ -300,53 +339,94 @@ func ParseTemplateError(err error) (templateName string, line int, description s return templateName, line, description } -// DEPRECATED Use TemplateLang, will be removed in future release -func (loader *TemplateLoader) Template(name string) (tmpl Template, err error) { - return loader.TemplateLang(name, "") -} // Template returns the Template with the given name. The name is the template's path // relative to a template loader root. // // An Error is returned if there was any problem with any of the templates. (In // this case, if a template is returned, it may still be usable.) -func (loader *TemplateLoader) TemplateLang(name, lang string) (tmpl Template, err error) { - if loader.compileError != nil { - return nil, loader.compileError - } -// Attempt to load a localized template first. - if lang != "" { - // Look up and return the template. - tmpl = loader.templateLoad(name + "." + lang) - } - // Return non localized version - if tmpl == nil { - tmpl = loader.templateLoad(name) +func (runtimeLoader *templateRuntime) TemplateLang(name, lang string) (tmpl Template, err error) { + if runtimeLoader.compileError != nil { + return nil, runtimeLoader.compileError } - if tmpl == nil && err == nil { + // Fetch the template from the map + tmpl = runtimeLoader.templateLoad(name, lang) + if tmpl == nil { err = fmt.Errorf("Template %s not found.", name) } return } -func (loader *TemplateLoader) templateLoad(name string) (tmpl Template) { - if t,found := loader.TemplateMap[name];!found && t != nil { - tmpl = t + +// Load and also updates map if name is not found (to speed up next lookup) +func (runtimeLoader *templateRuntime) templateLoad(name, lang string) (tmpl Template) { + langName := name + found := false + if lang != "" { + // Look up and return the template. + langName = name + "." + lang + tmpl, found = runtimeLoader.templateMap[langName] + if found { + return + } + tmpl, found = runtimeLoader.templateMap[name] } else { + tmpl, found = runtimeLoader.templateMap[name] + if found { + return + } + } + + if !found { + // Neither name is found // Look up and return the template. - for _, engine := range loader.templatesAndEngineList { + for _, engine := range runtimeLoader.templatesAndEngineList { + if tmpl = engine.Lookup(langName); tmpl != nil { + found = true + break + } if tmpl = engine.Lookup(name); tmpl != nil { - loader.TemplateMap[name] = tmpl + found = true break } } + if !found { + return + } + } + + // If we found anything store it in the map, we need to copy so we do not + // run into concurrency issues + runtimeLoader.loader.templateMutex.Lock() + defer runtimeLoader.loader.templateMutex.Unlock() + + // In case another thread has loaded the map, reload the atomic value and check + newRuntimeLoader := runtimeLoader.loader.runtimeLoader.Load().(*templateRuntime) + if newRuntimeLoader.version != runtimeLoader.version { + return + } + + newTemplateMap := map[string]Template{} + for k, v := range newRuntimeLoader.templateMap { + newTemplateMap[k] = v + } + newTemplateMap[langName] = tmpl + if _, found := newTemplateMap[name]; !found { + newTemplateMap[name] = tmpl } + runtimeCopy := &templateRuntime{} + *runtimeCopy = *newRuntimeLoader + runtimeCopy.templateMap = newTemplateMap + + // Set the atomic value + runtimeLoader.loader.runtimeLoader.Store(runtimeCopy) return } func (i *TemplateView) Location() string { return i.FilePath } + func (i *TemplateView) Content() (content []string) { if i.FileBytes != nil { // Parse the bytes @@ -358,7 +438,7 @@ func (i *TemplateView) Content() (content []string) { } return nil } + func NewBaseTemplate(templateName, filePath, basePath string, fileBytes []byte) *TemplateView { return &TemplateView{TemplateName: templateName, FilePath: filePath, FileBytes: fileBytes, BasePath: basePath} } - diff --git a/template_engine.go b/template_engine.go index acebfbcd7..077a387a3 100644 --- a/template_engine.go +++ b/template_engine.go @@ -67,25 +67,25 @@ func (loader *TemplateLoader) CreateTemplateEngine(templateEngineName string) (T } // Passing in a comma delimited list of engine names to be used with this loader to parse the template files -func (loader *TemplateLoader) InitializeEngines(templateEngineNameList string) (err *Error) { +func (loader *TemplateLoader) initializeEngines(runtimeLoader *templateRuntime, templateEngineNameList string) (err *Error) { // Walk through the template loader's paths and build up a template set. if templateEngineNameList == "" { templateEngineNameList = GO_TEMPLATE } - loader.templatesAndEngineList = []TemplateEngine{} + runtimeLoader.templatesAndEngineList = []TemplateEngine{} for _, engine := range strings.Split(templateEngineNameList, ",") { engine := strings.TrimSpace(strings.ToLower(engine)) if templateLoader, err := loader.CreateTemplateEngine(engine); err != nil { - loader.compileError = &Error{ + runtimeLoader.compileError = &Error{ Title: "Panic (Template Loader)", Description: err.Error(), } - return loader.compileError + return runtimeLoader.compileError } else { // Always assign a default engine, switch it if it is specified in the config - loader.templatesAndEngineList = append(loader.templatesAndEngineList, templateLoader) + runtimeLoader.templatesAndEngineList = append(runtimeLoader.templatesAndEngineList, templateLoader) } } return pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy