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:
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
}