diff options
| author | Sam Anthony <sam@samanthony.xyz> | 2026-03-14 10:13:55 -0400 |
|---|---|---|
| committer | Sam Anthony <sam@samanthony.xyz> | 2026-03-14 10:13:55 -0400 |
| commit | 7d622695ae19e518fded6aa5fbf001dae4652211 (patch) | |
| tree | cca62a7c6213c824adc0ad8447ff189cb106fcf0 /back | |
| parent | 3b8368d665c8818b84557f54681c5ebab35ba22e (diff) | |
| download | buth-7d622695ae19e518fded6aa5fbf001dae4652211.zip | |
back: add qver package to track qid versions
Diffstat (limited to 'back')
| -rw-r--r-- | back/qver/options.go | 32 | ||||
| -rw-r--r-- | back/qver/update.go | 29 | ||||
| -rw-r--r-- | back/qver/version.go | 178 | ||||
| -rw-r--r-- | back/qver/version_test.go | 206 |
4 files changed, 445 insertions, 0 deletions
diff --git a/back/qver/options.go b/back/qver/options.go new file mode 100644 index 0000000..41385ce --- /dev/null +++ b/back/qver/options.go @@ -0,0 +1,32 @@ +package qver + +type Option func(*options) + +type options struct { + parent *Version + UpdateFunc + state interface{} +} + +func parse(opts ...Option) options { + var o options + for _, opt := range opts { + opt(&o) + } + return o +} + +// Whenever the version changes, bump the parent's version as well. +func Parent(p *Version) Option { + return func(o *options) { + o.parent = p + } +} + +// Call f(state) to update the version whenever Version.Get() is called. +func Update(state interface{}, f UpdateFunc) Option { + return func(o *options) { + o.UpdateFunc = f + o.state = state + } +} diff --git a/back/qver/update.go b/back/qver/update.go new file mode 100644 index 0000000..afbe51c --- /dev/null +++ b/back/qver/update.go @@ -0,0 +1,29 @@ +package qver + +import ( + "os" + "time" +) + +// An UpdateFunc updates the version and state variables. It is called +// by Version.Get(). If there is an error, it should return the old +// version and state along with the error. +type UpdateFunc func(version uint32, state interface{}) (uint32, interface{}, error) + +// UpdateOnFileMod returns a state variable and an UpdateFunc that +// bumps the version when a file or directory is modified. The return +// values are to be passed to the Update() option. +func UpdateOnFileMod(path string) (state interface{}, f UpdateFunc) { + return time.Now(), func(ver uint32, state interface{}) (uint32, interface{}, error) { + mtime := state.(time.Time) + info, err := os.Stat(path) + if err != nil { + return ver, mtime, err + } + if info.ModTime().After(mtime) { + mtime = info.ModTime() + ver++ + } + return ver, mtime, err + } +} diff --git a/back/qver/version.go b/back/qver/version.go new file mode 100644 index 0000000..8c815cf --- /dev/null +++ b/back/qver/version.go @@ -0,0 +1,178 @@ +// Package qver keeps track of 9P qid versions. +package qver + +// Version is a 9P qid.version. +// +// A Version may have several child Versions, representing a directory +// hierarchy. The root version is incremented whenever any of the +// children's versions change. +// +// Version is safe to share between goroutines. +type Version struct { + req <-chan struct{} + res <-chan message + + bump chan<- struct{} + regChild chan<- childReg + kill chan<- struct{} +} + +type Bumper interface { + // Bump increments the version. + Bump() +} + +type version struct { + req chan<- struct{} + res chan<- message + + bump <-chan struct{} + regChild <-chan childReg + kill <-chan struct{} + + v uint32 + UpdateFunc + state interface{} + children []*child +} + +type childReg struct { + child *Version + done chan<- struct{} +} + +type child struct { + *Version + last uint32 +} + +type message struct { + ver uint32 + err error +} + +func New(opts ...Option) *Version { + o := parse(opts...) + + reqc := make(chan struct{}) + resc := make(chan message) + bumpc := make(chan struct{}) + childc := make(chan childReg) + killc := make(chan struct{}) + + go (&version{ + reqc, + resc, + bumpc, + childc, + killc, + 0, + o.UpdateFunc, + o.state, + nil, + }).run() + + v := &Version{reqc, resc, bumpc, childc, killc} + if o.parent != nil { + o.parent.register(v) + } + return v +} + +func (v *version) run() { + defer close(v.req) + defer close(v.res) + + for { + select { + case v.req <- struct{}{}: + err := v.update() + v.res <- message{v.v, err} + + case <-v.bump: + v.v++ + + case creg := <-v.regChild: + cver, _ := creg.child.Get() + v.children = append(v.children, &child{creg.child, cver}) + close(creg.done) + + case <-v.kill: + return + } + } +} + +func (v *version) update() error { + if v.UpdateFunc != nil { + var err error + v.v, v.state, err = v.UpdateFunc(v.v, v.state) + if err != nil { + return err + } + } + + var modified bool + for _, c := range v.children { + mod, err := c.update() + if err != nil { + return err + } + modified = modified || mod + } + if modified { + v.v++ + } + + return nil +} + +// Returns true if modified. +func (c *child) update() (bool, error) { + _, ok := <-c.req + if !ok { + // closed + return false, nil + } + + msg := <-c.res + if msg.err != nil { + return false, msg.err + } + if msg.ver != c.last { + // modified + c.last = msg.ver + return true, nil + } + + // not modified + return false, nil +} + +func (v *Version) register(child *Version) { + done := make(chan struct{}) + v.regChild <- childReg{child, done} + <-done +} + +func (v *Version) Close() { + v.kill <- struct{}{} + close(v.kill) + close(v.bump) + close(v.regChild) +} + +// Get returns the current version. +// +// If the Version was created with the Update option, Get calls the +// UpdateFunc and returns the new version. +func (v *Version) Get() (uint32, error) { + <-v.req + msg := <-v.res + return msg.ver, msg.err +} + +// Bump increments the version. +func (v *Version) Bump() { + v.bump <- struct{}{} +} diff --git a/back/qver/version_test.go b/back/qver/version_test.go new file mode 100644 index 0000000..4bc9eff --- /dev/null +++ b/back/qver/version_test.go @@ -0,0 +1,206 @@ +package qver_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "git.samanthony.xyz/buth/back/qver" +) + +// Bump() should increment the version. +func TestBump(t *testing.T) { + t.Parallel() + + ver := qver.New() + defer ver.Close() + var i uint32 + for i = 0; i < 5; i++ { + requireVer(t, i, ver) + ver.Bump() + } +} + +// Bumping the child version should bump the parent version too. +func TestBumpChild(t *testing.T) { + t.Parallel() + + parent := qver.New() + child := qver.New(qver.Parent(parent)) + defer parent.Close() + defer child.Close() + + var i uint32 + for i = 0; i < 5; i++ { + requireVer(t, i, child) + requireVer(t, i, parent) + child.Bump() + } +} + +// Bumping the parent version should not affect the child. +func TestBumpParent(t *testing.T) { + t.Parallel() + + parent := qver.New() + child := qver.New(qver.Parent(parent)) + defer parent.Close() + defer child.Close() + + requireVer(t, 0, parent) + requireVer(t, 0, child) + + for i := 0; i < 5; i++ { + parent.Bump() + } + requireVer(t, 5, parent) + requireVer(t, 0, child) +} + +// Bumps should propagate up the chain from parent to child. +func TestBumpChain(t *testing.T) { + t.Parallel() + + a := qver.New() + b := qver.New(qver.Parent(a)) + c := qver.New(qver.Parent(b)) + defer a.Close() + defer b.Close() + defer c.Close() + + var i uint32 + for i = 0; i < 5; i++ { + requireVer(t, i, a) + requireVer(t, i, b) + requireVer(t, i, c) + c.Bump() + } +} + +// Parent can have several children. +func TestMultiChild(t *testing.T) { + t.Parallel() + + root := qver.New() + a, b := qver.New(qver.Parent(root)), qver.New(qver.Parent(root)) + defer root.Close() + defer a.Close() + defer b.Close() + + var i uint32 + for i = 0; i < 3; i++ { + requireVer(t, i, root) + a.Bump() + } + + for i = 0; i < 3; i++ { + requireVer(t, i+3, root) + b.Bump() + } + + root.Bump() + requireVer(t, 3+3+1, root) +} + +func TestUpdate(t *testing.T) { + t.Parallel() + + state, update := newUpdateFunc() + ver := qver.New(qver.Update(state, update)) + defer ver.Close() + + requireVer(t, 0, ver) + state <- true + requireVer(t, 1, ver) + requireVer(t, 1, ver) + state <- false + requireVer(t, 1, ver) + state <- true + requireVer(t, 2, ver) +} + +// Updating the child should bump the parent's version. +func TestUpdateChild(t *testing.T) { + t.Parallel() + + parent := qver.New() + defer parent.Close() + + state, update := newUpdateFunc() + child := qver.New(qver.Parent(parent), qver.Update(state, update)) + defer close(state) + defer child.Close() + + requireVer(t, 0, parent) + requireVer(t, 0, child) + state <- true // update child + requireVer(t, 1, parent) + requireVer(t, 1, child) +} + +// Updating the parent should not affect the child. +func TestUpdateParent(t *testing.T) { + t.Parallel() + + state, update := newUpdateFunc() + parent := qver.New(qver.Update(state, update)) + child := qver.New(qver.Parent(parent)) + defer close(state) + defer parent.Close() + defer child.Close() + + requireVer(t, 0, parent) + requireVer(t, 0, child) + state <- true // update parent + requireVer(t, 1, parent) + requireVer(t, 0, child) +} + +// Parent should keep working after child is closed. +func TestCloseChild(t *testing.T) { + t.Parallel() + + a := qver.New() + requireVer(t, 0, a) + a.Bump() + requireVer(t, 1, a) + + b := qver.New(qver.Parent(a)) + b.Bump() + requireVer(t, 2, a) + b.Close() + requireVer(t, 2, a) + a.Bump() + requireVer(t, 3, a) + + a.Close() +} + +// Update when state buffer contains 'true'. +func newUpdateFunc() (state chan bool, f qver.UpdateFunc) { + return make(chan bool, 1), func(v uint32, state interface{}) (uint32, interface{}, error) { + s := state.(chan bool) + select { + case inc := <-s: + if inc { + v++ + } + default: + } + return v, s, nil + } +} + +func requireVer(t *testing.T, want uint32, v *qver.Version) { + t.Helper() + got, err := v.Get() + require.NoError(t, err) + require.Equal(t, want, got) +} + +func requireAllVer(t *testing.T, want uint32, vs ...*qver.Version) { + t.Helper() + for _, v := range vs { + requireVer(t, want, v) + } +} |