Skip to content

Commit

Permalink
feat: do not cache one op transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
tigerwill90 committed Nov 20, 2024
1 parent 58290a3 commit 240951c
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 23 deletions.
7 changes: 7 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ type Context interface {
QueryParam(name string) string
// SetHeader sets the response header for the given key to the specified value.
SetHeader(key, value string)
// AddHeader add the response header for the given key to the specified value.
AddHeader(key, value string)
// Header retrieves the value of the request header for the given key.
Header(key string) string
// String sends a formatted string with the specified status code.
Expand Down Expand Up @@ -251,6 +253,11 @@ func (c *cTx) SetHeader(key, value string) {
c.w.Header().Set(key, value)
}

// AddHeader add the response header for the given key to the specified value.
func (c *cTx) AddHeader(key, value string) {
c.w.Header().Add(key, value)
}

// Header retrieves the value of the request header for the given key.
func (c *cTx) Header(key string) string {
return c.req.Header.Get(key)
Expand Down
2 changes: 2 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,10 @@ func TestContext_Header(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "https://example.com/foo", nil)
fox, c := NewTestContext(w, r)
c.SetHeader(HeaderServer, "go")
c.AddHeader("foo", "bar")
fox.ServeHTTP(w, r)
assert.Equal(t, "go", w.Header().Get(HeaderServer))
assert.Equal(t, "bar", w.Header().Get("foo"))
}

func TestContext_GetHeader(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var (
ErrInvalidRedirectCode = errors.New("invalid redirect code")
ErrNoClientIPStrategy = errors.New("no client ip strategy")
ErrReadOnlyTxn = errors.New("write on read-only transaction")
ErrSettledTxn = errors.New("transaction settled")
)

// RouteConflictError is a custom error type used to represent conflicts when
Expand Down
12 changes: 8 additions & 4 deletions fox.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func (fox *Router) ClientIPStrategyEnabled() bool {
// It's safe to add a new handler while the router is serving requests. This function is safe for concurrent use by
// multiple goroutine. To override an existing route, use [Router.Update].
func (fox *Router) Handle(method, pattern string, handler HandlerFunc, opts ...RouteOption) (*Route, error) {
txn := fox.Txn(true)
txn := fox.txnWith(true, false)
defer txn.Abort()
rte, err := txn.Handle(method, pattern, handler, opts...)
if err != nil {
Expand Down Expand Up @@ -214,7 +214,7 @@ func (fox *Router) MustHandle(method, pattern string, handler HandlerFunc, opts
// It's safe to update a handler while the router is serving requests. This function is safe for concurrent use by
// multiple goroutine. To add new handler, use [Router.Handle] method.
func (fox *Router) Update(method, pattern string, handler HandlerFunc, opts ...RouteOption) (*Route, error) {
txn := fox.Txn(true)
txn := fox.txnWith(true, false)
defer txn.Abort()
rte, err := txn.Update(method, pattern, handler, opts...)
if err != nil {
Expand All @@ -231,7 +231,7 @@ func (fox *Router) Update(method, pattern string, handler HandlerFunc, opts ...R
// It's safe to delete a handler while the router is serving requests. This function is safe for concurrent use by
// multiple goroutine.
func (fox *Router) Delete(method, pattern string) error {
txn := fox.Txn(true)
txn := fox.txnWith(true, false)
defer txn.Abort()
if err := txn.Delete(method, pattern); err != nil {
return err
Expand Down Expand Up @@ -364,11 +364,15 @@ func (fox *Router) View(fn func(txn *Txn) error) error {
// However, the returned [Txn] itself is NOT tread-safe.
// See also [Router.Updates] and [Router.View] for managed read-write and read-only transaction.
func (fox *Router) Txn(write bool) *Txn {
return fox.txnWith(write, true)
}

func (fox *Router) txnWith(write, cache bool) *Txn {
if write {
fox.mu.Lock()
}

rootTxn := fox.getRoot().txn()
rootTxn := fox.getRoot().txn(cache)
return &Txn{
fox: fox,
write: write,
Expand Down
15 changes: 8 additions & 7 deletions fox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -494,32 +494,33 @@ func BenchmarkStaticAll(b *testing.B) {
benchRoutes(b, r, staticRoutes)
}

// BenchmarkInsertStatic-16 3975 318519 ns/op 335701 B/op 4506 allocs/op
func BenchmarkInsertStatic(b *testing.B) {
f := New()

b.ResetTimer()
b.ReportAllocs()

for range b.N {
f := New()
for _, route := range staticRoutes {
f.Handle(route.method, route.path, emptyHandler)
}
}
}

func BenchmarkInsertStaticTx(b *testing.B) {
f := New()

b.ResetTimer()
b.ReportAllocs()

for range b.N {
f := New()
txn := f.Txn(true)
for _, route := range staticRoutes {
txn.Handle(route.method, route.path, emptyHandler)
}
txn.Commit()
}

}

func BenchmarkGithubParamsAll(b *testing.B) {
Expand Down Expand Up @@ -5478,7 +5479,7 @@ func TestFuzzInsertLookupParam(t *testing.T) {
}
path := fmt.Sprintf(routeFormat, s1, e1, s2, e2, e3)
tree := r.getRoot()
txn := tree.txn()
txn := tree.txn(true)
if err := txn.insert(http.MethodGet, &Route{pattern: path, hself: emptyHandler}, 3); err == nil {
c := newTestContext(r)
n, tsr := lookupByPath(tree, txn.root[0].children[0], fmt.Sprintf(reqFormat, s1, "xxxx", s2, "xxxx", "xxxx"), c, false)
Expand All @@ -5501,7 +5502,7 @@ func TestFuzzInsertNoPanics(t *testing.T) {
f.Fuzz(&routes)

tree := r.getRoot()
txn := tree.txn()
txn := tree.txn(true)

for rte := range routes {
if rte == "" {
Expand Down Expand Up @@ -5529,7 +5530,7 @@ func TestFuzzInsertLookupUpdateAndDelete(t *testing.T) {
f.Fuzz(&routes)

tree := r.getRoot()
txn := tree.txn()
txn := tree.txn(true)
for rte := range routes {
path := "/" + rte
err := txn.insert(http.MethodGet, &Route{pattern: path, hself: emptyHandler}, 0)
Expand All @@ -5542,7 +5543,7 @@ func TestFuzzInsertLookupUpdateAndDelete(t *testing.T) {
assert.Equal(t, len(routes), countPath)

tree = r.getRoot()
txn = tree.txn()
txn = tree.txn(true)
for rte := range routes {
c := newTestContext(r)
n, tsr := lookupByPath(tree, tree.root[0].children[0], "/"+rte, c, true)
Expand Down
2 changes: 1 addition & 1 deletion iter.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type rawIterator struct {
stack []stack
}

const stackSizeThreshold = 15
const stackSizeThreshold = 25

type stack struct {
edges []*node
Expand Down
26 changes: 18 additions & 8 deletions tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,23 @@ import (

const defaultModifiedCache = 4096

// iTree implements an immutable Radix Tree. The immutability means that it is safe to concurrently read from a Tree
// without any coordination.
// iTree implements an immutable Radix Tree. The immutability means that it is safe to
// concurrently read from a Tree without any coordination.
type iTree struct {
ctx sync.Pool
root roots
fox *Router
root roots
maxParams uint32
maxDepth uint32
}

func (t *iTree) txn() *tXn {
func (t *iTree) txn(cache bool) *tXn {
return &tXn{
tree: t,
root: t.root,
maxParams: t.maxParams,
maxDepth: t.maxDepth,
cache: cache,
}
}

Expand All @@ -47,6 +48,7 @@ type tXn struct {
root roots
maxParams uint32
maxDepth uint32
cache bool
}

func (t *tXn) commit() *iTree {
Expand Down Expand Up @@ -123,7 +125,9 @@ STOP:
if p != nil {
if _, ok := t.writable.Get(p); !ok {
cp := p.clone()
t.writable.Add(cp, nil)
if t.cache {
t.writable.Add(cp, nil)
}
if pp == nil {
t.updateRoot(cp)
} else {
Expand Down Expand Up @@ -288,7 +292,9 @@ func (t *tXn) insert(method string, route *Route, paramsN uint32) error {

if result.matched == rootNode {
n.key = method
t.writable.Add(n, nil)
if t.cache {
t.writable.Add(n, nil)
}
t.updateRoot(n)
break
}
Expand Down Expand Up @@ -493,7 +499,9 @@ func (t *tXn) remove(method, path string) bool {
return t.removeRoot(method)
}
parent.key = method
t.writable.Add(parent, nil)
if t.cache {
t.writable.Add(parent, nil)
}
t.updateRoot(parent)
return true
}
Expand Down Expand Up @@ -527,7 +535,9 @@ func (t *tXn) remove(method, path string) bool {
return t.removeRoot(method)
}
parent.key = method
t.writable.Add(parent, nil)
if t.cache {
t.writable.Add(parent, nil)
}
t.updateRoot(parent)
return true
}
Expand Down
51 changes: 51 additions & 0 deletions txn.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"
)

// Txn is a read or write transaction on the routing tree.
type Txn struct {
fox *Router
rootTxn *tXn
Expand All @@ -20,6 +21,10 @@ type Txn struct {
// This function is NOT thread-safe and should be run serially, along with all other [Txn] APIs.
// To override an existing route, use [Txn.Update].
func (txn *Txn) Handle(method, pattern string, handler HandlerFunc, opts ...RouteOption) (*Route, error) {
if txn.rootTxn == nil {
panic(ErrSettledTxn)
}

if !txn.write {
return nil, ErrReadOnlyTxn
}
Expand Down Expand Up @@ -50,6 +55,10 @@ func (txn *Txn) Handle(method, pattern string, handler HandlerFunc, opts ...Rout
// This function is NOT thread-safe and should be run serially, along with all other [Txn] APIs.
// To add a new handler, use [Txn.Handle].
func (txn *Txn) Update(method, pattern string, handler HandlerFunc, opts ...RouteOption) (*Route, error) {
if txn.rootTxn == nil {
panic(ErrSettledTxn)
}

if !txn.write {
return nil, ErrReadOnlyTxn
}
Expand Down Expand Up @@ -79,6 +88,10 @@ func (txn *Txn) Update(method, pattern string, handler HandlerFunc, opts ...Rout
//
// This function is NOT thread-safe and should be run serially, along with all other [Txn] APIs.
func (txn *Txn) Delete(method, pattern string) error {
if txn.rootTxn == nil {
panic(ErrSettledTxn)
}

if !txn.write {
return ErrReadOnlyTxn
}
Expand All @@ -100,6 +113,10 @@ func (txn *Txn) Delete(method, pattern string) error {
}

func (txn *Txn) Truncate(methods ...string) error {
if txn.rootTxn == nil {
panic(ErrSettledTxn)
}

if !txn.write {
return ErrReadOnlyTxn
}
Expand All @@ -110,13 +127,21 @@ func (txn *Txn) Truncate(methods ...string) error {
// Has allows to check if the given method and route pattern exactly match a registered route. This function is NOT
// thread-safe and should be run serially, along with all other [Txn] APIs. See also [Txn.Route] as an alternative.
func (txn *Txn) Has(method, pattern string) bool {
if txn.rootTxn == nil {
panic(ErrSettledTxn)
}

return txn.Route(method, pattern) != nil
}

// Route performs a lookup for a registered route matching the given method and route pattern. It returns the [Route] if a
// match is found or nil otherwise. This function is NOT thread-safe and should be run serially, along with all
// other [Txn] APIs. See also [Tree.Has] as an alternative.
func (txn *Txn) Route(method, pattern string) *Route {
if txn.rootTxn == nil {
panic(ErrSettledTxn)
}

tree := txn.rootTxn.tree
c := tree.ctx.Get().(*cTx)
c.resetNil()
Expand All @@ -135,6 +160,10 @@ func (txn *Txn) Route(method, pattern string) *Route {
// (trailing slash action recommended). This function is NOT thread-safe and should be run serially, along with all
// other [Txn] APIs. See also [Txn.Lookup] as an alternative.
func (txn *Txn) Reverse(method, host, path string) (route *Route, tsr bool) {
if txn.rootTxn == nil {
panic(ErrSettledTxn)
}

tree := txn.rootTxn.tree
c := tree.ctx.Get().(*cTx)
c.resetNil()
Expand All @@ -152,6 +181,10 @@ func (txn *Txn) Reverse(method, host, path string) (route *Route, tsr bool) {
// [Route] and a [ContextCloser]. The [ContextCloser] should always be closed if non-nil. This function is NOT
// thread-safe and should be run serially, along with all other [Txn] APIs. See also [Txn.Reverse] as an alternative.
func (txn *Txn) Lookup(w ResponseWriter, r *http.Request) (route *Route, cc ContextCloser, tsr bool) {
if txn.rootTxn == nil {
panic(ErrSettledTxn)
}

tree := txn.rootTxn.tree
c := tree.ctx.Get().(*cTx)
c.resetWithWriter(w, r)
Expand All @@ -177,6 +210,10 @@ func (txn *Txn) Lookup(w ResponseWriter, r *http.Request) (route *Route, cc Cont
// iterating is allowed, but the mutation will not be observed in the result returned by iterators collection.
// This function is NOT thread-safe and should be run serially, along with all other [Txn] APIs.
func (txn *Txn) Iter() Iter {
if txn.rootTxn == nil {
panic(ErrSettledTxn)
}

rt := txn.rootTxn.root
if txn.write {
rt = txn.rootTxn.snapshot()
Expand Down Expand Up @@ -229,3 +266,17 @@ func (txn *Txn) Abort() {
txn.rootTxn = nil
txn.fox.mu.Unlock()
}

// Snapshot returns a point in time snapshot of the current state of the transaction.
// Returns a new read-only transaction or nil if the transaction is already aborted
// or commited.
func (txn *Txn) Snapshot() *Txn {
if txn.rootTxn == nil {
return nil
}

return &Txn{
fox: txn.fox,
rootTxn: txn.rootTxn.clone(),
}
}
Loading

0 comments on commit 240951c

Please sign in to comment.