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

duebook source pure

Package duebook is a pure scheduling primitive for realms that need to authorize an action now and perform it later. ...

Overview

Package duebook is a pure scheduling primitive for realms that need to authorize an action now and perform it later. It is to *time* what feeledger is to *money*: it holds no coins, performs no effects, imports no chain APIs, and owns no package-level state. The importing realm owns a *Book, supplies the clock, and performs its own effects.

The one idea

A realm does not ask duebook to execute anything — Gno has no autonomous execution, so "scheduled" always means "someone sends a transaction later". What a realm actually needs is for that later transaction to be authorized exactly once. Claim is that step:

Example
1d, err := book.Claim(id, now)
2if err != nil {
3	panic(err) // not due, expired, cancelled, or already claimed
4}
5// ... the realm performs its own effect here, under its own authority

Claim checks due / not-expired / still-open and CONSUMES the deferral in the same call, before returning. The realm then acts. Replay is not guarded against, it is structurally impossible:

  • IDs are allocated monotonically from an internal counter and are NEVER reused, for the lifetime of the Book;
  • a successful Claim removes the deferral before returning;

so at most one Claim per ID can ever succeed, across all transactions, forever. A second Claim of the same ID returns ErrNotFound whether the first one happened in this transaction or a year ago.

Because the realm performs the effect itself, no closure, callback, or capability ever crosses a realm boundary. duebook cannot be handed code to run, so it cannot be tricked into running the wrong code.

Lifecycle

A deferral is open from Schedule until exactly one of Claim, Cancel, or Expire consumes it. There is no other transition and no way back.

Example
1Schedule ──> open ──┬── Claim   (now >= DueAt, before ExpiresAt)
2                    ├── Cancel  (owner only, any time while open)
3                    └── Expire  (anyone, once now >= ExpiresAt)

Claimability is the half-open interval [DueAt, ExpiresAt): due at DueAt, no longer claimable at ExpiresAt. A deferral scheduled with ttl == 0 never expires and has ExpiresAt == 0.

State growth

Consumed deferrals are removed, not archived — the ID counter, not a tombstone, is what prevents replay, so there is nothing to keep. Open deferrals are capped per Book at construction. Storage is therefore bounded by maxOpen regardless of how many deferrals have ever existed. Audit history belongs in the consuming realm's events.

Consumer contract (the parts the package cannot enforce)

  1. SUPPLY A REAL CLOCK. duebook cannot verify that `now` came from runtime.ChainHeight() or time.Now().Unix(). A realm that lets a caller choose `now` has no delay at all. Pass the chain's clock, never a transaction parameter. This is the single most important obligation and the one most commonly got wrong.
  2. USE ONE CLOCK CONSISTENTLY. Heights and seconds must not be mixed within a Book; delay, ttl and now are all in the caller's chosen unit.
  3. DO NOT EXPORT THE BOOK. A *Book is a mutable handle. Returning one across a realm boundary hands out the right to schedule, cancel and claim. Expose your own crossing functions instead; this package returns Deferral values, never pointers into its state.
  4. AUTHORIZE THE ACTOR. duebook authenticates nothing but ownership on Cancel. Who may Schedule, and who may Claim, are the realm's policy — derive the caller from cur.Previous().Address(), not from an argument.
  5. ACT AFTER A SUCCESSFUL CLAIM, IN THE SAME TRANSACTION. Claim's return value is the authorization. Storing it to act on later reintroduces the replay window this package exists to close.

All failures are returned as errors and leave the Book COMPLETELY UNCHANGED. Must* wrappers are the only functions here that panic.

The Book is address-agnostic: owners are non-empty strings. Realms normally use address.String().

Constants 2

const MaxOpenLimit

1const MaxOpenLimit = 10000
source

MaxOpenLimit is the largest maxOpen a Book may be constructed with. It bounds the worst-case cost of Due and IterateOpen, which scan the open set.

const MaxPayloadLen

1const MaxPayloadLen = 4096
source

MaxPayloadLen bounds a single deferral's payload. The payload is opaque to duebook — it exists so a realm can recover what it scheduled without keeping a parallel table.

Variables 1

