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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

pausable.gno

7.34 Kb · 232 lines
  1// Package pausable is the switch a realm checks before it acts: a parsed pause
  2// state, the rule for combining two of them, and the asserts that stop a call.
  3//
  4// # Three levels, because "paused" is usually too blunt
  5//
  6// Taking a realm fully offline hides the thing people came to read. Most
  7// incidents only need the writes stopped: a board keeps rendering, an exchange
  8// keeps quoting, and nothing new lands while the fix is prepared. So the
  9// levels are Running, ReadOnly and Paused, and a realm normally guards its
 10// mutating functions with AssertWritable and leaves Render alone.
 11//
 12//	func Post(cur realm, body string) {
 13//		config.AssertWritable()  // aborts while the realm is ReadOnly or Paused
 14//		…
 15//	}
 16//
 17//	func Render(path string) string {
 18//		return config.TopBlock() + body  // the banner explains itself
 19//	}
 20//
 21// # It fails closed
 22//
 23// MustParse turns a value it does not recognise into Paused, not Running. A
 24// pause switch that a typo silently disables is not a pause switch, and the
 25// failure is loud and one transaction from fixed, where the opposite failure
 26// is silent and discovered during the incident it was meant to cover.
 27//
 28// The cost of that choice is a typo taking a realm offline, so the writer is
 29// expected to validate: Parse reports ok=false and the storing realm refuses
 30// the write. Validate on the way in, fail closed on the way out.
 31//
 32// # The stricter of two states wins
 33//
 34// A global pause and a per-realm pause combine with Strictest, so a stale
 35// per-realm entry can never re-open a realm during a global halt. The cost is
 36// that exempting one realm from a global pause is not expressible; clear the
 37// global and set the others instead.
 38package pausable
 39
 40import "strings"
 41
 42// Level is how much of a realm is still allowed to work.
 43type Level int
 44
 45const (
 46	// Running is the normal state, and what an unset value means.
 47	Running Level = iota
 48	// ReadOnly still renders and still answers queries; it refuses writes.
 49	ReadOnly
 50	// Paused refuses everything, reads included.
 51	Paused
 52)
 53
 54// The canonical spelling of each level, which is what a value round-trips to.
 55const (
 56	runningWord  = "running"
 57	readOnlyWord = "readonly"
 58	pausedWord   = "paused"
 59)
 60
 61// reasonSep separates the level from the human explanation in a stored value:
 62// "paused: migrating storage, back in an hour".
 63const reasonSep = ":"
 64
 65func (l Level) String() string {
 66	switch l {
 67	case Running:
 68		return runningWord
 69	case ReadOnly:
 70		return readOnlyWord
 71	case Paused:
 72		return pausedWord
 73	default:
 74		// Not reachable through Parse, and not worth panicking over in a
 75		// Render path: an unknown level reads as the strictest one.
 76		return pausedWord
 77	}
 78}
 79
 80// AllowsRead reports whether rendering and querying are still allowed.
 81func (l Level) AllowsRead() bool { return l != Paused }
 82
 83// AllowsWrite reports whether state-changing calls are still allowed.
 84func (l Level) AllowsWrite() bool { return l == Running }
 85
 86// State is a parsed pause setting: a level and an optional reason to show.
 87//
 88// The zero State is Running with no reason, which is what an unset setting
 89// means, so a realm that never configures anything is never paused.
 90type State struct {
 91	Level  Level
 92	Reason string
 93}
 94
 95// Parse reads a stored value: "", "running", "readonly", "paused", each
 96// optionally followed by ": <reason>".
 97//
 98// ok is false for anything else. A caller that is STORING the value should
 99// refuse on !ok; a caller that is READING one should use MustParse, which
