Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

instance.gno

4.76 Kb · 187 lines
  1package vmkit
  2
  3import (
  4	"errors"
  5
  6	"gno.land/p/nt/avl/v0"
  7)
  8
  9// ErrNoInstance is returned when an id names nothing.
 10var ErrNoInstance = errors.New("vmkit: no such instance")
 11
 12// ErrBudgetExhausted is returned when an instance has already spent its total
 13// fuel budget, so there is nothing left to step.
 14var ErrBudgetExhausted = errors.New("vmkit: instance fuel budget exhausted")
 15
 16// Instance is one guest program as a realm stores it: the code, the machine
 17// state between slices, and the accounting that survives the transaction.
 18//
 19// It holds a snapshot rather than a [Machine], on purpose. A realm keeps
 20// bytes; the machine is rebuilt for the slice that needs it and thrown away
 21// after. That is what makes the storage cost of a paused program measurable,
 22// which is the whole kill criterion for continuations: if resuming costs more
 23// than re-running, continuations are theater.
 24type Instance struct {
 25	ID    string
 26	Owner address
 27	VM    string // which guest machine the Program is for
 28
 29	Program  []byte
 30	Snapshot []byte
 31
 32	Status Status
 33	Trap   string
 34
 35	// FuelUsed is the total across every slice run so far.
 36	FuelUsed int64
 37	// FuelBudget caps FuelUsed across the instance's whole life, or is
 38	// [Unmetered] for no cap.
 39	FuelBudget int64
 40
 41	// Slices counts how many transactions have stepped this instance.
 42	Slices int64
 43
 44	Output []byte
 45}
 46
 47// NewInstance returns an instance ready for its first slice.
 48func NewInstance(id string, owner address, vm string, program []byte, budget int64) *Instance {
 49	if budget < 0 {
 50		budget = Unmetered
 51	}
 52	prog := make([]byte, len(program))
 53	copy(prog, program)
 54	return &Instance{
 55		ID:         id,
 56		Owner:      owner,
 57		VM:         vm,
 58		Program:    prog,
 59		Status:     Running,
 60		FuelBudget: budget,
 61	}
 62}
 63
 64// Remaining reports the fuel left in the instance's total budget, or
 65// [Unmetered].
 66func (i *Instance) Remaining() int64 {
 67	if i.FuelBudget == Unmetered {
 68		return Unmetered
 69	}
 70	left := i.FuelBudget - i.FuelUsed
 71	if left < 0 {
 72		return 0
 73	}
 74	return left
 75}
 76
 77// Slice returns how much fuel the next call to [Instance.Run] may spend when
 78// the caller asks for `want`: `want` clamped to what the budget still allows.
 79func (i *Instance) Slice(want int64) int64 {
 80	left := i.Remaining()
 81	if left == Unmetered {
 82		return want
 83	}
 84	if want == Unmetered || want > left {
 85		return left
 86	}
 87	return want
 88}
 89
 90// Run steps m for one slice of at most `fuel` units and folds the result back
 91// into the instance: the new snapshot, the status, the fuel spent, and
 92// anything the guest wrote to h.
 93//
 94// m must be a fresh machine loaded from i.Program; Run restores i.Snapshot
 95// into it when there is one, so the caller never has to remember the order.
 96// The instance is left untouched when Restore fails, which means a snapshot
 97// that cannot be decoded costs the caller gas but never corrupts the stored
 98// program.
 99func (i *Instance) Run(m Machine, h Host, fuel int64) error {
100	if i.Status.Done() && i.Status != Running {
101		return ErrBudgetExhausted
102	}
103	if len(i.Snapshot) > 0 {
104		if err := m.Restore(i.Snapshot); err != nil {
105			return err
106		}
107	}
108	slice := i.Slice(fuel)
109	if slice == 0 {
110		i.Status = OutOfFuel
111		return ErrBudgetExhausted
112	}
113
114	used, status := m.Step(h, slice)
115
116	i.FuelUsed += used
117	i.Slices++
118	i.Snapshot = m.Snapshot()
119	i.Status = status
120	i.Trap = TrapReason(m)
121	if status == Running && i.Remaining() == 0 {
122		i.Status = OutOfFuel
123	}
124	return nil
125}
126
127// Store is the avl-backed set of instances a realm owns, keyed by id.
128type Store struct {
129	tree *avl.Tree
130}
131
132// NewStore returns an empty store.
133func NewStore() *Store { return &Store{tree: avl.NewTree()} }
134
135// Set writes an instance, replacing any instance with the same id.
136func (s *Store) Set(i *Instance) {
137	if i == nil {
138		return
139	}
140	s.tree.Set(i.ID, i)
141}
142
143// Get returns the instance with this id, or nil.
144func (s *Store) Get(id string) *Instance {
145	v := s.tree.Get(id)
146	if v == nil {
147		return nil
148	}
149	inst, ok := v.(*Instance)
150	if !ok {
151		return nil
152	}
153	return inst
154}
155
156// Remove deletes an instance and reports whether it existed.
157func (s *Store) Remove(id string) bool {
158	_, removed := s.tree.Remove(id)
159	return removed
160}
161
162// Size returns how many instances are stored.
163func (s *Store) Size() int { return s.tree.Size() }
164
165// Iterate walks every instance in key order, stopping early when fn returns
166// true.
167func (s *Store) Iterate(fn func(*Instance) bool) {
168	s.tree.Iterate("", "", func(_ string, v any) bool {
169		inst, ok := v.(*Instance)
170		if !ok {
171			return false
172		}
173		return fn(inst)
174	})
175}
176
177// ReverseIterate walks every instance in descending key order, which is how a
178// realm renders newest-first when ids are zero-padded and ascending.
179func (s *Store) ReverseIterate(fn func(*Instance) bool) {
180	s.tree.ReverseIterate("", "", func(_ string, v any) bool {
181		inst, ok := v.(*Instance)
182		if !ok {
183			return false
184		}
185		return fn(inst)
186	})
187}