Skip to content

Commit

Permalink
feat: add auto-reloading for html render (#207)
Browse files Browse the repository at this point in the history
Co-authored-by: byene0923 <[email protected]>
  • Loading branch information
welkeyever and byene0923 authored Sep 1, 2022
1 parent 510696f commit 9c4d0da
Show file tree
Hide file tree
Showing 12 changed files with 365 additions and 11 deletions.
5 changes: 5 additions & 0 deletions examples/html_rendering/index.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<html>
<h1>
{[{ .title }]}
</h1>
</html>
60 changes: 60 additions & 0 deletions examples/html_rendering/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2022 CloudWeGo Authors
*
* 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 main

import (
"context"
"fmt"
"html/template"
"time"

"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/common/utils"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)

func formatAsDate(t time.Time) string {
year, month, day := t.Date()
return fmt.Sprintf("%d/%02d/%02d", year, month, day)
}

func main() {
// set interval to 0 means using fs-watching mechanism.
h := server.Default(server.WithAutoReloadRender(true, 0))

h.Delims("{[{", "}]}")

h.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate,
})
h.LoadHTMLGlob("./examples/html_rendering/*")

h.GET("/index", func(c context.Context, ctx *app.RequestContext) {
ctx.HTML(consts.StatusOK, "index.tmpl", utils.H{
"title": "Main website",
})
})

h.GET("/raw", func(c context.Context, ctx *app.RequestContext) {
ctx.HTML(consts.StatusOK, "template.html", utils.H{
"now": time.Date(2017, 0o7, 0o1, 0, 0, 0, 0, time.UTC),
})
})

h.Spin()
}
1 change: 1 addition & 0 deletions examples/html_rendering/template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Date: {[{.now | formatAsDate}]}</h1>
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ require (
github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7
github.com/bytedance/sonic v1.3.0
github.com/cloudwego/netpoll v0.2.4
github.com/fsnotify/fsnotify v1.5.4
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad
google.golang.org/protobuf v1.27.1
)
5 changes: 4 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ github.com/cloudwego/netpoll v0.2.4/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzC
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/goccy/go-json v0.9.4 h1:L8MLKG2mvVXiQu07qB6hmfqeSYQdOnqPot2GhsIwIaI=
github.com/goccy/go-json v0.9.4/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
Expand Down Expand Up @@ -58,8 +60,9 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VA
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe h1:W8vbETX/n8S6EmY0Pu4Ix7VvpsJUESTwl0oCK8MJOgk=
golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
Expand Down
11 changes: 11 additions & 0 deletions pkg/app/server/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,14 @@ func WithRegistry(r registry.Registry, info *registry.Info) config.Option {
o.RegistryInfo = info
}}
}

// WithAutoReloadRender sets the config of auto reload render.
// If auto reload render is enabled:
// 1. interval = 0 means reload render according to file watch mechanism.(recommended)
// 2. interval > 0 means reload render every interval.
func WithAutoReloadRender(b bool, interval time.Duration) config.Option {
return config.Option{F: func(o *config.Options) {
o.AutoReloadRender = b
o.AutoReloadInterval = interval
}}
}
6 changes: 6 additions & 0 deletions pkg/app/server/option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func TestOptions(t *testing.T) {
WithALPN(true),
WithTraceLevel(stats.LevelDisabled),
WithRegistry(nil, info),
WithAutoReloadRender(true, 5*time.Second),
})
assert.DeepEqual(t, opt.ReadTimeout, time.Second)
assert.DeepEqual(t, opt.IdleTimeout, time.Second)
Expand All @@ -79,6 +80,8 @@ func TestOptions(t *testing.T) {
assert.DeepEqual(t, opt.TraceLevel, stats.LevelDisabled)
assert.DeepEqual(t, opt.RegistryInfo, info)
assert.DeepEqual(t, opt.Registry, nil)
assert.DeepEqual(t, opt.AutoReloadRender, true)
assert.DeepEqual(t, opt.AutoReloadInterval, 5*time.Second)
}

