Skip to content

Commit

Permalink
feat: wip on documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
tigerwill90 committed Nov 19, 2024
1 parent f1eb0bb commit 7d84ee2
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 41 deletions.
23 changes: 15 additions & 8 deletions fox.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@ func (fox *Router) Lookup(w ResponseWriter, r *http.Request) (route *Route, cc C
return nil, nil, tsr
}

// Iter returns a collection of range iterators for traversing registered routes. It creates a point-in-time snapshot
// of the routing tree. Therefore, all iterators returned by Iter will not observe subsequent write on the router.
// This function is safe for concurrent use by multiple goroutine and while mutation on routes are ongoing.
func (fox *Router) Iter() Iter {
rt := fox.getRoot()
return Iter{
Expand All @@ -319,11 +322,10 @@ func (fox *Router) Iter() Iter {
}

// Updates executes a function within the context of a read-write managed transaction. If no error is returned from the
// function then the transaction is committed. If an error is returned then the entire transaction is rolled back.
// Updates returns any error returned by the callback. It's safe to run a transaction while the tree is in use for
// serving requests. This function is safe for concurrent use by multiple goroutine, however [Txn] itself is not tread-safe.
// For unmanaged transaction, use [Router.Txn] method.
// This API is EXPERIMENTAL and may change in future releases.
// function then the transaction is committed. If an error is returned then the entire transaction is aborted.
// Updates returns any error returned by fn. This function is safe for concurrent use by multiple goroutine and while
// the router is serving request. However [Txn] itself is NOT tread-safe.
// See also [Router.Txn] for unmanaged transaction and [Router.View] for managed read-only transaction.
func (fox *Router) Updates(fn func(txn *Txn) error) error {
txn := fox.Txn(true)
defer func() {
Expand All @@ -340,6 +342,10 @@ func (fox *Router) Updates(fn func(txn *Txn) error) error {
return nil
}

// View executes a function within the context of a read-only managed transaction. View returns any error returned
// by fn. This function is safe for concurrent use by multiple goroutine and while mutation on routes are ongoing.
// However [Txn] itself is NOT tread-safe.
// See also [Router.Txn] for unmanaged transaction and [Router.Updates] for managed read-write transaction.
func (fox *Router) View(fn func(txn *Txn) error) error {
txn := fox.Txn(false)
defer func() {
Expand All @@ -352,9 +358,10 @@ func (fox *Router) View(fn func(txn *Txn) error) error {
return fn(txn)
}

// Txn create a new read-write transaction. Each Txn must be finalized with [Txn.Commit] or [Txn.Abort].
// It's safe to create and execute a transaction while the tree is in use for serving requests.
// This function is safe for concurrent use by multiple goroutine. For managed transaction, use [Router.Updates].
// Txn create a new read-write transaction. Each [Txn] must be finalized with [Txn.Commit] or [Txn.Abort].
// It's safe to create transaction from multiple goroutine and while the router is serving request.
// 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 {
if write {
fox.mu.Lock()
Expand Down
55 changes: 31 additions & 24 deletions tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ func (t *txn) commit() *iTree {
return nt
}

// clone capture a point-in-time clone of the transaction. The cloned transaction will contain
// any uncommited writes in the original transaction but further mutations to either will be independent and result
// in different tree on commit.
func (t *txn) clone() *txn {

Check failure on line 71 in tree.go

View workflow job for this annotation

GitHub Actions / Lint Fox (>=1.21)

func `(*txn).clone` is unused (unused)
// reset the writable node cache to avoid leaking future writes into the clone
t.writable = nil
Expand All @@ -77,6 +80,13 @@ func (t *txn) clone() *txn {
return tx
}

// snapshot capture a point-in-time snapshot of the root tree. Further mutation to txn
// will not be reflected on the snapshot.
func (t *txn) snapshot() root {
t.writable = nil
return t.root
}

// insert is not safe for concurrent use
func (t *txn) insert(method string, route *Route, paramsN uint32) error {
if t.writable == nil {
Expand Down Expand Up @@ -565,67 +575,64 @@ func (t *txn) truncate(methods []string) {
func (t *txn) updateToRoot(p, pp, ppp *node, visited []*node, n *node) {
last := n
if p != nil {
pc := p
if _, ok := t.writable.Get(pc); !ok {
pc = pc.clone()
t.writable.Add(pc, nil)
if _, ok := t.writable.Get(p); !ok {
p = p.clone()
t.writable.Add(p, nil)
// pc is root and has never been writen
if pp == nil {
pc.updateEdge(n)
t.updateRoot(pc)
p.updateEdge(n)
t.updateRoot(p)
return
}
}

// If it's a clone, it's not a root
pc.updateEdge(n)
p.updateEdge(n)
if pp == nil {
return
}
last = pc
last = p
}

if pp != nil {
ppc := pp
if _, ok := t.writable.Get(ppc); !ok {
ppc = ppc.clone()
t.writable.Add(ppc, nil)
if _, ok := t.writable.Get(pp); !ok {
pp = pp.clone()
t.writable.Add(pp, nil)
// ppc is root and has never been writen
if ppp == nil {
ppc.updateEdge(last)
t.updateRoot(ppc)
pp.updateEdge(last)
t.updateRoot(pp)
return
}
}

// If it's a clone, it's not a root
ppc.updateEdge(last)
pp.updateEdge(last)
if ppp == nil {
return
}
last = ppc
last = pp
}

pppc := ppp
if _, ok := t.writable.Get(pppc); !ok {
pppc = pppc.clone()
t.writable.Add(pppc, nil)
if _, ok := t.writable.Get(ppp); !ok {
ppp = ppp.clone()
t.writable.Add(ppp, nil)
// pppc is root and has never been writen
if len(visited) == 0 {
pppc.updateEdge(last)
t.updateRoot(pppc)
ppp.updateEdge(last)
t.updateRoot(ppp)
return
}
}

// If it's a clone, it's not a root
pppc.updateEdge(last)
ppp.updateEdge(last)
if len(visited) == 0 {
return
}

// Propagate update to the root node
current := pppc
current := ppp
for i := len(visited) - 1; i >= 0; i-- {
vNode := visited[i]

Expand Down
19 changes: 16 additions & 3 deletions txn.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (txn *Txn) Route(method, pattern string) *Route {
return nil
}

// Reverse perform a reverse lookup on the tree for the given method, host and path and return the matching registered [Route]
// Reverse perform a reverse lookup for the given method, host and path and return the matching registered [Route]
// (if any) along with a boolean indicating if the route was matched by adding or removing a trailing slash
// (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.
Expand Down Expand Up @@ -172,16 +172,26 @@ func (txn *Txn) Lookup(w ResponseWriter, r *http.Request) (route *Route, cc Cont
return nil, nil, tsr
}

// Iter returns a collection of iterators for traversing the routing tree.
// Iter returns a collection of range iterators for traversing registered routes. When called on a write transaction,
// Iter creates a point-in-time snapshot of the transaction state. Therefore, writing on the current transaction while
// 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 {
rt := txn.rootTxn.root
if txn.write {
rt = txn.rootTxn.snapshot()
}

return Iter{
tree: txn.rootTxn.tree,
root: txn.rootTxn.root,
root: rt,
maxDepth: txn.rootTxn.maxDepth,
}
}

// Commit finalize the transaction. This is a noop for read transactions, already aborted or
// committed transactions. This function is NOT thread-safe and should be run serially,
// along with all other [Txn] APIs.
func (txn *Txn) Commit() {
// Noop for a read transaction
if !txn.write {
Expand All @@ -201,6 +211,9 @@ func (txn *Txn) Commit() {
txn.fox.mu.Unlock()
}

// Abort cancel the transaction. This is a noop for read transactions, already aborted or
// committed transactions. This function is NOT thread-safe and should be run serially,
// along with all other [Txn] APIs.
func (txn *Txn) Abort() {
// Noop for a read transaction
if !txn.write {
Expand Down
37 changes: 31 additions & 6 deletions txn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,39 @@ func TestX(t *testing.T) {
require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ab", emptyHandler)))
require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ab/cd", emptyHandler)))
require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ab/cd/ef", emptyHandler)))
require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ab/cd/ef/gh", emptyHandler)))
require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ab/cd/ef/gh/ij", emptyHandler)))
require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ab/c", emptyHandler)))
require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ax/bba", emptyHandler)))
require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ax/cbb", emptyHandler)))
require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ax/", emptyHandler)))
for method, route := range txn.Iter().All() {
fmt.Println(method, route.Pattern())
txn.Handle(http.MethodGet, "/ab/cd/ef/gh", emptyHandler)
txn.Handle(http.MethodGet, "/ax", emptyHandler)
}
/* require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ab/cd/ef/gh", emptyHandler)))
require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ab/cd/ef/gh/ij", emptyHandler)))
require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ab/c", emptyHandler)))
require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ax/bba", emptyHandler)))
require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ax/cbb", emptyHandler)))
require.NoError(t, onlyError(txn.Handle(http.MethodGet, "/ax/", emptyHandler)))*/
return nil
})
tree := f.getRoot()
fmt.Println(tree.root[0])
}

func TestB(t *testing.T) {
f := New()

iter := f.Iter()

f.Updates(func(txn *Txn) error {
txn.Handle(http.MethodGet, "/foo", emptyHandler)
return nil
})

for method, route := range iter.All() {
fmt.Println(method, route.Pattern())
}

iter = f.Iter()
for method, route := range iter.All() {
fmt.Println(method, route.Pattern())
}
}

0 comments on commit 7d84ee2

Please sign in to comment.