Skip to content

attempt to process esi in parallel #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/middlewares.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
name: Install Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: 1.22
-
name: Checkout code
uses: actions/checkout@v3
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/non-regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jobs:
name: install Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: 1.22
-
name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
-
name: install xcaddy
run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
go-version: 1.22
-
name: Checkout
uses: actions/checkout@v2
Expand Down
6 changes: 2 additions & 4 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,16 @@ linters:
disable:
- bodyclose
- cyclop
- exhaustivestruct
- depguard
- exhaustruct
- forbidigo
- gochecknoglobals
- gci
- golint
- gomnd
- ifshort
- ireturn
- nestif
- nonamedreturns
- nosnakecase
- mnd
- revive
- testpackage
- stylecheck
Expand Down
83 changes: 64 additions & 19 deletions esi/esi.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package esi

import (
"net/http"
"slices"
"sync"
)

func findTagName(b []byte) Tag {
Expand Down Expand Up @@ -47,14 +49,6 @@ func HasOpenedTags(b []byte) bool {
return esi.FindIndex(b) != nil || escapeRg.FindIndex(b) != nil
}

func CanProcess(b []byte) bool {
if tag := findTagName(b); tag != nil {
return tag.HasClose(b)
}

return false
}

func ReadToTag(next []byte, pointer int) (startTagPosition, esiPointer int, t Tag) {
var isEscapeTag bool

Expand Down Expand Up @@ -83,35 +77,86 @@ func ReadToTag(next []byte, pointer int) (startTagPosition, esiPointer int, t Ta

func Parse(b []byte, req *http.Request) []byte {
pointer := 0
includes := make(map[int]Tag)

for pointer < len(b) {
var escapeTag bool

next := b[pointer:]
tagIdx := esi.FindIndex(next)

if escIdx := escapeRg.FindIndex(next); escIdx != nil && (tagIdx == nil || escIdx[0] < tagIdx[0]) {
tagIdx = escIdx
tagIdx[1] = escIdx[0]
escapeTag = true
}

if tagIdx == nil {
break
}

esiPointer := tagIdx[1]
t := findTagName(next[esiPointer:])
t := findTagName(next[tagIdx[1]:])

if t != nil {
includes[pointer+tagIdx[0]] = t
}

if escapeTag {
esiPointer += 7
switch t.(type) {
case *escapeTag:
pointer += tagIdx[1] + 7
default:
pointer += tagIdx[1]
}
}

if len(includes) == 0 {
return b
}

return processEsiTags(b, req, includes)
}

func processEsiTags(b []byte, req *http.Request, includes map[int]Tag) []byte {
type Response struct {
Start int
End int
Res []byte
Tag Tag
}

var wg sync.WaitGroup

responses := make(chan Response, len(includes))

for start, t := range includes {
wg.Add(1)

go func(s int, tag Tag) {
defer wg.Done()

data, length := tag.Process(b[s:], req)

responses <- Response{
Start: s,
End: s + length,
Res: data,
Tag: tag,
}
}(start, t)
}

wg.Wait()
close(responses)

// we need to replace from the bottom to top
sorted := make([]Response, 0, len(responses))
for r := range responses {
sorted = append(sorted, r)
}

res, p := t.Process(next[esiPointer:], req)
esiPointer += p
slices.SortFunc(sorted, func(a, b Response) int {
return b.Start - a.Start
})

b = append(b[:pointer], append(next[:tagIdx[0]], append(res, next[esiPointer:]...)...)...)
pointer += len(res) + tagIdx[0]
for _, r := range sorted {
b = append(b[:r.Start], append(r.Res, b[r.End:]...)...)
}

return b
Expand Down
31 changes: 27 additions & 4 deletions esi/esi_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package esi_test

import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

"github.com/darkweak/go-esi/esi"
Expand Down Expand Up @@ -62,7 +64,13 @@ var expected = map[string]string{
func verify(t *testing.T, fixture string) {
t.Helper()

if result := string(esi.Parse(loadFromFixtures(fixture), getRequest())); result != expected[fixture] {
html := loadFromFixtures(fixture)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("<h1>CHAINED 2</h1>"))
}))
html = []byte(strings.ReplaceAll(string(html), "http://domain.com:9080", server.URL))

if result := string(esi.Parse(html, getRequest())); result != expected[fixture] {
t.Errorf("ESI parsing mismatch from `%s` expected\nExpected:\n%+v\nGiven:\n%+v\n", fixture, expected[fixture], result)
}
}
Expand Down Expand Up @@ -114,10 +122,17 @@ func Test_Parse_fullMock(t *testing.T) {

// Benchmarks.
func BenchmarkInclude(b *testing.B) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("<h1>CHAINED 2</h1>"))
}))