var ErrEmptyOwner, ErrPayloadTooBig, ErrInvalidNow, ErrInvalidDelay, ErrInvalidTTL, ErrInvalidConfig, ErrBookFull, ErrNotFound, ErrNotDue, ErrExpired, ErrNotExpired, ErrNotOwner, ErrOverflow, ErrIDExhausted

 1var (
 2	ErrEmptyOwner    = errors.New("duebook: empty owner key")
 3	ErrPayloadTooBig = errors.New("duebook: payload exceeds MaxPayloadLen")
 4	ErrInvalidNow    = errors.New("duebook: now must be non-negative")
 5	ErrInvalidDelay  = errors.New("duebook: delay outside [minDelay, maxDelay]")
 6	ErrInvalidTTL    = errors.New("duebook: ttl must be non-negative")
 7	ErrInvalidConfig = errors.New("duebook: invalid book configuration")
 8	ErrBookFull      = errors.New("duebook: open deferral cap reached")
 9	ErrNotFound      = errors.New("duebook: no such open deferral")
10	ErrNotDue        = errors.New("duebook: not due yet")
11	ErrExpired       = errors.New("duebook: deferral has expired")
12	ErrNotExpired    = errors.New("duebook: deferral has not expired")
13	ErrNotOwner      = errors.New("duebook: caller does not own this deferral")
14	ErrOverflow      = errors.New("duebook: int64 overflow")
15	ErrIDExhausted   = errors.New("duebook: identifier space exhausted")
16)
source

Errors returned by Book operations.

Functions 2

func MustNew

1func MustNew(minDelay, maxDelay int64, maxOpen int) *Book
source

MustNew is New but panics on error.

func New

1func New(minDelay, maxDelay int64, maxOpen int) (*Book, error)
source

New returns an empty Book.

minDelay is the floor on how far ahead a deferral may be scheduled; 0 permits same-instant scheduling. maxDelay is the ceiling, and doubles as the overflow guard on now+delay. maxOpen caps simultaneously open deferrals and must be in [1, MaxOpenLimit].

Units are the caller's choice — block heights or seconds — but must be used consistently for the life of the Book.

Types 2

type Book

struct
1type Book struct {
2	minDelay int64
3	maxDelay int64
4	maxOpen  int
5	nextID   uint64    // never decreases; IDs are never reused
6	open     *avl.Tree // padded id -> Deferral
7}
source

Book holds the open deferrals of one consuming realm. The zero value is not usable; construct with New.

Methods on Book

func Cancel

method on Book
1func (b *Book) Cancel(id uint64, owner string) (Deferral, error)
source

Cancel consumes an open deferral without performing it. Only its owner may cancel, and cancellation is permitted at any time while the deferral is open — including after it became due but before anyone claimed it.

Fails with ErrEmptyOwner, ErrNotFound, or ErrNotOwner. On error nothing is modified.

func Claim

method on Book
1func (b *Book) Claim(id uint64, now int64) (Deferral, error)
source

Claim consumes the deferral and returns it, authorizing the caller to perform the deferred action NOW, in this transaction.

It succeeds only while the deferral is open and now is in [DueAt, ExpiresAt). The deferral is removed BEFORE Claim returns, so a re-entrant or later Claim of the same ID finds nothing; combined with non-reused IDs, at most one Claim per ID ever succeeds.

Fails with ErrInvalidNow, ErrNotFound (never existed, or already consumed by Claim/Cancel/Expire), ErrNotDue, or ErrExpired. On error nothing is modified.

duebook does NOT check who is claiming: whether a deferral is permissionlessly claimable or restricted to its owner is the consuming realm's policy, applied before calling Claim.

func Due

method on Book
1func (b *Book) Due(now int64, limit int) []Deferral
source

Due returns up to limit open deferrals that are claimable at now, in ascending ID order (oldest first). A limit <= 0 returns nothing.

It scans the open set, so its cost is bounded by MaxOpen.

func Expirable

method on Book
1func (b *Book) Expirable(now int64, limit int) []Deferral
source

Expirable returns up to limit open deferrals that Expire would accept at now, in ascending ID order. A limit <= 0 returns nothing.

func Expire

method on Book
1func (b *Book) Expire(id uint64, now int64) (Deferral, error)
source

Expire consumes a deferral that is past its expiry, reclaiming its storage. It is deliberately permissionless: an expired deferral can never be claimed again, so letting anyone clear it keeps a Book from silting up with dead entries that block Schedule against maxOpen.

Fails with ErrInvalidNow, ErrNotFound, or ErrNotExpired (including for deferrals with no expiry, which never expire). On error nothing is modified.

