Skip to content

Commit ac828e7

Browse files
committed
attempt to process esi in parallel
1 parent dfdfaa8 commit ac828e7

File tree

17 files changed

+149
-40
lines changed

17 files changed

+149
-40
lines changed

.github/workflows/middlewares.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
name: Install Go
1717
uses: actions/setup-go@v3
1818
with:
19-
go-version: 1.19
19+
go-version: 1.22
2020
-
2121
name: Checkout code
2222
uses: actions/checkout@v3

.github/workflows/non-regression.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ jobs:
2020
name: install Go
2121
uses: actions/setup-go@v3
2222
with:
23-
go-version: 1.19
23+
go-version: 1.22
2424
-
2525
name: golangci-lint
26-
uses: golangci/golangci-lint-action@v3
26+
uses: golangci/golangci-lint-action@v6
2727
-
2828
name: install xcaddy
2929
run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

.github/workflows/release.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
name: Set up Go
1414
uses: actions/setup-go@v2
1515
with:
16-
go-version: 1.19
16+
go-version: 1.22
1717
-
1818
name: Checkout
1919
uses: actions/checkout@v2

.golangci.yml

+2-4
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,16 @@ linters:
77
disable:
88
- bodyclose
99
- cyclop
10-
- exhaustivestruct
10+
- depguard
1111
- exhaustruct
1212
- forbidigo
1313
- gochecknoglobals
1414
- gci
15-
- golint
1615
- gomnd
17-
- ifshort
1816
- ireturn
1917
- nestif
2018
- nonamedreturns
21-
- nosnakecase
19+
- mnd
2220
- revive
2321
- testpackage
2422
- stylecheck

esi/esi.go

+64-19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package esi
22

33
import (
44
"net/http"
5+
"slices"
6+
"sync"
57
)
68

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

50-
func CanProcess(b []byte) bool {
51-
if tag := findTagName(b); tag != nil {
52-
return tag.HasClose(b)
53-
}
54-
55-
return false
56-
}
57-
5852
func ReadToTag(next []byte, pointer int) (startTagPosition, esiPointer int, t Tag) {
5953
var isEscapeTag bool
6054

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

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

8782
for pointer < len(b) {
88-
var escapeTag bool
89-
9083
next := b[pointer:]
9184
tagIdx := esi.FindIndex(next)
9285

9386
if escIdx := escapeRg.FindIndex(next); escIdx != nil && (tagIdx == nil || escIdx[0] < tagIdx[0]) {
9487
tagIdx = escIdx
9588
tagIdx[1] = escIdx[0]
96-
escapeTag = true
9789
}
9890

9991
if tagIdx == nil {
10092
break
10193
}
10294

103-
esiPointer := tagIdx[1]
104-
t := findTagName(next[esiPointer:])
95+
t := findTagName(next[tagIdx[1]:])
96+
97+
if t != nil {
98+
includes[pointer+tagIdx[0]] = t
99+
}
105100

106-
if escapeTag {
107-
esiPointer += 7
101+
switch t.(type) {
102+
case *escapeTag:
103+
pointer += tagIdx[1] + 7
104+
default:
105+
pointer += tagIdx[1]
108106
}
107+
}
108+
109+
if len(includes) == 0 {
110+
return b
111+
}
112+
113+
return processEsiTags(b, req, includes)
114+
}
115+
116+
func processEsiTags(b []byte, req *http.Request, includes map[int]Tag) []byte {
117+
type Response struct {
118+
Start int
119+
End int
120+
Res []byte
121+
Tag Tag
122+
}
123+
124+
var wg sync.WaitGroup
125+
126+
responses := make(chan Response, len(includes))
127+
128+
for start, t := range includes {
129+
wg.Add(1)
130+
131+
go func(s int, tag Tag) {
132+
defer wg.Done()
133+
134+
data, length := tag.Process(b[s:], req)
135+
136+
responses <- Response{
137+
Start: s,
138+
End: s + length,
139+
Res: data,
140+
Tag: tag,
141+
}
142+
}(start, t)
143+
}
144+
145+
wg.Wait()
146+
close(responses)
147+
148+
// we need to replace from the bottom to top
149+
sorted := make([]Response, 0, len(responses))
150+
for r := range responses {
151+
sorted = append(sorted, r)
152+
}
109153

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

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

117162
return b

esi/esi_test.go

+27-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package esi_test
22

33
import (
4+
"fmt"
45
"net/http"
56
"net/http/httptest"
67
"os"
8+
"strings"
79
"testing"
810

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

65-
if result := string(esi.Parse(loadFromFixtures(fixture), getRequest())); result != expected[fixture] {
67+
html := loadFromFixtures(fixture)
68+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
69+
_, _ = w.Write([]byte("<h1>CHAINED 2</h1>"))
70+
}))
71+
html = []byte(strings.ReplaceAll(string(html), "http://domain.com:9080", server.URL))
72+
73+
if result := string(esi.Parse(html, getRequest())); result != expected[fixture] {
6674
t.Errorf("ESI parsing mismatch from `%s` expected\nExpected:\n%+v\nGiven:\n%+v\n", fixture, expected[fixture], result)
6775
}
6876
}
@@ -114,10 +122,17 @@ func Test_Parse_fullMock(t *testing.T) {
114122

115123
// Benchmarks.
116124
func BenchmarkInclude(b *testing.B) {
125+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
126+
_, _ = w.Write([]byte("<h1>CHAINED 2</h1>"))
127+
}))
128+
117129
for i := 0; i < b.N; i++ {
118130
esi.Parse(
119131
[]byte(
120-
`<esi:include src="http://domain.com:9080/chained-esi-include-1" alt=http://domain.com:9080/alt-esi-include/>`,
132+
fmt.Sprintf(
133+
`<esi:include src="http://%s/chained-esi-include-1" alt=http://domain.com:9080/alt-esi-include/>`,
134+
server.URL,
135+
),
121136
),
122137
httptest.NewRequest(http.MethodGet, "http://domain.com:9080", nil),
123138
)
@@ -132,9 +147,13 @@ var remove = `<esi:include src="http://domain.com:9080/chained-esi-include-1"/>
132147
<esi:include src="http://domain.com:9080/chained-esi-include-1"/>`
133148

134149
func BenchmarkRemove(b *testing.B) {
150+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151+
_, _ = w.Write([]byte("<h1>CHAINED 2</h1>"))
152+
}))
153+
135154
for i := 0; i < b.N; i++ {
136155
esi.Parse(
137-
[]byte(remove),
156+
[]byte(strings.ReplaceAll(remove, "domain.com:9080", server.URL)),
138157
httptest.NewRequest(http.MethodGet, "http://domain.com:9080", nil),
139158
)
140159
}
@@ -171,9 +190,13 @@ const full = `<html>
171190
`
172191

