kanban

Kanban client in Gio
Log | Files | Refs | README | LICENSE

commit e2d3e44b4c3921543363e723a3fea156364af542
parent c4aae2bbcef03c8c638885a812f301997a8bc733
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date:   Wed, 17 Feb 2021 21:06:00 +0800

feat: lazy load storage implementation with write through cache

Diffstat:
Mcmd/kanban/main.go | 4++--
Mcmd/kanban/ui.go | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Astorage/lazy/lazy.go | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mstorage/mem/mem.go | 14+++++++-------
Mstorage/storage.go | 6+++---
Mstorage/storm/storm.go | 16++++++++--------
6 files changed, 185 insertions(+), 32 deletions(-)

diff --git a/cmd/kanban/main.go b/cmd/kanban/main.go @@ -6,7 +6,7 @@ import ( "os" "path/filepath" - "git.sr.ht/~jackmordaunt/kanban/storage/storm" + "git.sr.ht/~jackmordaunt/kanban/storage/lazy" "github.com/spf13/pflag" @@ -39,7 +39,7 @@ func main() { } db := filepath.Join(data, "kanban.db") fmt.Printf("%s\n", db) - return storm.Open(db) + return lazy.Open(db) } }() if err != nil { diff --git a/cmd/kanban/ui.go b/cmd/kanban/ui.go @@ -48,6 +48,11 @@ type UI struct { // Storage driver responsible for allocating Project objects. Storage storage.Storer + // Projects is an in-memory list of the projects. + // Refreshed from Storage before every frame. + // Save to Storage after every frame. + Projects Projects + // Project is the currently active kanban Project. // Contains the state and methods for kanban operations. // Points to memory allocated by the storage implementation. @@ -97,8 +102,10 @@ func (ui *UI) Loop() error { return event.Err case system.FrameEvent: gtx := layout.NewContext(&ops, event) + ui.Load() ui.Update(gtx) ui.Layout(gtx) + ui.Save() event.Frame(gtx.Ops) } } @@ -107,9 +114,9 @@ func (ui *UI) Loop() error { // Shutdown does cleanup. func (ui *UI) Shutdown() error { - if err := ui.Storage.Save(ui.Project); err != nil { - return fmt.Errorf("saving project: %v", err) - } + // if err := ui.Storage.Save(*ui.Project); err != nil { + // return fmt.Errorf("saving project: %v", err) + // } return nil } @@ -162,16 +169,17 @@ func (ui *UI) Update(gtx C) { ui.Clear() } if p, ok := ui.Rail.Selected(); ok { - if ui.Project != nil { - if err := ui.Storage.Save(ui.Project); err != nil { - log.Printf("saving project: %v", err) - } - } + // if ui.Project != nil { + // if err := ui.Storage.Save(ui.Project); err != nil { + // log.Printf("saving project: %v", err) + // } + // } if ui.Project == nil || ui.Project.Name != p { - project, ok, err := ui.Storage.Load(p) - if err != nil { - log.Printf("loading project %q: %v", p, err) - } + project, ok := ui.Projects.Find(p) + // project, ok, err := ui.Storage.Load(p) + // if err != nil { + // log.Printf("loading project %q: %v", p, err) + // } if ok { ui.Clear() ui.Project = project @@ -494,3 +502,36 @@ func (ui *UI) DeleteTicket(t kanban.Ticket) { }) } } + +// Projects is a list of Project entities with added behaviours. +type Projects []kanban.Project + +// Find and return project by name. +// Boolean indicates whether the project exists. +func (plist Projects) Find(name string) (*kanban.Project, bool) { + for _, p := range plist { + p := p + if p.Name == name { + return &p, true + } + } + return nil, false +} + +// Load entities from storage. +func (ui *UI) Load() { + projects, err := ui.Storage.List() + if err != nil { + log.Printf("error: loading projects: %v", err) + } + ui.Projects = projects +} + +// Save entities to storage. +func (ui *UI) Save() { + for _, p := range ui.Projects { + if err := ui.Storage.Save(p); err != nil { + log.Printf("error: saving project %q: %v", p.Name, err) + } + } +} diff --git a/storage/lazy/lazy.go b/storage/lazy/lazy.go @@ -0,0 +1,112 @@ +// Package lazy implements a lazy storage that only touches the +// disk when necessary. +package lazy + +import ( + "fmt" + "reflect" + + "git.sr.ht/~jackmordaunt/kanban" + "git.sr.ht/~jackmordaunt/kanban/storage" + "git.sr.ht/~jackmordaunt/kanban/storage/mem" + "git.sr.ht/~jackmordaunt/kanban/storage/storm" +) + +var _ storage.Storer = (*Storer)(nil) + +// Load/Save to an in-memory cache. +// Write through to disk when data has changed, and refresh the cache. +type Storer struct { + Cache *mem.Storer + Disk *storm.Storer +} + +// Open a lazy storer, initializing the underlying database at the path +// specified. +func Open(path string) (*Storer, error) { + disk, err := storm.Open(path) + if err != nil { + return nil, err + } + s := &Storer{ + Cache: mem.New(), + Disk: disk, + } + return s, s.Populate() +} + +// Create a project. Saves to disk. +func (s *Storer) Create(p kanban.Project) error { + if err := s.Cache.Create(p); err != nil { + return fmt.Errorf("creating on disk: %v", err) + } + if err := s.Disk.Create(p); err != nil { + return fmt.Errorf("creating on disk: %v", err) + } + return nil +} + +// Save a project. Only saves to disk if changed. +// +// @bug the old data loaded from cache is always up to date with the new +// data, why? +// We need to have an old one and a new one in order to detect changes. +func (s *Storer) Save(p kanban.Project) error { + old, ok, err := s.Cache.Load(p.Name) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("project does not exist: %q", p.Name) + } + fmt.Printf("old: %v, p: %v\n", old, p) + if !reflect.DeepEqual(p, old) { + fmt.Printf("saving project to disk: %v\n", p) + if err := s.Disk.Save(p); err != nil { + return fmt.Errorf("saving to disk: %v", err) + } + return s.Refresh(p.Name) + } + return nil +} + +// Load a project by name. +// Bool indicates whether a project exists for that name. +func (s *Storer) Load(name string) (kanban.Project, bool, error) { + return s.Cache.Load(name) +} + +// List projects. +func (s *Storer) List() ([]kanban.Project, error) { + return s.Cache.List() +} + +// Refresh a project entity by loading from disk. +func (s *Storer) Refresh(name string) error { + p, ok, err := s.Disk.Load(name) + if err != nil { + return fmt.Errorf("loading from disk: %v", err) + } + if !ok { + return fmt.Errorf("project does not exist: %v", name) + } + return s.Cache.Save(p) +} + +// Populate cache from disk. +func (s *Storer) Populate() error { + projects, err := s.Disk.List() + if err != nil { + return fmt.Errorf("loading projects from disk: %v", err) + } + for _, p := range projects { + if err := s.Cache.Create(p); err != nil { + return fmt.Errorf("saving project to cache: %v", err) + } + } + return nil +} + +func (s *Storer) Close() error { + return s.Disk.DB.Close() +} diff --git a/storage/mem/mem.go b/storage/mem/mem.go @@ -32,26 +32,26 @@ func (s *Storer) Create(p kanban.Project) error { return nil } -func (s *Storer) Save(p *kanban.Project) error { +func (s *Storer) Save(p kanban.Project) error { if _, ok := s.Data[p.Name]; ok { - s.Data[p.Name] = *p + s.Data[p.Name] = p } else { return fmt.Errorf("project %q does not exist", p.Name) } return nil } -func (s *Storer) Load(name string) (*kanban.Project, bool, error) { +func (s *Storer) Load(name string) (kanban.Project, bool, error) { if p, ok := s.Data[name]; ok { - return &p, ok, nil + return p, ok, nil } - return nil, false, nil + return kanban.Project{}, false, nil } -func (s *Storer) List() (list []*kanban.Project, err error) { +func (s *Storer) List() (list []kanban.Project, err error) { for _, name := range s.Order { if p, ok := s.Data[name]; ok { - list = append(list, &p) + list = append(list, p) } } return list, nil diff --git a/storage/storage.go b/storage/storage.go @@ -9,7 +9,7 @@ import ( // Storer persists Project entities. type Storer interface { Create(p kanban.Project) error - Save(p *kanban.Project) error - Load(name string) (*kanban.Project, bool, error) - List() ([]*kanban.Project, error) + Save(p kanban.Project) error + Load(name string) (kanban.Project, bool, error) + List() ([]kanban.Project, error) } diff --git a/storage/storm/storm.go b/storage/storm/storm.go @@ -44,23 +44,23 @@ func (s *Storer) Create(p kanban.Project) error { return s.DB.Save(&Schema{ID: p.Name, Project: p}) } -func (s *Storer) Save(p *kanban.Project) error { - return s.DB.Update(&Schema{ID: p.Name, Project: *p}) +func (s *Storer) Save(p kanban.Project) error { + return s.DB.Update(&Schema{ID: p.Name, Project: p}) } -func (s *Storer) Load(name string) (*kanban.Project, bool, error) { +func (s *Storer) Load(name string) (kanban.Project, bool, error) { var schema Schema if err := s.DB.One("ID", name, &schema); err != nil { if errors.Is(err, storm.ErrNotFound) { - return &schema.Project, false, nil + return schema.Project, false, nil } else { - return &schema.Project, false, err + return schema.Project, false, err } } - return &schema.Project, true, nil + return schema.Project, true, nil } -func (s *Storer) List() (list []*kanban.Project, err error) { +func (s *Storer) List() (list []kanban.Project, err error) { var ( projects []Schema ) @@ -69,7 +69,7 @@ func (s *Storer) List() (list []*kanban.Project, err error) { } for _, p := range projects { p := p - list = append(list, &p.Project) + list = append(list, p.Project) } return list, nil }