recovery.go RawHistoryFolder
1package domain
2
3import (
4 "encoding/hex"
5 "errors"
6 "fmt"
7 "io"
8 "slices"
9 "strings"
10)
11
12// IncarnationID permanently distinguishes uses of the same session name.
13type IncarnationID [16]byte
14
15// CheckpointRef identifies one immutable snapshot manifest.
16type CheckpointRef struct {
17 Generation uint64
18 ManifestDigest [32]byte
19}
20
21// CatalogueRecord is the durable source of truth for one named session.
22type CatalogueRecord struct {
23 Name string
24 IncarnationID IncarnationID
25 Cwd string
26 CreatedAt int64
27 UpdatedAt int64
28 LastUsedSeq uint64
29 TabNames []string
30 Committed *CheckpointRef
31 DegradedReason string
32}
33
34// Equal reports canonical value equality between checkpoint references.
35func (r *CheckpointRef) Equal(other *CheckpointRef) bool {
36 if r == nil || other == nil {
37 return r == nil && other == nil
38 }
39 return *r == *other
40}
41
42// Equal reports canonical value equality, including ordered tab names and
43// pointed-to checkpoint values rather than pointer identity.
44func (r CatalogueRecord) Equal(other CatalogueRecord) bool {
45 return r.Name == other.Name && r.IncarnationID == other.IncarnationID && r.Cwd == other.Cwd &&
46 r.CreatedAt == other.CreatedAt && r.UpdatedAt == other.UpdatedAt && r.LastUsedSeq == other.LastUsedSeq &&
47 slices.Equal(r.TabNames, other.TabNames) && r.Committed.Equal(other.Committed) &&
48 r.DegradedReason == other.DegradedReason
49}
50
51// CatalogueMetadataUpdate changes mutable runtime metadata for one catalogue
52// incarnation without carrying authority-owned recovery or checkpoint fields.
53type CatalogueMetadataUpdate struct {
54 Name string
55 IncarnationID IncarnationID
56 Cwd *string
57 UpdatedAt *int64
58 LastUsedSeq *uint64
59 TabNames *[]string
60}
61
62// MetadataUpdate returns a complete mutable-metadata update for r. Catalogue
63// implementations retain all recovery and checkpoint fields they own.
64func (r CatalogueRecord) MetadataUpdate() CatalogueMetadataUpdate {
65 cwd, updatedAt, lastUsedSeq := r.Cwd, r.UpdatedAt, r.LastUsedSeq
66 tabNames := append([]string(nil), r.TabNames...)
67 return CatalogueMetadataUpdate{
68 Name: r.Name, IncarnationID: r.IncarnationID,
69 Cwd: &cwd, UpdatedAt: &updatedAt, LastUsedSeq: &lastUsedSeq, TabNames: &tabNames,
70 }
71}
72
73// NewIncarnationID reads a new non-zero identity from r.
74func NewIncarnationID(r io.Reader) (IncarnationID, error) {
75 var id IncarnationID
76 if _, err := io.ReadFull(r, id[:]); err != nil {
77 return id, err
78 }
79 if id == (IncarnationID{}) {
80 return id, errors.New("zero incarnation ID")
81 }
82 return id, nil
83}
84
85func (id IncarnationID) String() string {
86 return hex.EncodeToString(id[:])
87}
88
89// MarshalText emits the canonical lowercase hexadecimal identity.
90func (id IncarnationID) MarshalText() ([]byte, error) {
91 if id == (IncarnationID{}) {
92 return nil, errors.New("zero incarnation ID")
93 }
94 return []byte(id.String()), nil
95}
96
97// UnmarshalText accepts only a canonical lowercase non-zero identity.
98func (id *IncarnationID) UnmarshalText(text []byte) error {
99 if len(text) != 32 || strings.ToLower(string(text)) != string(text) {
100 return errors.New("invalid incarnation ID")
101 }
102 var decoded IncarnationID
103 if _, err := hex.Decode(decoded[:], text); err != nil {
104 return fmt.Errorf("invalid incarnation ID: %w", err)
105 }
106 if decoded == (IncarnationID{}) {
107 return errors.New("zero incarnation ID")
108 }
109 *id = decoded
110 return nil
111}
112
113// Validate rejects catalogue states that cannot be restored deterministically.
114func (r CatalogueRecord) Validate() error {
115 if err := ValidateSessionName(r.Name); err != nil {
116 return err
117 }
118 if r.IncarnationID == (IncarnationID{}) {
119 return errors.New("zero catalogue incarnation ID")
120 }
121
122 if r.DegradedReason != "" && r.Committed == nil {
123 return errors.New("broken session has no committed checkpoint")
124 }
125 if r.Committed != nil {
126 if r.Committed.Generation == 0 {
127 return errors.New("zero checkpoint generation")
128 }
129 if r.Committed.ManifestDigest == ([32]byte{}) {
130 return errors.New("zero checkpoint manifest digest")
131 }
132 }
133 return nil
134}