100// fails closed instead.
101func Parse(raw string) (State, bool) {
102	raw = strings.TrimSpace(raw)
103	if raw == "" {
104		return State{}, true
105	}
106
107	word, reason := raw, ""
108	if i := strings.Index(raw, reasonSep); i >= 0 {
109		word = strings.TrimSpace(raw[:i])
110		reason = strings.TrimSpace(raw[i+1:])
111	}
112
113	switch strings.ToLower(word) {
114	case runningWord:
115		return State{Level: Running, Reason: reason}, true
116	case readOnlyWord:
117		return State{Level: ReadOnly, Reason: reason}, true
118	case pausedWord:
119		return State{Level: Paused, Reason: reason}, true
120	default:
121		return State{}, false
122	}
123}
124
125// MustParse is Parse for a reader, failing closed: a value it cannot read
126// becomes Paused, carrying the raw text as the reason so whoever hits it can
127// see what is wrong.
128func MustParse(raw string) State {
129	if s, ok := Parse(raw); ok {
130		return s
131	}
132	return State{Level: Paused, Reason: "unreadable pause setting: " + strings.TrimSpace(raw)}
133}
134
135// String renders a State back into a storable value. It round-trips through
136// Parse, and the zero State renders empty so an unset setting stays unset.
137func (s State) String() string {
138	if s.Level == Running && s.Reason == "" {
139		return ""
140	}
141	if s.Reason == "" {
142		return s.Level.String()
143	}
144	return s.Level.String() + reasonSep + " " + s.Reason
145}
146
147// Strictest returns whichever of the two stops more, keeping the reason that
148// belongs to the level it returns. On a tie the first argument wins, so a
149// caller passing (global, scoped) keeps the global explanation when both say
150// the same thing, and the scoped one when it is the stricter.
151func Strictest(a, b State) State {
152	if b.Level > a.Level {
153		return b
154	}
155	return a
156}
157
158// AllowsRead reports whether rendering and querying are still allowed.
159func (s State) AllowsRead() bool { return s.Level.AllowsRead() }
160
161// AllowsWrite reports whether state-changing calls are still allowed.
162func (s State) AllowsWrite() bool { return s.Level.AllowsWrite() }
163
164// IsPaused reports whether anything at all is being held back. It is true for
165// ReadOnly as well as Paused, because the question a caller usually means by
166// "is it paused" is "is it behaving normally".
167func (s State) IsPaused() bool { return s.Level != Running }
168
169// AssertWritable aborts unless writes are allowed. This is the one a realm
170// puts at the top of every state-changing function.
171func (s State) AssertWritable() {
172	if !s.AllowsWrite() {
173		panic(s.message("writes are paused"))
174	}
175}
176
177// AssertReadable aborts unless reads are allowed. Most realms do not want this
178// in Render: a page that aborts tells a reader nothing, while Notice tells
179// them what happened and when to come back.
180func (s State) AssertReadable() {
181	if !s.AllowsRead() {
182		panic(s.message("paused"))
183	}
184}
185
186func (s State) message(what string) string {
187	if s.Reason == "" {
188		return what
189	}
190	return what + reasonSep + " " + s.Reason
191}
192
193// Notice is the banner a Render puts above its content, or "" while running.
194//
195// It is a markdown blockquote, so it reads as set apart from the page without
196// needing any style the renderer might not have.
197func (s State) Notice() string {
198	if s.Level == Running {
199		return ""
200	}
201	head := "**Paused.** This realm is not accepting anything right now."
202	if s.Level == ReadOnly {
203		head = "**Read-only.** This realm is still readable, but not accepting changes."
204	}
205	if s.Reason == "" {
206		return "> " + head
207	}
208	// The reason is text a manager typed, and it lands inside a blockquote
209	// where a newline would end the quote and let the rest render as page
210	// content. Folding it to a single line keeps the banner one block.
211	return "> " + head + " " + oneLine(s.Reason)
212}
213
214// oneLine collapses every run of whitespace, newlines included, into single
215// spaces.
216func oneLine(s string) string {
217	var b strings.Builder
218	space := false
219	for i := 0; i < len(s); i++ {
220		c := s[i]
221		if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
222			space = true
223			continue
224		}
225		if space && b.Len() > 0 {
226			b.WriteByte(' ')
227		}
228		space = false
229		b.WriteByte(c)
230	}
231	return b.String()
232}