commit e9267c22cf90e505139335e2f6a7b08aa32d6ee2
parent b922c3fb94942c4d980548bd44ad813ecd8dcad4
Author: lash <dev@holbrook.no>
Date: Thu, 20 Apr 2023 16:55:11 +0100
Recover non-root state on engine persist
Diffstat:
11 files changed, 166 insertions(+), 28 deletions(-)
diff --git a/cache/cache.go b/cache/cache.go
@@ -44,11 +44,12 @@ func(ca *Cache) Add(key string, value string, sizeLimit uint16) error {
}
checkFrame := ca.frameOf(key)
if checkFrame > -1 {
- if checkFrame == len(ca.Cache) - 1 {
+ thisFrame := len(ca.Cache) - 1
+ if checkFrame == thisFrame {
Logg.Debugf("Ignoring load request on frame that has symbol already loaded")
return nil
}
- return fmt.Errorf("key %v already defined in frame %v", key, checkFrame)
+ return fmt.Errorf("key %v already defined in frame %v, this is frame %v", key, checkFrame, thisFrame)
}
var sz uint32
if len(value) > 0 {
diff --git a/dev/interactive/main.go b/dev/interactive/main.go
@@ -13,16 +13,22 @@ func main() {
var dir string
var root string
var size uint
- //var sessionId string
+ var sessionId string
+ var persist bool
flag.StringVar(&dir, "d", ".", "resource dir to read from")
flag.UintVar(&size, "s", 0, "max size of output")
flag.StringVar(&root, "root", "root", "entry point symbol")
- //flag.StringVar(&sessionId, "session-id", "default", "session id")
+ flag.StringVar(&sessionId, "session-id", "default", "session id")
+ flag.BoolVar(&persist, "persist", true, "use state persistence")
flag.Parse()
fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, dir)
ctx := context.Background()
- en := engine.NewSizedEngine(dir, uint32(size))
+ en, err := engine.NewSizedEngine(dir, uint32(size), persist, &sessionId)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "engine create error: %v", err)
+ os.Exit(1)
+ }
cont, err := en.Init(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "engine init exited with error: %v\n", err)
@@ -37,7 +43,7 @@ func main() {
os.Stdout.Write([]byte{0x0a})
os.Exit(0)
}
- err = engine.Loop(&en, os.Stdin, os.Stdout, ctx)
+ err = engine.Loop(en, os.Stdin, os.Stdout, ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err)
os.Exit(1)
diff --git a/engine/default.go b/engine/default.go
@@ -2,26 +2,59 @@ package engine
import (
"context"
+ "fmt"
+ "os"
+ "path"
"git.defalsify.org/vise.git/cache"
+ "git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/state"
)
// NewDefaultEngine is a convenience function to instantiate a filesystem-backed engine with no output constraints.
-func NewDefaultEngine(dir string) Engine {
+func NewDefaultEngine(dir string, persisted bool, session *string) (EngineIsh, error) {
+ var err error
st := state.NewState(0)
rs := resource.NewFsResource(dir)
ca := cache.NewCache()
cfg := Config{
Root: "root",
}
+ if session != nil {
+ cfg.SessionId = *session
+ } else if !persisted {
+ return nil, fmt.Errorf("session must be set if persist is used")
+ }
ctx := context.TODO()
- return NewEngine(cfg, &st, &rs, ca, ctx)
+ var en EngineIsh
+ if persisted {
+ dp := path.Join(dir, ".state")
+ err = os.MkdirAll(dp, 0700)
+ if err != nil {
+ return nil, err
+ }
+ pr := persist.NewFsPersister(dp)
+ en, err = NewPersistedEngine(cfg, pr, &rs, ctx)
+ if err != nil {
+ Logg.Infof("persisted engine create error. trying again with persisting empty state first...")
+ pr = pr.WithContent(&st, ca)
+ err = pr.Save(cfg.SessionId, nil)
+ if err != nil {
+ return nil, err
+ }
+ en, err = NewPersistedEngine(cfg, pr, &rs, ctx)
+ }
+ } else {
+ enb := NewEngine(cfg, &st, &rs, ca, ctx)
+ en = &enb
+ }
+ return en, err
}
// NewSizedEngine is a convenience function to instantiate a filesystem-backed engine with a specified output constraint.
-func NewSizedEngine(dir string, size uint32) Engine {
+func NewSizedEngine(dir string, size uint32, persisted bool, session *string) (EngineIsh, error) {
+ var err error
st := state.NewState(0)
rs := resource.NewFsResource(dir)
ca := cache.NewCache()
@@ -29,6 +62,33 @@ func NewSizedEngine(dir string, size uint32) Engine {
OutputSize: size,
Root: "root",
}
+ if session != nil {
+ cfg.SessionId = *session
+ } else if !persisted {
+ return nil, fmt.Errorf("session must be set if persist is used")
+ }
ctx := context.TODO()
- return NewEngine(cfg, &st, &rs, ca, ctx)
+ var en EngineIsh
+ if persisted {
+ dp := path.Join(dir, ".state")
+ err = os.MkdirAll(dp, 0700)
+ if err != nil {
+ return nil, err
+ }
+ pr := persist.NewFsPersister(dp)
+ en, err = NewPersistedEngine(cfg, pr, &rs, ctx)
+ if err != nil {
+ Logg.Infof("persisted engine create error. trying again with persisting empty state first...")
+ pr = pr.WithContent(&st, ca)
+ err = pr.Save(cfg.SessionId, nil)
+ if err != nil {
+ return nil, err
+ }
+ en, err = NewPersistedEngine(cfg, pr, &rs, ctx)
+ }
+ } else {
+ enb := NewEngine(cfg, &st, &rs, ca, ctx)
+ en = &enb
+ }
+ return en, err
}
diff --git a/engine/engine.go b/engine/engine.go
@@ -12,6 +12,13 @@ import (
"git.defalsify.org/vise.git/vm"
)
+type EngineIsh interface {
+ Init(ctx context.Context) (bool, error)
+ Exec(input []byte, ctx context.Context) (bool, error)
+ WriteResult(w io.Writer, ctx context.Context) (int, error)
+ Finish() error
+}
+
// Config globally defines behavior of all components driven by the engine.
type Config struct {
OutputSize uint32 // Maximum size of output from a single rendered page
@@ -51,10 +58,26 @@ func NewEngine(cfg Config, st *state.State, rs resource.Resource, ca cache.Memor
return engine
}
+// Finish implements EngineIsh interface
+func(en *Engine) Finish() error {
+ return nil
+}
+
+func(en *Engine) restore() {
+ location, _ := en.st.Where()
+ if len(location) == 0 {
+ return
+ }
+ if en.root != location {
+ en.root = "." //location
+ }
+}
+
// Init must be explicitly called before using the Engine instance.
//
// It loads and executes code for the start node.
func(en *Engine) Init(ctx context.Context) (bool, error) {
+ en.restore()
if en.initd {
Logg.DebugCtxf(ctx, "already initialized")
return true, nil
@@ -111,7 +134,10 @@ func (en *Engine) Exec(input []byte, ctx context.Context) (bool, error) {
if err != nil {
return false, err
}
+ return en.exec(input, ctx)
+}
+func(en *Engine) exec(input []byte, ctx context.Context) (bool, error) {
Logg.InfoCtxf(ctx, "new VM execution with input", "input", string(input))
code, err := en.st.GetCode()
if err != nil {
diff --git a/engine/loop.go b/engine/loop.go
@@ -17,7 +17,8 @@ import (
// Any error not handled by the engine will terminate the oop and return an error.
//
// Rendered output is written to the provided writer.
-func Loop(en *Engine, reader io.Reader, writer io.Writer, ctx context.Context) error {
+func Loop(en EngineIsh, reader io.Reader, writer io.Writer, ctx context.Context) error {
+ defer en.Finish()
var err error
_, err = en.WriteResult(writer, ctx)
if err != nil {
diff --git a/engine/persist.go b/engine/persist.go
@@ -8,12 +8,14 @@ import (
"git.defalsify.org/vise.git/resource"
)
+// PersistedEngine adds persisted state to the Engine object. It provides a persisted state option for synchronous/interactive clients.
type PersistedEngine struct {
*Engine
pr persist.Persister
}
+// NewPersistedEngine creates a new PersistedEngine
func NewPersistedEngine(cfg Config, pr persist.Persister, rs resource.Resource, ctx context.Context) (PersistedEngine, error) {
err := pr.Load(cfg.SessionId)
if err != nil {
@@ -21,23 +23,32 @@ func NewPersistedEngine(cfg Config, pr persist.Persister, rs resource.Resource,
}
st := pr.GetState()
ca := pr.GetMemory()
+
enb := NewEngine(cfg, st, rs, ca, ctx)
en := PersistedEngine{
&enb,
pr,
}
- return en, nil
+ return en, err
}
-func(pe *PersistedEngine) Exec(input []byte, ctx context.Context) (bool, error) {
+// Exec executes the parent method Engine.Exec, and afterwards persists the new state.
+func(pe PersistedEngine) Exec(input []byte, ctx context.Context) (bool, error) {
v, err := pe.Engine.Exec(input, ctx)
if err != nil {
return v, err
}
- err = pe.pr.Save(pe.Engine.session)
+ renderer := pe.Engine.vm.Renderer()
+ err = pe.pr.Save(pe.Engine.session, renderer)
return v, err
}
+// Finish implements EngineIsh interface
+func(pe PersistedEngine) Finish() error {
+ renderer := pe.Engine.vm.Renderer()
+ return pe.pr.Save(pe.Engine.session, renderer)
+}
+
// RunPersisted performs a single vm execution from client input using a persisted state.
//
// State is first loaded from storage. The vm is initialized with the state and executed. The new state is then saved to storage.
@@ -60,7 +71,8 @@ func RunPersisted(cfg Config, rs resource.Resource, pr persist.Persister, input
if err != nil {
return err
}
- err = pr.Save(cfg.SessionId)
+ renderer := en.vm.Renderer()
+ err = pr.Save(cfg.SessionId, renderer)
if err != nil {
return err
}
@@ -76,5 +88,7 @@ func RunPersisted(cfg Config, rs resource.Resource, pr persist.Persister, input
if err != nil {
return err
}
- return pr.Save(cfg.SessionId)
+ en.Finish()
+ renderer = en.vm.Renderer()
+ return pr.Save(cfg.SessionId, renderer)
}
diff --git a/persist/fs.go b/persist/fs.go
@@ -7,6 +7,7 @@ import (
"github.com/fxamacker/cbor/v2"
"git.defalsify.org/vise.git/cache"
+ "git.defalsify.org/vise.git/render"
"git.defalsify.org/vise.git/state"
)
@@ -14,6 +15,7 @@ import (
type FsPersister struct {
State *state.State
Memory *cache.Cache
+ MapKeys []string
dir string
}
@@ -39,6 +41,12 @@ func(p *FsPersister) WithContent(st *state.State, ca *cache.Cache) *FsPersister
return p
}
+// WithRenderer extracts the mapped keys to add to serialization.
+func(p *FsPersister) WithRenderer(pg render.Renderer) *FsPersister {
+ p.MapKeys = pg.Keys()
+ return p
+}
+
// GetState implements the Persister interface.
func(p *FsPersister) GetState() *state.State {
return p.State
@@ -61,13 +69,16 @@ func(p *FsPersister) Deserialize(b []byte) error {
}
// GetState implements the Persister interface.
-func(p *FsPersister) Save(key string) error {
+func(p *FsPersister) Save(key string, renderer render.Renderer) error {
+ if renderer != nil {
+ p = p.WithRenderer(renderer)
+ }
b, err := p.Serialize()
if err != nil {
return err
}
fp := path.Join(p.dir, key)
- Logg.Debugf("saved state and cache", "key", key, "bytecode", p.State.Code)
+ Logg.Debugf("saved state and cache", "key", key, "bytecode", p.State.Code, "map", p.MapKeys)
return ioutil.WriteFile(fp, b, 0600)
}
@@ -79,6 +90,10 @@ func(p *FsPersister) Load(key string) error {
return err
}
err = p.Deserialize(b)
- Logg.Debugf("loaded state and cache", "key", key, "bytecode", p.State.Code)
+ Logg.Debugf("loaded state and cache", "key", key, "bytecode", p.State.Code, "map", p.MapKeys)
return err
}
+
+func(p *FsPersister) GetKeys() []string {
+ return p.MapKeys
+}
diff --git a/persist/persist.go b/persist/persist.go
@@ -2,6 +2,7 @@ package persist
import (
"git.defalsify.org/vise.git/cache"
+ "git.defalsify.org/vise.git/render"
"git.defalsify.org/vise.git/state"
)
@@ -9,9 +10,10 @@ import (
type Persister interface {
Serialize() ([]byte, error) // Output serializes representation of the state.
Deserialize(b []byte) error // Restore state from a serialized state.
- Save(key string) error // Serialize and commit the state representation to persisted storage.
+ Save(key string, renderer render.Renderer) error // Serialize and commit the state representation to persisted storage.
Load(key string) error // Load the state representation from persisted storage and Deserialize.
GetState() *state.State // Get the currently loaded State object.
GetMemory() cache.Memory // Get the currently loaded Cache object.
+ GetKeys() []string // Get all mapped keys for renderer.
}
diff --git a/render/page.go b/render/page.go
@@ -128,6 +128,15 @@ func(pg *Page) Sizes() (map[string]uint16, error) {
return sizes, nil
}
+// Keys returns all mapped symbols.
+func(pg *Page) Keys() []string {
+ var r []string
+ for k, _ := range pg.cacheMap {
+ r = append(r, k)
+ }
+ return r
+}
+
// RenderTemplate is an adapter to implement the builtin golang text template renderer as resource.RenderTemplate.
func(pg *Page) RenderTemplate(sym string, values map[string]string, idx uint16) (string, error) {
tpl, err := pg.resource.GetTemplate(sym)
diff --git a/render/render.go b/render/render.go
@@ -0,0 +1,6 @@
+package render
+
+type Renderer interface {
+ Keys() []string
+ Map(key string) error
+}
diff --git a/vm/runner.go b/vm/runner.go
@@ -34,6 +34,11 @@ func NewVm(st *state.State, rs resource.Resource, ca cache.Memory, sizer *render
return vmi
}
+// Renderer returns the current state of the renderer operated on by the vm.
+func(vmi *Vm) Renderer() render.Renderer {
+ return vmi.pg
+}
+
// Reset re-initializes sub-components for output rendering.
func(vmi *Vm) Reset() {
vmi.mn = render.NewMenu()
@@ -281,14 +286,7 @@ func(vm *Vm) RunMove(b []byte, ctx context.Context) ([]byte, error) {
if err != nil {
return b, err
}
- if sym == "_" {
- vm.st.Up()
- vm.ca.Pop()
- sym, _ = vm.st.Where()
- } else {
- vm.st.Down(sym)
- vm.ca.Push()
- }
+ sym, _, err = applyTarget([]byte(sym), vm.st, vm.ca, ctx)
code, err := vm.rs.GetCode(sym)
if err != nil {
return b, err