diff --git a/context.go b/context.go index a4fd648..dcd4de2 100644 --- a/context.go +++ b/context.go @@ -191,6 +191,8 @@ var defaultFormValue = func(ctx *Context, key string) []byte { } type Context struct { + // isCopy shows that whether it is a copy through ctx.Copy(). + isCopy bool conn network.Conn Request protocol.Request Response protocol.Response @@ -775,8 +777,8 @@ func (ctx *Context) Copy() *Context { conn: ctx.conn, Params: ctx.Params, } - ctx.Request.CopyTo(&cp.Request) - ctx.Response.CopyTo(&cp.Response) + ctx.Request.CopyToAndMark(&cp.Request) + ctx.Response.CopyToAndMark(&cp.Response) cp.index = rConsts.AbortIndex cp.handlers = nil cp.Keys = map[string]interface{}{} @@ -829,7 +831,7 @@ func (ctx *Context) ResetWithoutConn() { ctx.index = -1 ctx.fullPath = "" ctx.Keys = nil - + ctx.isCopy = false if ctx.finished != nil { close(ctx.finished) ctx.finished = nil diff --git a/examples/main.go b/examples/main.go index 91f5530..a9f6bd7 100644 --- a/examples/main.go +++ b/examples/main.go @@ -1,5 +1,26 @@ package main +import ( + "context" + + "github.com/oarkflow/frame" + "github.com/oarkflow/frame/server" +) + +func main() { + srv := server.New() + srv.GET("/", func(c context.Context, ctx *frame.Context) { + ctx.JSON(200, "Hello world") + }) + srv.GET("/update", func(c context.Context, ctx *frame.Context) { + srv.GET("/", func(c context.Context, ctx *frame.Context) { + ctx.JSON(200, "Bye World") + }) + }) + + srv.Spin() +} + /*func main() { secret := "OdR4DlWhZk6osDd0qXLdVT88lHOvj14K" v4 := paseto.NewPV4Local() diff --git a/go.mod b/go.mod index 7abbad2..ba5b379 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/oarkflow/frame go 1.21.0 require ( - github.com/bytedance/go-tagexpr/v2 v2.9.9 + github.com/bytedance/go-tagexpr/v2 v2.9.11 github.com/bytedance/gopkg v0.0.0-20230728082804-614d0af6619b github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 - github.com/cloudwego/netpoll v0.5.0 + github.com/cloudwego/netpoll v0.5.1 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/oarkflow/log v1.0.74 github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee @@ -21,14 +21,14 @@ require ( github.com/andeya/ameda v1.5.3 // indirect github.com/andeya/goutil v1.0.1 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/nyaruka/phonenumbers v1.1.8 // indirect + github.com/nyaruka/phonenumbers v1.0.55 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect diff --git a/go.sum b/go.sum index 94d56ac..8893b0f 100644 --- a/go.sum +++ b/go.sum @@ -2,15 +2,15 @@ github.com/andeya/ameda v1.5.3 h1:SvqnhQPZwwabS8HQTRGfJwWPl2w9ZIPInHAw9aE1Wlk= github.com/andeya/ameda v1.5.3/go.mod h1:FQDHRe1I995v6GG+8aJ7UIUToEmbdTJn/U26NCPIgXQ= github.com/andeya/goutil v1.0.1 h1:eiYwVyAnnK0dXU5FJsNjExkJW4exUGn/xefPt3k4eXg= github.com/andeya/goutil v1.0.1/go.mod h1:jEG5/QnnhG7yGxwFUX6Q+JGMif7sjdHmmNVjn7nhJDo= -github.com/bytedance/go-tagexpr/v2 v2.9.9 h1:nawxXMVp7/Fba0lnDiZACj1hy0cMfqWg94OJN3KB7vQ= -github.com/bytedance/go-tagexpr/v2 v2.9.9/go.mod h1:UAyKh4ZRLBPGsyTRFZoPqTni1TlojMdOJXQnEIPCX84= +github.com/bytedance/go-tagexpr/v2 v2.9.11 h1:jJgmoDKPKacGl0llPYbYL/+/2N+Ng0vV0ipbnVssXHY= +github.com/bytedance/go-tagexpr/v2 v2.9.11/go.mod h1:UAyKh4ZRLBPGsyTRFZoPqTni1TlojMdOJXQnEIPCX84= github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= github.com/bytedance/gopkg v0.0.0-20230728082804-614d0af6619b h1:R6PWoQtxEMpWJPHnpci+9LgFxCS7iJCfOGBvCgZeTKI= github.com/bytedance/gopkg v0.0.0-20230728082804-614d0af6619b/go.mod h1:FtQG3YbQG9L/91pbKSw787yBQPutC+457AvDW77fgUQ= github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 h1:SjZ2GvvOononHOpK84APFuMvxqsk3tEIaKH/z4Rpu3g= github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE= -github.com/cloudwego/netpoll v0.5.0 h1:oRrOp58cPCvK2QbMozZNDESvrxQaEHW2dCimmwH1lcU= -github.com/cloudwego/netpoll v0.5.0/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ= +github.com/cloudwego/netpoll v0.5.1 h1:zDUF7xF0C97I10fGlQFJ4jg65khZZMUvSu/TWX44Ohc= +github.com/cloudwego/netpoll v0.5.1/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ= 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= @@ -19,18 +19,16 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg= github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= -github.com/nyaruka/phonenumbers v1.1.8 h1:mjFu85FeoH2Wy18aOMUvxqi1GgAqiQSJsa/cCC5yu2s= -github.com/nyaruka/phonenumbers v1.1.8/go.mod h1:DC7jZd321FqUe+qWSNcHi10tyIyGNXGcNbfkPvdp1Vs= github.com/oarkflow/log v1.0.74 h1:ZF+G7ZMO2bHRcNMVovqa3LwkzxsaQqhaFCe92Gwwhtg= github.com/oarkflow/log v1.0.74/go.mod h1:GjB0Np5m9DXTwlS2fpkH5jDsiTMYhD60aG/9UegLNvw= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= @@ -58,9 +56,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= @@ -120,7 +117,6 @@ golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/pkg/protocol/args.go b/pkg/protocol/args.go index aa91b21..2963d5e 100644 --- a/pkg/protocol/args.go +++ b/pkg/protocol/args.go @@ -62,9 +62,11 @@ type argsScanner struct { type Args struct { noCopy nocopy.NoCopy //lint:ignore U1000 until noCopy is used - - args []argsKV - buf []byte + // isCopy shows that whether it is a copy through ctx.Copy(). + // Other APIs such as CopyTo do not need to handle this. + isCopy bool + args []argsKV + buf []byte } // Set sets 'key=value' argument. @@ -75,6 +77,16 @@ func (a *Args) Set(key, value string) { // Reset clears query args. func (a *Args) Reset() { a.args = a.args[:0] + + // a.ParseBytes() will trigger reset, which is a process during lazy load(read scenario), so do not reset this flag. + // Args is not a recycle object so the risk of dirty data is relatively low even though we do not reset this field. + // a.isCopy = false +} + +// CopyToAndMark copies all args to dst and mark the dst args as a copy. +func (a *Args) CopyToAndMark(dst *Args) { + dst.isCopy = true + a.CopyTo(dst) } // CopyTo copies all args to dst. @@ -343,6 +355,9 @@ func peekArgStrExists(h []argsKV, k string) (string, bool) { // // The returned value is valid until the next call to Args methods. func (a *Args) QueryString() []byte { + if a.isCopy { + return a.AppendBytes(nil) + } a.buf = a.AppendBytes(a.buf[:0]) return a.buf } diff --git a/pkg/protocol/header.go b/pkg/protocol/header.go index f7c879e..19053f7 100644 --- a/pkg/protocol/header.go +++ b/pkg/protocol/header.go @@ -65,7 +65,9 @@ var ( type RequestHeader struct { noCopy nocopy.NoCopy //lint:ignore U1000 until noCopy is used - + // isCopy shows that whether it is a copy through ctx.Copy(). + // Other APIs such as CopyTo do not need to handle this. + isCopy bool disableNormalizing bool connectionClose bool noDefaultContentType bool @@ -109,7 +111,9 @@ func (h *RequestHeader) SetRawHeaders(r []byte) { // goroutines. type ResponseHeader struct { noCopy nocopy.NoCopy //lint:ignore U1000 until noCopy is used - + // isCopy shows that whether it is a copy through ctx.Copy(). + // Other APIs such as CopyTo do not need to handle this. + isCopy bool disableNormalizing bool connectionClose bool noDefaultContentType bool @@ -219,11 +223,15 @@ func (h *ResponseHeader) GetHeaders() []argsKV { func (h *ResponseHeader) Reset() { h.disableNormalizing = false h.Trailer().disableNormalizing = false - h.noDefaultContentType = false - h.noDefaultDate = false h.ResetSkipNormalize() } +// CopyToAndMark copies all the headers to dst and mark the dst header as a copy. +func (h *ResponseHeader) CopyToAndMark(dst *ResponseHeader) { + dst.isCopy = true + h.CopyTo(dst) +} + // CopyTo copies all the headers to dst. func (h *ResponseHeader) CopyTo(dst *ResponseHeader) { dst.Reset() @@ -434,6 +442,9 @@ func (h *RequestHeader) AppendBytes(dst []byte) []byte { // // The returned representation is valid until the next call to RequestHeader methods. func (h *RequestHeader) Header() []byte { + if h.isCopy { + return h.AppendBytes(nil) + } h.bufKV.value = h.AppendBytes(h.bufKV.value[:0]) return h.bufKV.value } @@ -497,6 +508,9 @@ func checkWriteHeaderCode(code int) { func (h *ResponseHeader) ResetSkipNormalize() { h.protocol = "" + h.isCopy = false + h.noDefaultContentType = false + h.noDefaultDate = false h.connectionClose = false h.statusCode = 0 @@ -630,6 +644,9 @@ func (h *ResponseHeader) DelBytes(key []byte) { // // The returned value is valid until the next call to ResponseHeader methods. func (h *ResponseHeader) Header() []byte { + if h.isCopy { + return h.AppendBytes(nil) + } h.bufKV.value = h.AppendBytes(h.bufKV.value[:0]) return h.bufKV.value } @@ -683,6 +700,9 @@ func (h *ResponseHeader) DelClientCookieBytes(key []byte) { // Returned value is valid until the next call to ResponseHeader. // Do not store references to returned value. Make copies instead. func (h *ResponseHeader) Peek(key string) []byte { + if h.isCopy { + return h.peek(getHeaderKeyBytes(&argsKV{}, key, h.disableNormalizing)) + } k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing) return h.peek(k) } @@ -729,6 +749,9 @@ func (h *ResponseHeader) peek(key []byte) []byte { // Any future calls to the Peek* will modify the returned value. // Do not store references to returned value. Use ResponseHeader.GetAll(key) instead. func (h *ResponseHeader) PeekAll(key string) [][]byte { + if h.isCopy { + return h.peekAll(getHeaderKeyBytes(&argsKV{}, key, h.disableNormalizing)) + } k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing) return h.peekAll(k) } @@ -771,6 +794,9 @@ func (h *ResponseHeader) peekAll(key []byte) [][]byte { // Any future calls to the Peek* will modify the returned value. // Do not store references to returned value. Use RequestHeader.GetAll(key) instead. func (h *RequestHeader) PeekAll(key string) [][]byte { + if h.isCopy { + return h.peekAll(getHeaderKeyBytes(&argsKV{}, key, h.disableNormalizing)) + } k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing) return h.peekAll(k) } @@ -1115,10 +1141,19 @@ func (h *RequestHeader) CopyTo(dst *RequestHeader) { // Returned value is valid until the next call to RequestHeader. // Do not store references to returned value. Make copies instead. func (h *RequestHeader) Peek(key string) []byte { + if h.isCopy { + return h.peek(getHeaderKeyBytes(&argsKV{}, key, h.disableNormalizing)) + } k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing) return h.peek(k) } +// CopyToAndMark copies all the headers to dst and mark the dst header as a copy. +func (h *RequestHeader) CopyToAndMark(dst *RequestHeader) { + dst.isCopy = true + h.CopyTo(dst) +} + // SetMultipartFormBoundary sets the following Content-Type: // 'multipart/form-data; boundary=...' // where ... is substituted by the given boundary. @@ -1412,6 +1447,7 @@ func (h *RequestHeader) SetCanonical(key, value []byte) { } func (h *RequestHeader) ResetSkipNormalize() { + h.isCopy = false h.connectionClose = false h.protocol = "" h.noDefaultContentType = false @@ -1528,8 +1564,12 @@ func (h *RequestHeader) VisitAll(f func(key, value []byte)) { h.collectCookies() if len(h.cookies) > 0 { - h.bufKV.value = appendRequestCookieBytes(h.bufKV.value[:0], h.cookies) - f(bytestr.StrCookie, h.bufKV.value) + if h.isCopy { + f(bytestr.StrCookie, appendRequestCookieBytes(nil, h.cookies)) + } else { + h.bufKV.value = appendRequestCookieBytes(h.bufKV.value[:0], h.cookies) + f(bytestr.StrCookie, h.bufKV.value) + } } visitArgs(h.h, f) if h.ConnectionClose() { diff --git a/pkg/protocol/request.go b/pkg/protocol/request.go index acbcd9c..c45f8ca 100644 --- a/pkg/protocol/request.go +++ b/pkg/protocol/request.go @@ -88,6 +88,12 @@ func (noBody) Close() error { return nil } type Request struct { noCopy nocopy.NoCopy //lint:ignore U1000 until noCopy is used + // isCopy shows that whether it is a copy through ctx.Copy(). + // Other APIs such as CopyTo do not need to handle this. + isCopy bool + // readLock is used to protect lazy load read APIs + readLock sync.Mutex + Header RequestHeader uri URI @@ -189,6 +195,7 @@ func (req *Request) ResetSkipHeader() { req.uri.Reset() req.parsedURI = false req.parsedPostArgs = false + req.isCopy = false req.postArgs.Reset() req.isTLS = false } @@ -227,6 +234,11 @@ func (req *Request) PostArgString() []byte { // RemoveMultipartFormFiles must be called after returned multipart form // is processed. func (req *Request) MultipartForm() (*multipart.Form, error) { + if req.isCopy { + // use lock to prevent concurrent parse. + req.readLock.Lock() + defer req.readLock.Unlock() + } if req.multipartForm != nil { return req.multipartForm, nil } @@ -372,6 +384,40 @@ func (req *Request) CopyTo(dst *Request) { } } +// CopyToAndMark copies req contents to dst except of body stream and mark the dst req as a copy. +func (req *Request) CopyToAndMark(dst *Request) { + // Same with req.CopyTo(dst), but use the .CopyToAndMark instead of .CopyTo + dst.Reset() + + dst.isCopy = true + + req.Header.CopyToAndMark(&dst.Header) + + req.uri.CopyToAndMark(&dst.uri) + dst.parsedURI = req.parsedURI + + req.postArgs.CopyToAndMark(&dst.postArgs) + dst.parsedPostArgs = req.parsedPostArgs + dst.isTLS = req.isTLS + + if req.options != nil { + dst.options = &config.RequestOptions{} + req.options.CopyTo(dst.options) + } + + // copy body + if req.bodyRaw != nil { + dst.bodyRaw = append(dst.bodyRaw[:0], req.bodyRaw...) + if dst.body != nil { + dst.body.Reset() + } + } else if req.body != nil { + dst.BodyBuffer().Set(req.body.B) + } else if dst.body != nil { + dst.body.Reset() + } +} + func (req *Request) CopyToSkipBody(dst *Request) { dst.Reset() req.Header.CopyTo(&dst.Header) @@ -685,6 +731,11 @@ func (req *Request) BodyWriter() io.Writer { // PostArgs returns POST arguments. func (req *Request) PostArgs() *Args { + if req.isCopy { + // use lock to prevent concurrent parse. + req.readLock.Lock() + defer req.readLock.Unlock() + } req.parsePostArgs() return &req.postArgs } @@ -764,6 +815,11 @@ func (req *Request) CloseBodyStream() error { // URI returns request URI func (req *Request) URI() *URI { + if req.isCopy { + // use lock to prevent concurrent parse. + req.readLock.Lock() + defer req.readLock.Unlock() + } req.ParseURI() return &req.uri } diff --git a/pkg/protocol/response.go b/pkg/protocol/response.go index 1d4764f..e779a1d 100644 --- a/pkg/protocol/response.go +++ b/pkg/protocol/response.go @@ -64,6 +64,9 @@ var responsePool sync.Pool type Response struct { noCopy nocopy.NoCopy //lint:ignore U1000 until noCopy is used + // isCopy shows that whether it is a copy through ctx.Copy(). + // Other APIs such as CopyTo do not need to handle this. + isCopy bool // Response header // // Copying Header by value is forbidden. Use pointer to Header instead. @@ -249,6 +252,31 @@ func (resp *Response) CopyTo(dst *Response) { } else if dst.body != nil { dst.body.Reset() } + resp.isCopy = false +} + +// CopyToAndMark copies req contents to dst except of body stream and mark the dst resp as a copy. +func (resp *Response) CopyToAndMark(dst *Response) { + dst.isCopy = true + + // Same with resp.CopyTo(dst), but use the .CopyToAndMark instead of .CopyTo + dst.Reset() + resp.Header.CopyToAndMark(&dst.Header) + dst.SkipBody = resp.SkipBody + dst.raddr = resp.raddr + dst.laddr = resp.laddr + + // copy body + if resp.bodyRaw != nil { + dst.bodyRaw = append(dst.bodyRaw[:0], resp.bodyRaw...) + if dst.body != nil { + dst.body.Reset() + } + } else if resp.body != nil { + dst.BodyBuffer().Set(resp.body.B) + } else if dst.body != nil { + dst.body.Reset() + } } func SwapResponseBody(a, b *Response) { diff --git a/pkg/protocol/uri.go b/pkg/protocol/uri.go index 699d2bf..384b4cd 100644 --- a/pkg/protocol/uri.go +++ b/pkg/protocol/uri.go @@ -77,6 +77,12 @@ var uriPool = &sync.Pool{ type URI struct { noCopy nocopy.NoCopy //lint:ignore U1000 until noCopy is used + // isCopy shows that whether it is a copy through ctx.Copy(). + // Other APIs such as CopyTo do not need to handle this. + isCopy bool + // readLock is used to protect lazy load read APIs + readLock sync.Mutex + pathOriginal []byte scheme []byte path []byte @@ -110,6 +116,12 @@ func (kv *argsKV) GetValue() []byte { return kv.value } +// CopyToAndMark copies uri contents to dst and mark the dst uri as a copy. +func (u *URI) CopyToAndMark(dst *URI) { + dst.isCopy = true + u.CopyTo(dst) +} + // CopyTo copies uri contents to dst. func (u *URI) CopyTo(dst *URI) { dst.Reset() @@ -132,6 +144,11 @@ func (u *URI) CopyTo(dst *URI) { // QueryArgs returns query args. func (u *URI) QueryArgs() *Args { + if u.isCopy { + // use lock to prevent concurrent parse. + u.readLock.Lock() + defer u.readLock.Unlock() + } u.parseQueryArgs() return &u.queryArgs } @@ -283,7 +300,7 @@ func (u *URI) Reset() { u.hash = u.hash[:0] u.username = u.username[:0] u.password = u.password[:0] - + u.isCopy = false u.host = u.host[:0] u.queryArgs.Reset() u.parsedQueryArgs = false @@ -641,10 +658,14 @@ func (u *URI) AppendBytes(dst []byte) []byte { // RequestURI returns RequestURI - i.e. URI without Scheme and Host. func (u *URI) RequestURI() []byte { var dst []byte + buf := u.requestURI[:0] + if u.isCopy { + buf = nil + } if u.DisablePathNormalizing { - dst = append(u.requestURI[:0], u.PathOriginal()...) + dst = append(buf, u.PathOriginal()...) } else { - dst = bytesconv.AppendQuotedPath(u.requestURI[:0], u.Path()) + dst = bytesconv.AppendQuotedPath(buf, u.Path()) } if u.queryArgs.Len() > 0 { dst = append(dst, '?') @@ -653,6 +674,9 @@ func (u *URI) RequestURI() []byte { dst = append(dst, '?') dst = append(dst, u.queryString...) } + if u.isCopy { + return dst + } u.requestURI = dst return u.requestURI } @@ -665,6 +689,9 @@ func (u *URI) appendSchemeHost(dst []byte) []byte { // FullURI returns full uri in the form {Scheme}://{Host}{RequestURI}#{Hash}. func (u *URI) FullURI() []byte { + if u.isCopy { + return u.AppendBytes(nil) + } u.fullURI = u.AppendBytes(u.fullURI[:0]) return u.fullURI } diff --git a/pkg/route/tree.go b/pkg/route/tree.go index c8898a3..28f49a9 100644 --- a/pkg/route/tree.go +++ b/pkg/route/tree.go @@ -48,6 +48,8 @@ import ( "strings" "unicode" + "github.com/oarkflow/log" + "github.com/oarkflow/frame" "github.com/oarkflow/frame/internal/bytesconv" @@ -206,11 +208,11 @@ func (r *router) insert(path string, h frame.HandlersChain, t kind, ppath string prefixLen := len(currentNode.prefix) lcpLen := 0 - max := prefixLen - if searchLen < max { - max = searchLen + max1 := prefixLen + if searchLen < max1 { + max1 = searchLen } - for ; lcpLen < max && search[lcpLen] == currentNode.prefix[lcpLen]; lcpLen++ { + for ; lcpLen < max1 && search[lcpLen] == currentNode.prefix[lcpLen]; lcpLen++ { } if lcpLen == 0 { @@ -298,7 +300,8 @@ func (r *router) insert(path string, h frame.HandlersChain, t kind, ppath string } else { // Node already exists if currentNode.handlers != nil && h != nil { - panic("handlers are already registered for path '" + ppath + "'") + log.Warn().Str("handler_path", ppath).Msg("handlers are already registered for path '" + ppath + "'") + // panic("handlers are already registered for path '" + ppath + "'") } if h != nil { @@ -313,7 +316,7 @@ func (r *router) insert(path string, h frame.HandlersChain, t kind, ppath string } } -// find finds registered handler by method and path, parses URL params and puts params to context +// find registered handler by method and path, parses URL params and puts params to context func (r *router) find(path string, paramsPointer *param.Params, unescape bool) (res nodeValue) { var ( cn = r.root // current node @@ -348,7 +351,7 @@ func (r *router) find(path string, paramsPointer *param.Params, unescape bool) ( // for param/any node.prefix value is always `:` so we can not deduce searchIndex from that and must use pValue // for that index as it would also contain part of path we cut off before moving into node we are backtracking from searchIndex -= len((*paramsPointer)[paramIndex].Value) - (*paramsPointer) = (*paramsPointer)[:paramIndex] + *paramsPointer = (*paramsPointer)[:paramIndex] } search = path[searchIndex:] return @@ -410,7 +413,7 @@ func (r *router) find(path string, paramsPointer *param.Params, unescape bool) ( if i == -1 { i = len(search) } - (*paramsPointer) = (*paramsPointer)[:(paramIndex + 1)] + *paramsPointer = (*paramsPointer)[:(paramIndex + 1)] val := search[:i] if unescape { if v, err := url.QueryUnescape(search[:i]); err == nil { @@ -433,7 +436,7 @@ func (r *router) find(path string, paramsPointer *param.Params, unescape bool) ( if child := cn.anyChild; child != nil { // If any node is found, use remaining path for paramValues cn = child - (*paramsPointer) = (*paramsPointer)[:(paramIndex + 1)] + *paramsPointer = (*paramsPointer)[:(paramIndex + 1)] index := len(cn.pnames) - 1 val := search if unescape {