func TestDefaultOptions(t *testing.T) {
Expand All @@ -103,5 +106,8 @@ func TestDefaultOptions(t *testing.T) {
assert.DeepEqual(t, opt.ReadBufferSize, 4096)
assert.DeepEqual(t, opt.ALPN, false)
assert.DeepEqual(t, opt.Registry, registry.NoopRegistry)
assert.DeepEqual(t, opt.AutoReloadRender, false)
assert.Assert(t, opt.RegistryInfo == nil)
assert.DeepEqual(t, opt.AutoReloadRender, false)
assert.DeepEqual(t, opt.AutoReloadInterval, time.Duration(0))
}
113 changes: 112 additions & 1 deletion pkg/app/server/render/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,13 @@ package render

import (
"html/template"
"log"
"sync"
"time"

"github.com/cloudwego/hertz/pkg/common/hlog"
"github.com/cloudwego/hertz/pkg/protocol"
"github.com/fsnotify/fsnotify"
)

// Delims represents a set of Left and Right delimiters for HTML template rendering.
Expand All @@ -59,12 +64,12 @@ type Delims struct {
type HTMLRender interface {
// Instance returns an HTML instance.
Instance(string, interface{}) Render
Close() error
}

// HTMLProduction contains template reference and its delims.
type HTMLProduction struct {
Template *template.Template
Delims Delims
}

// HTML contains template reference and its name with given interface object.
Expand All @@ -85,6 +90,10 @@ func (r HTMLProduction) Instance(name string, data interface{}) Render {
}
}

func (r HTMLProduction) Close() error {
return nil
}

// Render (HTML) executes template and writes its result with custom ContentType for response.
func (r HTML) Render(resp *protocol.Response) error {
r.WriteContentType(resp)
Expand All @@ -99,3 +108,105 @@ func (r HTML) Render(resp *protocol.Response) error {
func (r HTML) WriteContentType(resp *protocol.Response) {
writeContentType(resp, htmlContentType)
}

type HTMLDebug struct {
sync.Once
Template *template.Template
RefreshInterval time.Duration
updateTimeStamp time.Time

Files []string
FuncMap template.FuncMap
Delims Delims

reloadCh chan struct{}
watcher *fsnotify.Watcher
}

func (h *HTMLDebug) Instance(name string, data interface{}) Render {
h.Do(func() {
h.startChecker()
})

select {
case <-h.reloadCh:
h.reload()
default:
}

return HTML{
Template: h.Template,
Name: name,
Data: data,
}
}

func (h *HTMLDebug) Close() error {
if h.watcher == nil {
return nil
}
return h.watcher.Close()
}

func (h *HTMLDebug) reload() {
h.Template = template.Must(template.New("").
Delims(h.Delims.Left, h.Delims.Right).
Funcs(h.FuncMap).
ParseFiles(h.Files...))
}

func (h *HTMLDebug) startChecker() {
h.reloadCh = make(chan struct{})

if h.RefreshInterval > 0 {
go func() {
hlog.Debugf("HERTZ[HTMLDebug]: HTML template reloader started with interval %v", h.RefreshInterval)
for {
n := time.Now()
if n.UTC().Sub(h.updateTimeStamp.UTC()) > h.RefreshInterval {
hlog.Debugf("HERTZ[HTMLDebug]: triggering HTML template reloader")
h.reloadCh <- struct{}{}
hlog.Debugf("HERTZ[HTMLDebug]: HTML template has been reloaded, next reload in %v", h.RefreshInterval)
h.updateTimeStamp = time.Now()
}
}
}()
return
}

watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
h.watcher = watcher
for _, f := range h.Files {
err := watcher.Add(f)
hlog.Debugf("HERTZ[HTMLDebug]: watching file: %s", f)
if err != nil {
hlog.Errorf("HERTZ[HTMLDebug]: add watching file: %s, error happened: %v", f, err)
}

}

go func() {
hlog.Debugf("HERTZ[HTMLDebug]: HTML template reloader started with file watcher")
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write == fsnotify.Write {
hlog.Debugf("HERTZ[HTMLDebug]: modified file: %s, html render template will be reloaded at the next rendering", event.Name)
h.reloadCh <- struct{}{}
hlog.Debugf("HERTZ[HTMLDebug]: HTML template has been reloaded")
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
hlog.Errorf("HERTZ: error happened when watching the rendering files: %v", err)
}
}
}()
}
73 changes: 73 additions & 0 deletions pkg/app/server/render/html_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2022 CloudWeGo Authors
*
* 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 render

import (
"os"
"testing"
"time"
)

func TestHTMLDebug_StartChecker_timer(t *testing.T) {
render := &HTMLDebug{RefreshInterval: 2 * time.Second}
select {
case <-render.reloadCh:
t.Fatalf("should not be triggered")
default:
}
render.startChecker()
select {
case <-time.After(50 * time.Millisecond):
t.Fatalf("should be triggered immediately")
case <-render.reloadCh:

}
select {
// some ci servers have poor computing power, so we add some extra time here.
case <-time.After(2*time.Second + 50*time.Millisecond):
t.Fatalf("should be triggered before 2 seconds")
case <-render.reloadCh:
t.Logf("paas")
}
}

func TestHTMLDebug_StartChecker_fs_watcher(t *testing.T) {
f, _ := os.CreateTemp("./", "test.tmpl")
defer func() {
f.Close()
os.Remove(f.Name())
}()
render := &HTMLDebug{Files: []string{f.Name()}}
select {
case <-render.reloadCh:
t.Fatalf("should not be triggered")
default:
}
render.startChecker()
f.Write([]byte("hello"))

select {
case <-time.After(50 * time.Millisecond):
t.Fatalf("should be triggered immediately")
case <-render.reloadCh:
}
select {
case <-render.reloadCh:
t.Fatalf("should not be triggered")
default:
}
}
6 changes: 6 additions & 0 deletions pkg/common/config/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ type Options struct {
Registry registry.Registry
// RegistryInfo is base info used for service registry.
RegistryInfo *registry.Info
// Enable automatically HTML template reloading mechanism.
AutoReloadRender bool
// If AutoReloadInterval is set to 0(default).
// The HTML template will reload according to files' changing event
// otherwise it will reload after AutoReloadInterval.
AutoReloadInterval time.Duration
}

func (o *Options) Apply(opts []Option) {
Expand Down
Loading

0 comments on commit 9c4d0da

Please sign in to comment.