func Get

method on Book
1func (b *Book) Get(id uint64) (Deferral, bool)
source

Get returns an open deferral by ID. The second result is false if the deferral never existed or has already been consumed — Get cannot tell those apart, by design: consumed deferrals leave no tombstone.

Get is a read-only preview and never authorizes anything. Only Claim's return value authorizes an action.

func IterateOpen

method on Book
1func (b *Book) IterateOpen(fn func(Deferral) bool)
source

IterateOpen calls fn for every open deferral in ascending ID order. Iteration stops early when fn returns true.

fn receives a COPY: Deferral is passed by value and holds only scalars and strings, so fn gets no pointer into the Book and cannot reach past it — Book's fields are all unexported.

Two rules for fn, and the second is the one that is easy to miss.

  • Do not Schedule, Claim, Cancel or Expire from inside fn: mutating the tree while iterating it is undefined. Collect IDs first, then act after IterateOpen returns.

  • fn runs under the CALLING REALM'S STORAGE AUTHORITY. This method's receiver is stamped with the importing realm's PkgID, so the borrow rules leave the realm context set to that realm for the whole callback, and a top-level fn has no receiver and no declaring realm to anchor it elsewhere. A callback that re-enters the calling realm's own mutators therefore does so with that realm's authority. Never pass a caller-supplied function here from inside a permission-gated path; pass only a closure this package's consumer wrote itself.

func MaxDelay

method on Book
1func (b *Book) MaxDelay() int64
source

MaxDelay returns the Book's scheduling ceiling.

func MaxOpen

method on Book
1func (b *Book) MaxOpen() int
source

MaxOpen returns the Book's cap on simultaneously open deferrals.

func MinDelay

method on Book
1func (b *Book) MinDelay() int64
source

MinDelay returns the Book's scheduling floor.

func MustCancel

method on Book
1func (b *Book) MustCancel(id uint64, owner string) Deferral
source

MustCancel is Cancel but panics on error.

func MustClaim

method on Book
1func (b *Book) MustClaim(id uint64, now int64) Deferral
source

MustClaim is Claim but panics on error.

func MustExpire

method on Book
1func (b *Book) MustExpire(id uint64, now int64) Deferral
source

MustExpire is Expire but panics on error.

func MustSchedule

method on Book
1func (b *Book) MustSchedule(owner, payload string, now, delay, ttl int64) uint64
source

MustSchedule is Schedule but panics on error.

func NextID

method on Book
1func (b *Book) NextID() uint64
source

NextID returns the identifier the next Schedule will allocate. It only ever increases, which is what makes a consumed ID unreusable.

func OpenCount

method on Book
1func (b *Book) OpenCount() int
source

OpenCount returns how many deferrals are currently open.

func Schedule

method on Book
1func (b *Book) Schedule(owner, payload string, now, delay, ttl int64) (uint64, error)
source

Schedule opens a deferral owned by owner, due at now+delay, expiring ttl after that (ttl == 0 means it never expires). It returns the new deferral's ID.

Fails with ErrEmptyOwner, ErrPayloadTooBig, ErrInvalidNow (now < 0), ErrInvalidDelay (delay outside [MinDelay, MaxDelay]), ErrInvalidTTL (ttl < 0), ErrBookFull, ErrOverflow, or ErrIDExhausted. On error nothing is modified.

type Deferral

struct
1type Deferral struct {
2	ID        uint64
3	Owner     string
4	Payload   string
5	CreatedAt int64
6	DueAt     int64
7	ExpiresAt int64
8}
source

Deferral is a scheduled action. It is returned BY VALUE: holders cannot reach into a Book through it. Payload is opaque to this package.

ExpiresAt == 0 means the deferral never expires. Otherwise the deferral is claimable exactly on [DueAt, ExpiresAt).

Methods on Deferral

func IsClaimable

method on Deferral
1func (d Deferral) IsClaimable(now int64) bool
source

IsClaimable reports whether Claim would succeed at now, assuming the deferral is still open.

func IsDue

method on Deferral
1func (d Deferral) IsDue(now int64) bool
source

IsDue reports whether the deferral has reached its due time at now.

func IsExpired

method on Deferral
1func (d Deferral) IsExpired(now int64) bool
source

IsExpired reports whether the deferral is past its expiry at now. A deferral with no expiry is never expired.

Imports 3

Source Files 2