summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--back/qver/options.go32
-rw-r--r--back/qver/update.go29
-rw-r--r--back/qver/version.go178
-rw-r--r--back/qver/version_test.go206
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)
+ }
+}