mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1792794944815912745 (novox/mesh-sdk at e1f393bb into main, don…
mesh/delivery-group group fix/1930-digest-shape merged: every member is merged
mesh/check pass 403a5d72ab93 on 1ff6d339780c | unit 4545 run, 2097 kept, 13 skipped | integration 4595 run, 2188 kept, 21 skipped | over bound: unit, …
A check ask's answer kept the digest's packages as a map keyed by Go import path;
the node runtime refuses a state value with a field ending in a credential word,
so the end of any check of mesh-controller (internal/token) was never written and
the ask was acked anyway. The level's digest is now delivery.LevelDigest, packages
a sorted list of {package, digest}, reading the old map form too. finish logs an
end refused every time, writes a small failed end naming the write's error, and
the puller gives the ask back instead of acking it when no end could be written.
503 lines
20 KiB
Go
503 lines
20 KiB
Go
package delivery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// **Asks: a build or a check step is a message a build node pulls when it has a free slot** (novox/hq ADR 0525, as
|
|
// ADR 0536 §4 changes it; issue 1887). The builder puts a trunk fingerprint's build on the delivery stream of the
|
|
// artifact's language and a candidate's on the check stream; the checker puts a check of one level on the check stream,
|
|
// and a re-judge after a trunk move on the recheck stream. Each build module claims the node seat of its language
|
|
// (`node-build-go`, `node-build-node`, `node-build-image`), whose accepted kinds the broker derives as the work queue
|
|
// `SEAT_NODE_BUILD_<LANGUAGE>` with one shared pull worker per kind; a holder pulls delivery first, then check, then
|
|
// recheck, at most its node's build capacity at once (ADR 0525 §4, §5).
|
|
//
|
|
// **An ask is one whole job on one node** (ADR 0525 §3): a build ask is the fetch, the build (or copy) of each artifact
|
|
// and the push; a check ask is the fetch, the digest and the tests of one level. Its end is written on the build module's
|
|
// step key `<node>.<ask id>` (StepsBucket), which the asker watches with AskStepWatch (ADR 0525 §6).
|
|
|
|
// The kinds of ask: the seat's accepted kinds, in the order a holder pulls them.
|
|
const (
|
|
AskDelivery = "delivery"
|
|
AskCheck = "check"
|
|
AskRecheck = "recheck"
|
|
)
|
|
|
|
// AskKinds are the accepted kinds of a build seat, in the order a holder pulls them (ADR 0536 §4).
|
|
var AskKinds = []string{AskDelivery, AskCheck, AskRecheck}
|
|
|
|
// The languages of the build seats: each is one build module's (mesh-build-<language>) and one seat's
|
|
// (node-build-<language>).
|
|
const (
|
|
AskGo = "go"
|
|
AskNode = "node"
|
|
AskImage = "image"
|
|
)
|
|
|
|
// AskLanguages are the build seats' languages.
|
|
var AskLanguages = []string{AskGo, AskNode, AskImage}
|
|
|
|
// AskRuns is how many times an ask is run before it is taken for poison (issue 1907): an ask whose run never ends (it
|
|
// takes its node down, or its process dies with it) is delivered again by its stream to the next free node (ADR 0525
|
|
// §6); the holder that takes it past AskRuns runs it no more, writes its step failed saying so, and ends it, so the
|
|
// asker's key fails with why and no further node is taken down. NATS-native: the count is the stream's own delivery
|
|
// count of the message, no timer.
|
|
const AskRuns = 3
|
|
|
|
// AskMaxDeliver is the build seats' workers' bound of deliveries (the broker's MaxDeliver): past AskRuns, so the holder
|
|
// that says the poison gets its delivery, with one more for a holder that dies saying it.
|
|
const AskMaxDeliver = AskRuns + 2
|
|
|
|
// StepAskVersion is the version of an Ask value; a reader refuses another (ErrUnknownVersion).
|
|
const StepAskVersion = 1
|
|
|
|
func checkAskKind(kind string) error {
|
|
for _, k := range AskKinds {
|
|
if kind == k {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("delivery: the ask kind %q is not delivery, check or recheck", kind)
|
|
}
|
|
|
|
func checkAskLanguage(language string) error {
|
|
for _, l := range AskLanguages {
|
|
if language == l {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("delivery: the ask language %q is not go, node or image", language)
|
|
}
|
|
|
|
// BuildSeat is the node seat a build module of language claims: node-build-<language>.
|
|
func BuildSeat(language string) (string, error) {
|
|
if err := checkAskLanguage(language); err != nil {
|
|
return "", err
|
|
}
|
|
return "node-build-" + language, nil
|
|
}
|
|
|
|
// BuildModuleOf is the build module that holds language's seat: mesh-build-<language>.
|
|
func BuildModuleOf(language string) (string, error) {
|
|
if err := checkAskLanguage(language); err != nil {
|
|
return "", err
|
|
}
|
|
return "mesh-build-" + language, nil
|
|
}
|
|
|
|
// AskSubject is the subject an ask of kind for language is published on: the seat's accept subject,
|
|
// `mesh.seat.node-build-<language>.accept.<kind>` (mesh-controller internal/broker SeatStreams).
|
|
func AskSubject(language, kind string) (string, error) {
|
|
seat, err := BuildSeat(language)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := checkAskKind(kind); err != nil {
|
|
return "", err
|
|
}
|
|
return "mesh.seat." + seat + ".accept." + kind, nil
|
|
}
|
|
|
|
// ParseAskSubject is the inverse of AskSubject; it refuses a subject of another seat or kind.
|
|
func ParseAskSubject(subject string) (language, kind string, err error) {
|
|
rest, ok := strings.CutPrefix(subject, "mesh.seat.node-build-")
|
|
if !ok {
|
|
return "", "", fmt.Errorf("delivery: %q is not a build seat's accept subject", subject)
|
|
}
|
|
language, kind, ok = strings.Cut(rest, ".accept.")
|
|
if !ok {
|
|
return "", "", fmt.Errorf("delivery: %q is not a build seat's accept subject", subject)
|
|
}
|
|
if want, err := AskSubject(language, kind); err != nil || want != subject {
|
|
return "", "", fmt.Errorf("delivery: %q is not a build seat's accept subject: %v", subject, err)
|
|
}
|
|
return language, kind, nil
|
|
}
|
|
|
|
// AskStream is the work-queue stream of language's seat, as the broker names a seat's stream: SEAT_NODE_BUILD_<LANGUAGE>.
|
|
func AskStream(language string) (string, error) {
|
|
seat, err := BuildSeat(language)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return "SEAT_" + strings.ToUpper(strings.ReplaceAll(seat, "-", "_")), nil
|
|
}
|
|
|
|
// AskWorker is the shared pull consumer of one kind on language's stream, filtered on that kind's subject:
|
|
// SEAT_NODE_BUILD_<LANGUAGE>_<KIND>_worker. Every holder of the seat pulls from it, so the work is shared without a
|
|
// holder knowing the others (ADR 0190), and the kinds' workers are apart so a holder can pull them in their order.
|
|
func AskWorker(language, kind string) (string, error) {
|
|
stream, err := AskStream(language)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := checkAskKind(kind); err != nil {
|
|
return "", err
|
|
}
|
|
return stream + "_" + strings.ToUpper(kind) + "_worker", nil
|
|
}
|
|
|
|
// An ask id is a step id of the form buildstep.NewID gives: the time in milliseconds in base 36 (8 or 9 digits) and six
|
|
// random hex digits. Its form keeps it apart from a module's or a node's name in a key.
|
|
var askIDOK = regexp.MustCompile(`^[0-9a-z]{8,9}[0-9a-f]{6}$`)
|
|
|
|
func checkAskID(id string) error {
|
|
if !askIDOK.MatchString(id) {
|
|
return fmt.Errorf("delivery: %q is not an ask id (base-36 milliseconds and six hex digits)", id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// StepAsk is the message on a build seat's stream: who asked what, of which kind and language, and the one whole job.
|
|
// Exactly one of Build and Check is set: a delivery ask is a build; a check ask a build (a candidate's fingerprint) or a
|
|
// check; a recheck ask a check; an image ask is always a build.
|
|
type StepAsk struct {
|
|
V int `json:"v"`
|
|
ID string `json:"id"`
|
|
Kind string `json:"kind"`
|
|
Language string `json:"language"`
|
|
// Asker is the module that put the ask (mesh-delivery-builder or mesh-delivery-checker); the stream's grant is what
|
|
// makes it true, this field only names it to a reader.
|
|
Asker string `json:"asker"`
|
|
At time.Time `json:"at"`
|
|
Build *BuildJob `json:"build,omitempty"`
|
|
Check *CheckJob `json:"check,omitempty"`
|
|
}
|
|
|
|
// BuildJob is a build ask's job: fetch the commit (unless every artifact is a copy), then build or copy each artifact,
|
|
// pushing it when Publish says so, and answer each artifact's digest and reference (BuildAnswer).
|
|
type BuildJob struct {
|
|
// Module is the module's name; Path its directory in the repository (the build tool's `module`, "." for its root).
|
|
Module string `json:"module"`
|
|
Path string `json:"path,omitempty"`
|
|
// FP is the fingerprint the build is of (the builder's key), named for a reader of the ask.
|
|
FP string `json:"fp,omitempty"`
|
|
// Fetch is the fetch tool's arguments, as the build module's fetch tool takes them (buildstep.FetchRequest's Args);
|
|
// absent when every artifact is a copy.
|
|
Fetch map[string]any `json:"fetch,omitempty"`
|
|
// Artifacts are built in this order.
|
|
Artifacts []AskArtifact `json:"artifacts"`
|
|
// Publish says the build pushes an image or publishes a package: a trunk build only (ADR 0487 clause 12).
|
|
Publish bool `json:"publish,omitempty"`
|
|
// Toolchain is the toolchain the fingerprint was computed with; a build module running another refuses the ask.
|
|
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
|
|
}
|
|
|
|
// AskArtifact is one artifact of a build ask: built from the fetched tree, or copied from From, an upstream pin (ADR
|
|
// 0498 clause 4).
|
|
type AskArtifact struct {
|
|
Artifact string `json:"artifact"`
|
|
From string `json:"from,omitempty"`
|
|
}
|
|
|
|
// CheckJob is a check ask's job, the levels of one check (ADR 0525 §3): fetch the head merged onto its trunk once, then
|
|
// for each level in order digest its packages, take the kept verdicts for the digests that have one, and test the rest,
|
|
// the packages the change's own tests are in first, stopping at the first level that does not pass (CheckAnswer).
|
|
type CheckJob struct {
|
|
// Fetch is the fetch tool's arguments (repository, commit, ref, base, and the build module's own: beside, package).
|
|
Fetch map[string]any `json:"fetch"`
|
|
// Levels are run in this order (LevelUnit, LevelIntegration); a try may name one.
|
|
Levels []string `json:"levels"`
|
|
// Kept says the build module takes the kept verdicts of the checker's kept bucket (CheckerKeptBucket, by KeptKey)
|
|
// for the packages whose digest has one and covers the siblings the fetch placed, and tests only the others (issue
|
|
// 1887's design limit 1). A try that runs some tests (Run), and a level the candidate's own build module judges
|
|
// (Runner), keep nothing.
|
|
Kept bool `json:"kept,omitempty"`
|
|
// Runner is the test step's runner ("workspace": the candidate's own build module judges it, ADR 0535 §4), used
|
|
// when the merge changes a path under RunnerWhen (every merge when RunnerWhen is empty).
|
|
Runner string `json:"runner,omitempty"`
|
|
RunnerWhen string `json:"runner_when,omitempty"`
|
|
// Run is a try's test pattern (go test -run): each level runs it alone, digesting nothing.
|
|
Run string `json:"run,omitempty"`
|
|
}
|
|
|
|
func (a StepAsk) check() error {
|
|
if err := checkAskID(a.ID); err != nil {
|
|
return err
|
|
}
|
|
if err := checkAskKind(a.Kind); err != nil {
|
|
return err
|
|
}
|
|
if err := checkAskLanguage(a.Language); err != nil {
|
|
return err
|
|
}
|
|
if err := checkName("asker", a.Asker); err != nil {
|
|
return err
|
|
}
|
|
if a.At.IsZero() {
|
|
return fmt.Errorf("delivery: the ask %s names no time", a.ID)
|
|
}
|
|
switch {
|
|
case (a.Build == nil) == (a.Check == nil):
|
|
return fmt.Errorf("delivery: the ask %s holds a build or a check, exactly one", a.ID)
|
|
case a.Build != nil && a.Kind == AskRecheck:
|
|
return fmt.Errorf("delivery: the ask %s is a build on the recheck stream, which carries checks alone", a.ID)
|
|
case a.Check != nil && a.Kind == AskDelivery:
|
|
return fmt.Errorf("delivery: the ask %s is a check on the delivery stream, which carries trunk builds alone", a.ID)
|
|
case a.Check != nil && a.Language == AskImage:
|
|
return fmt.Errorf("delivery: the ask %s is a check of an image, which has no tests", a.ID)
|
|
}
|
|
if a.Build != nil {
|
|
return a.Build.check(a.ID)
|
|
}
|
|
return a.Check.check(a.ID)
|
|
}
|
|
|
|
func (b BuildJob) check(id string) error {
|
|
if err := checkName("module", b.Module); err != nil {
|
|
return err
|
|
}
|
|
if len(b.Artifacts) == 0 {
|
|
return fmt.Errorf("delivery: the build ask %s names no artifact", id)
|
|
}
|
|
fetch := false
|
|
for _, a := range b.Artifacts {
|
|
if err := checkName("artifact", a.Artifact); err != nil {
|
|
return err
|
|
}
|
|
fetch = fetch || a.From == ""
|
|
}
|
|
if fetch {
|
|
if b.Path == "" {
|
|
return fmt.Errorf("delivery: the build ask %s names no directory (path) for the module %s", id, b.Module)
|
|
}
|
|
if err := checkFetchArgs(id, b.Fetch); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c CheckJob) check(id string) error {
|
|
if len(c.Levels) == 0 {
|
|
return fmt.Errorf("delivery: the check ask %s names no level", id)
|
|
}
|
|
for _, l := range c.Levels {
|
|
if err := checkLevel(l); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return checkFetchArgs(id, c.Fetch)
|
|
}
|
|
|
|
var askCommitOK = regexp.MustCompile(`^[0-9a-f]{40}$`)
|
|
|
|
// checkFetchArgs refuses fetch arguments with no repository or no full commit; the rest is the build module's fetch to
|
|
// judge, as a direct call's is.
|
|
func checkFetchArgs(id string, args map[string]any) error {
|
|
repository, _ := args["repository"].(string)
|
|
commit, _ := args["commit"].(string)
|
|
if repository == "" || !strings.Contains(repository, "/") {
|
|
return fmt.Errorf("delivery: the ask %s fetches no repository <owner>/<name>", id)
|
|
}
|
|
if !askCommitOK.MatchString(commit) {
|
|
return fmt.Errorf("delivery: the ask %s fetches no full commit", id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeStepAsk is a as JSON at StepAskVersion; it refuses what DecodeStepAsk refuses.
|
|
func EncodeStepAsk(a StepAsk) ([]byte, error) {
|
|
a.V = StepAskVersion
|
|
a.At = a.At.UTC()
|
|
if err := a.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
return json.Marshal(a)
|
|
}
|
|
|
|
// DecodeStepAsk reads an ask; it refuses an unknown version (ErrUnknownVersion), an unknown kind or language, an id that
|
|
// is not an ask id, a build and a check together or neither, a job on a stream that does not carry it, and a fetch with
|
|
// no repository or no full commit.
|
|
func DecodeStepAsk(data []byte) (StepAsk, error) {
|
|
var probe struct {
|
|
V *int `json:"v"`
|
|
}
|
|
if err := json.Unmarshal(data, &probe); err != nil {
|
|
return StepAsk{}, fmt.Errorf("delivery: an ask is not JSON: %w", err)
|
|
}
|
|
if probe.V == nil || *probe.V != StepAskVersion {
|
|
return StepAsk{}, fmt.Errorf("%w: an ask of version %v, this reader reads %d", ErrUnknownVersion, probe.V, StepAskVersion)
|
|
}
|
|
var a StepAsk
|
|
if err := json.Unmarshal(data, &a); err != nil {
|
|
return StepAsk{}, fmt.Errorf("delivery: an ask does not fit its shape: %w", err)
|
|
}
|
|
if err := a.check(); err != nil {
|
|
return StepAsk{}, err
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// BuildAnswer is a build ask's step result: each artifact's digest and, for a pushed one, its reference.
|
|
type BuildAnswer struct {
|
|
Artifacts []AskBuilt `json:"artifacts"`
|
|
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
|
|
}
|
|
|
|
// AskBuilt is one artifact a build ask built.
|
|
type AskBuilt struct {
|
|
Artifact string `json:"artifact"`
|
|
Digest string `json:"digest"`
|
|
Reference string `json:"reference,omitempty"`
|
|
}
|
|
|
|
// CheckAnswer is a check ask's step result, each part the build module's own tool's answer whole: the fetch's, then one
|
|
// LevelAnswer per level run, in order; a level that did not pass is the last. The checker judges and keeps from it as it
|
|
// did from the tools' answers.
|
|
type CheckAnswer struct {
|
|
Fetch json.RawMessage `json:"fetch"`
|
|
Levels []LevelAnswer `json:"levels,omitempty"`
|
|
}
|
|
|
|
// LevelAnswer is one level of a check ask: the digest's answer (absent when nothing was digested: the level ran whole and
|
|
// keeps nothing), the kept verdicts taken, and one test result per test step run, in the order run.
|
|
type LevelAnswer struct {
|
|
Level string `json:"level"`
|
|
// Judged is what the digest under the candidate's runner said of the program that ran it ("candidate <merge>"),
|
|
// when the candidate judged the level (ADR 0535 §4, §5); empty otherwise.
|
|
Judged string `json:"judged,omitempty"`
|
|
Digest *LevelDigest `json:"digest,omitempty"`
|
|
Kept []KeptTaken `json:"kept,omitempty"`
|
|
Tests []json.RawMessage `json:"tests,omitempty"`
|
|
}
|
|
|
|
// LevelDigest is the digest's answer as a level keeps it in the ask's step result: its packages a list of
|
|
// {package, digest} sorted by package, never a map keyed by the package's path (issue 1930). The step result is a
|
|
// state value, and the node runtime refuses a value with a field name ending in a credential word (novox/hq ADR 0201):
|
|
// a Go package such as .../internal/token is such a name as a map key, and is a plain value in the list. It decodes
|
|
// the digest tool's own answer, and a value stored before issue 1930, with packages as a map too.
|
|
type LevelDigest struct {
|
|
Level string `json:"level,omitempty"`
|
|
Packages []PackageDigest `json:"packages"`
|
|
Beside []string `json:"beside,omitempty"`
|
|
WithoutTests []string `json:"without_tests,omitempty"`
|
|
JudgedBy string `json:"judged_by,omitempty"`
|
|
}
|
|
|
|
// PackageDigest is one package's digest at a level.
|
|
type PackageDigest struct {
|
|
Package string `json:"package"`
|
|
Digest string `json:"digest"`
|
|
}
|
|
|
|
// UnmarshalJSON reads packages as the list or as the map keyed by package path (the digest tool's answer, and a level
|
|
// answer stored before issue 1930), and sorts them by package.
|
|
func (d *LevelDigest) UnmarshalJSON(raw []byte) error {
|
|
var v struct {
|
|
Level string `json:"level"`
|
|
Packages json.RawMessage `json:"packages"`
|
|
Beside []string `json:"beside"`
|
|
WithoutTests []string `json:"without_tests"`
|
|
JudgedBy string `json:"judged_by"`
|
|
}
|
|
if err := json.Unmarshal(raw, &v); err != nil {
|
|
return err
|
|
}
|
|
var pkgs []PackageDigest
|
|
if p := strings.TrimSpace(string(v.Packages)); p != "" && p != "null" {
|
|
if strings.HasPrefix(p, "{") {
|
|
var m map[string]string
|
|
if err := json.Unmarshal(v.Packages, &m); err != nil {
|
|
return fmt.Errorf("a digest's packages: %w", err)
|
|
}
|
|
for pkg, digest := range m {
|
|
pkgs = append(pkgs, PackageDigest{Package: pkg, Digest: digest})
|
|
}
|
|
} else if err := json.Unmarshal(v.Packages, &pkgs); err != nil {
|
|
return fmt.Errorf("a digest's packages: %w", err)
|
|
}
|
|
}
|
|
sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].Package < pkgs[j].Package })
|
|
*d = LevelDigest{Level: v.Level, Packages: pkgs, Beside: v.Beside, WithoutTests: v.WithoutTests, JudgedBy: v.JudgedBy}
|
|
return nil
|
|
}
|
|
|
|
// Map is the digests by package, for a lookup; nil when it names none.
|
|
func (d *LevelDigest) Map() map[string]string {
|
|
if d == nil || len(d.Packages) == 0 {
|
|
return nil
|
|
}
|
|
m := make(map[string]string, len(d.Packages))
|
|
for _, p := range d.Packages {
|
|
m[p.Package] = p.Digest
|
|
}
|
|
return m
|
|
}
|
|
|
|
// KeptTaken is a package whose kept verdict the check took instead of testing it.
|
|
type KeptTaken struct {
|
|
Package string `json:"package"`
|
|
Digest string `json:"digest"`
|
|
Tests int `json:"tests"`
|
|
}
|
|
|
|
// AskStepKey is the key, in the build module's StepsBucket, of the step that runs an ask on node: `<node>.<ask id>`
|
|
// (ADR 0525 §6). The node is the one that pulled the ask, which the asker learns from the key.
|
|
func AskStepKey(node, askID string) (string, error) {
|
|
if err := checkAskID(askID); err != nil {
|
|
return "", err
|
|
}
|
|
return StepKey(node, askID)
|
|
}
|
|
|
|
// AskStepWatch is the pattern an asker watches its ask's step with, the node a wildcard: `*.<ask id>`.
|
|
func AskStepWatch(askID string) (string, error) {
|
|
if err := checkAskID(askID); err != nil {
|
|
return "", err
|
|
}
|
|
return "*." + askID, nil
|
|
}
|
|
|
|
// ParseAskStepKey reads the node and the ask id from a step key of an ask; it refuses a key that is not `<node>.<ask
|
|
// id>`, such as a have key `<node>.<module>` or a step key a direct tool call wrote under another id form.
|
|
func ParseAskStepKey(key string) (node, askID string, err error) {
|
|
node, askID, err = ParseStepKey(key)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if err := checkAskID(askID); err != nil {
|
|
return "", "", fmt.Errorf("delivery: %q is not an ask's step key: %w", key, err)
|
|
}
|
|
return node, askID, nil
|
|
}
|
|
|
|
// PauseKey is the key, in a build module's StepsBucket, that keeps whether the module on node takes asks:
|
|
// `paused.<node>` (issue 1887's design limit 2: every holder pulls from one shared worker, so a node's pause is its holder
|
|
// not fetching, kept here so a restart keeps it). A pause key is never an ask's step key: its second part is a node.
|
|
func PauseKey(node string) (string, error) {
|
|
if err := checkName("node", node); err != nil {
|
|
return "", err
|
|
}
|
|
return "paused." + node, nil
|
|
}
|
|
|
|
// Pause is the value of a pause key: whether the node's module takes no asks, who said so and when.
|
|
type Pause struct {
|
|
V int `json:"v"`
|
|
Paused bool `json:"paused"`
|
|
By string `json:"by,omitempty"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
// EncodePause is p as JSON at Version.
|
|
func EncodePause(p Pause) ([]byte, error) {
|
|
p.V = Version
|
|
p.At = p.At.UTC()
|
|
return json.Marshal(p)
|
|
}
|
|
|
|
// DecodePause reads a pause value; it refuses an unknown version (ErrUnknownVersion).
|
|
func DecodePause(data []byte) (Pause, error) {
|
|
var p Pause
|
|
if err := decode(data, &p); err != nil {
|
|
return Pause{}, err
|
|
}
|
|
return p, nil
|
|
}
|