for i := 0; i < b.N; i++ {
esi.Parse(
[]byte(
`<esi:include src="http://domain.com:9080/chained-esi-include-1" alt=http://domain.com:9080/alt-esi-include/>`,
fmt.Sprintf(
`<esi:include src="http://%s/chained-esi-include-1" alt=http://domain.com:9080/alt-esi-include/>`,
server.URL,
),
),
httptest.NewRequest(http.MethodGet, "http://domain.com:9080", nil),
)
Expand All @@ -132,9 +147,13 @@ var remove = `<esi:include src="http://domain.com:9080/chained-esi-include-1"/>
<esi:include src="http://domain.com:9080/chained-esi-include-1"/>`

func BenchmarkRemove(b *testing.B) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("<h1>CHAINED 2</h1>"))
}))

for i := 0; i < b.N; i++ {
esi.Parse(
[]byte(remove),
[]byte(strings.ReplaceAll(remove, "domain.com:9080", server.URL)),
httptest.NewRequest(http.MethodGet, "http://domain.com:9080", nil),
)
}
Expand Down Expand Up @@ -171,9 +190,13 @@ const full = `<html>
`

func BenchmarkFull(b *testing.B) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("<h1>CHAINED 2</h1>"))
}))

for i := 0; i < b.N; i++ {
esi.Parse(
[]byte(full),
[]byte(strings.ReplaceAll(full, "domain.com:9080", server.URL)),
httptest.NewRequest(http.MethodGet, "http://domain.com:9080", nil),
)
}
Expand Down
2 changes: 1 addition & 1 deletion esi/include.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
const include = "include"

var (
closeInclude = regexp.MustCompile("/>")
closeInclude = regexp.MustCompile(">")
srcAttribute = regexp.MustCompile(`src="?(.+?)"?( |/>)`)
altAttribute = regexp.MustCompile(`alt="?(.+?)"?( |/>)`)
onErrorAttribute = regexp.MustCompile(`onerror="?(.+?)"?( |/>)`)
Expand Down
6 changes: 3 additions & 3 deletions esi/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (

type (
Tag interface {
Process([]byte, *http.Request) ([]byte, int)
HasClose([]byte) bool
GetClosePosition([]byte) int
Process(body []byte, request *http.Request) ([]byte, int)
HasClose(body []byte) bool
GetClosePosition(body []byte) int
}

baseTag struct {
Expand Down
4 changes: 3 additions & 1 deletion esi/vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ type varsTag struct {
*baseTag
}

const varTagLength = len("<esi:vars>")

// Input (e.g. comment text="This is a comment." />).
func (c *varsTag) Process(b []byte, req *http.Request) ([]byte, int) {
found := closeVars.FindIndex(b)
Expand All @@ -85,7 +87,7 @@ func (c *varsTag) Process(b []byte, req *http.Request) ([]byte, int) {

c.length = found[1]

return interpretedVar.ReplaceAllFunc(b[5:found[0]], func(b []byte) []byte {
return interpretedVar.ReplaceAllFunc(b[varTagLength:found[0]], func(b []byte) []byte {
return []byte(parseVariables(b, req))
}), c.length
}
Expand Down
1 change: 1 addition & 0 deletions esi/when.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func validateTest(b []byte, req *http.Request) bool {
} else if r := comparison.FindSubmatch(b); r != nil {
r1 := strings.TrimSpace(parseVariables(r[1], req))
r2 := strings.TrimSpace(parseVariables(r[3], req))

switch string(r[2]) {
case "==":
return r1 == r2
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/darkweak/go-esi

go 1.19
go 1.22
2 changes: 1 addition & 1 deletion middleware/caddy/esi.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/darkweak/go-esi/writer"
)

var bufPool *sync.Pool = &sync.Pool{
var bufPool = &sync.Pool{
New: func() any {
return &bytes.Buffer{}
},
Expand Down
4 changes: 3 additions & 1 deletion middleware/caddy/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module github.com/darkweak/go-esi/middleware/caddy

go 1.19
go 1.22

toolchain go1.23.1

require (
github.com/caddyserver/caddy/v2 v2.6.4
Expand Down
Loading
Loading