v0 source pure
Package pausable is the switch a realm checks before it acts: a parsed pause state, the rule for combining two of the...
View source
gno.land/p/moul/pausable
The switch a realm checks before it acts: a parsed pause state, the rule for combining two of them, and the asserts that stop a call.
It holds no state and knows nothing about where a pause setting is stored.
r/moul/config is the realm that stores one and
wires this to it; this package is the part worth getting right once.
1st := pausable.MustParse(raw) // a stored value, failing closed
2st.AllowsRead() // false only when fully paused
3st.AllowsWrite() // false unless running
4st.AssertWritable() // aborts, with the reason if there is one
5st.Notice() // the markdown banner, or "" while running
6pausable.Strictest(a, b) // combine a global and a per-realm state
Three levels, because "paused" is usually too blunt
Taking a realm fully offline hides the thing people came to read. Most incidents only need the writes stopped: a board keeps rendering, an exchange keeps quoting, nothing new lands while the fix is prepared.
| level | value | reads | writes |
|---|---|---|---|
| Running | "" or running |
yes | yes |
| ReadOnly | readonly |
yes | no |
| Paused | paused |
no | no |
Each optionally followed by : <reason>, which shows up in the banner and in
the abort message: paused: migrating storage, back in an hour.
So a realm guards its mutating functions and leaves Render alone:
1func Post(cur realm, body string) {
2 config.AssertWritable() // aborts while ReadOnly or Paused
3 ...
4}
5
6func Render(path string) string {
7 return config.TopBlock() + body // the banner explains itself
8}
AssertReadable exists but most realms should not put it in Render. A page
that aborts tells a reader nothing; the banner tells them what happened and
when to come back. Reach for it only where serving stale data is itself the
harm.
It fails closed, and that is why the writer must validate
MustParse turns a value it does not recognise into Paused, not Running. A
pause switch that a typo silently disables is not a pause switch: the failure
would be invisible until the incident it was meant to cover.
The cost is that a typo takes a realm offline, so Parse reports ok=false
and the storing realm is expected to refuse the write. Validate on the way
in, fail closed on the way out. r/moul/config does exactly that, so in
practice the closed path is never reached.
The values this refuses are the ones someone would plausibly type: yes,
true, 1, on, stop, read-only. None of them may read as Running.
The stricter of two states wins
A global pause and a per-realm pause combine with Strictest, so a per-realm
entry left behind from last month can never re-open a realm during a global
halt.
The cost is that exempting one realm from a global pause is not expressible. Clear the global and set the others instead. That is the right trade for an emergency brake: the failure mode of the alternative is a stale exemption nobody remembers, discovered during the incident.
The reason travels with the level that won, and on a tie the first argument
does, so calling Strictest(global, scoped) keeps the global explanation when
both say the same thing.
Notice is one markdown block
The reason is text a manager typed, and it lands inside a blockquote where a
newline would end the quote and let the rest render as page content. Notice
folds it to a single line.
Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.
Package pausable is the switch a realm checks before it acts: a parsed pause state, the rule for combining two of them, and the asserts that stop a call.
Three levels, because "paused" is usually too blunt
Taking a realm fully offline hides the thing people came to read. Most incidents only need the writes stopped: a board keeps rendering, an exchange keeps quoting, and nothing new lands while the fix is prepared. So the levels are Running, ReadOnly and Paused, and a realm normally guards its mutating functions with AssertWritable and leaves Render alone.
Example
1func Post(cur realm, body string) {
2 config.AssertWritable() // aborts while the realm is ReadOnly or Paused
3 …
4}
5
6func Render(path string) string {
7 return config.TopBlock() + body // the banner explains itself
8}
It fails closed
MustParse turns a value it does not recognise into Paused, not Running. A pause switch that a typo silently disables is not a pause switch, and the failure is loud and one transaction from fixed, where the opposite failure is silent and discovered during the incident it was meant to cover.
The cost of that choice is a typo taking a realm offline, so the writer is expected to validate: Parse reports ok=false and the storing realm refuses the write. Validate on the way in, fail closed on the way out.
The stricter of two states wins
A global pause and a per-realm pause combine with Strictest, so a stale per-realm entry can never re-open a realm during a global halt. The cost is that exempting one realm from a global pause is not expressible; clear the global and set the others instead.
1
3
func MustParse
MustParse is Parse for a reader, failing closed: a value it cannot read becomes Paused, carrying the raw text as the reason so whoever hits it can see what is wrong.
func Parse
Parse reads a stored value: "", "running", "readonly", "paused", each optionally followed by ": <reason>".
ok is false for anything else. A caller that is STORING the value should refuse on !ok; a caller that is READING one should use MustParse, which fails closed instead.
func Strictest
Strictest returns whichever of the two stops more, keeping the reason that belongs to the level it returns. On a tie the first argument wins, so a caller passing (global, scoped) keeps the global explanation when both say the same thing, and the scoped one when it is the stricter.
2
type Level
identLevel is how much of a realm is still allowed to work.
Methods on Level
func AllowsRead
method on LevelAllowsRead reports whether rendering and querying are still allowed.
func AllowsWrite
method on LevelAllowsWrite reports whether state-changing calls are still allowed.
func String
method on Leveltype State
structState is a parsed pause setting: a level and an optional reason to show.
The zero State is Running with no reason, which is what an unset setting means, so a realm that never configures anything is never paused.
Methods on State
func AllowsRead
method on StateAllowsRead reports whether rendering and querying are still allowed.
func AllowsWrite
method on StateAllowsWrite reports whether state-changing calls are still allowed.
func AssertReadable
method on StateAssertReadable aborts unless reads are allowed. Most realms do not want this in Render: a page that aborts tells a reader nothing, while Notice tells them what happened and when to come back.
func AssertWritable
method on StateAssertWritable aborts unless writes are allowed. This is the one a realm puts at the top of every state-changing function.
func IsPaused
method on StateIsPaused reports whether anything at all is being held back. It is true for ReadOnly as well as Paused, because the question a caller usually means by "is it paused" is "is it behaving normally".
func Notice
method on StateNotice is the banner a Render puts above its content, or "" while running.
It is a markdown blockquote, so it reads as set apart from the page without needing any style the renderer might not have.
func String
method on StateString renders a State back into a storable value. It round-trips through Parse, and the zero State renders empty so an unset setting stays unset.
1
- strings stdlib