173192
func BenchmarkFull(b *testing.B) {
193+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
194+
_, _ = w.Write([]byte("<h1>CHAINED 2</h1>"))
195+
}))
196+
174197
for i := 0; i < b.N; i++ {
175198
esi.Parse(
176-
[]byte(full),
199+
[]byte(strings.ReplaceAll(full, "domain.com:9080", server.URL)),
177200
httptest.NewRequest(http.MethodGet, "http://domain.com:9080", nil),
178201
)
179202
}

esi/include.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
const include = "include"
1313

1414
var (
15-
closeInclude = regexp.MustCompile("/>")
15+
closeInclude = regexp.MustCompile(">")
1616
srcAttribute = regexp.MustCompile(`src="?(.+?)"?( |/>)`)
1717
altAttribute = regexp.MustCompile(`alt="?(.+?)"?( |/>)`)
1818
onErrorAttribute = regexp.MustCompile(`onerror="?(.+?)"?( |/>)`)

esi/type.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import (
66

77
type (
88
Tag interface {
9-
Process([]byte, *http.Request) ([]byte, int)
10-
HasClose([]byte) bool
11-
GetClosePosition([]byte) int
9+
Process(body []byte, request *http.Request) ([]byte, int)
10+
HasClose(body []byte) bool
11+
GetClosePosition(body []byte) int
1212
}
1313

1414
baseTag struct {

esi/vars.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ type varsTag struct {
7676
*baseTag
7777
}
7878

79+
const varTagLength = len("<esi:vars>")
80+
7981
// Input (e.g. comment text="This is a comment." />).
8082
func (c *varsTag) Process(b []byte, req *http.Request) ([]byte, int) {
8183
found := closeVars.FindIndex(b)
@@ -85,7 +87,7 @@ func (c *varsTag) Process(b []byte, req *http.Request) ([]byte, int) {
8587

8688
c.length = found[1]
8789

88-
return interpretedVar.ReplaceAllFunc(b[5:found[0]], func(b []byte) []byte {
90+
return interpretedVar.ReplaceAllFunc(b[varTagLength:found[0]], func(b []byte) []byte {
8991
return []byte(parseVariables(b, req))
9092
}), c.length
9193
}

esi/when.go

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func validateTest(b []byte, req *http.Request) bool {
2323
} else if r := comparison.FindSubmatch(b); r != nil {
2424
r1 := strings.TrimSpace(parseVariables(r[1], req))
2525
r2 := strings.TrimSpace(parseVariables(r[3], req))
26+
2627
switch string(r[2]) {
2728
case "==":
2829
return r1 == r2

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module github.com/darkweak/go-esi
22

3-
go 1.19
3+
go 1.22

middleware/caddy/esi.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
"github.com/darkweak/go-esi/writer"
1313
)
1414

15-
var bufPool *sync.Pool = &sync.Pool{
15+
var bufPool = &sync.Pool{
1616
New: func() any {
1717
return &bytes.Buffer{}
1818
},

middleware/caddy/go.mod

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module github.com/darkweak/go-esi/middleware/caddy
22

3-
go 1.19
3+
go 1.22
4+
5+
toolchain go1.23.1
46

57
require (
68
github.com/caddyserver/caddy/v2 v2.6.4

0 commit comments

Comments
 (0)